diff --git a/Store/src/DataToolkitStore.jl b/Store/src/DataToolkitStore.jl
index 00d8a40e..c55fe37f 100644
--- a/Store/src/DataToolkitStore.jl
+++ b/Store/src/DataToolkitStore.jl
@@ -10,6 +10,7 @@ using Compat
 
 @compat public load_inventory, fetch!
 
+include("lockfile.jl")
 include("types.jl")
 
 const INVENTORY_VERSION = 0
diff --git a/Store/src/inventory.jl b/Store/src/inventory.jl
index 1811b47b..3045a284 100644
--- a/Store/src/inventory.jl
+++ b/Store/src/inventory.jl
@@ -29,10 +29,12 @@ function load_inventory(path::String, create::Bool=true)
                     for (key, val) in get(data, "collections", Dict{String, Any}[])]
         stores = map(s -> convert(StoreSource, s), get(data, "store", Dict{String, Any}[]))
         caches = map(c -> convert(CacheSource, c), get(data, "cache", Dict{String, Any}[]))
-        Inventory(file, cmerkle, config, collections, stores, caches, last_gc)
+        Inventory(file, LockFile(path, "Inventory"), cmerkle,
+                  config, collections, stores, caches, last_gc)
     elseif create
         inventory = Inventory(
             MonitoredFile(path),
+            LockFile(path, "Inventory"),
             CachedMerkles(MonitoredFile(
                 joinpath(dirname(path), DEFAULT_INVENTORY_CONFIG.store_dir, MERKLE_FILENAME)), []),
             convert(InventoryConfig, Dict{String, Any}()),
diff --git a/Store/src/lockfile.jl b/Store/src/lockfile.jl
new file mode 100644
index 00000000..955cb24f
--- /dev/null
+++ b/Store/src/lockfile.jl
@@ -0,0 +1,76 @@
+"""
+    LockFile(path::String) -> LockFile
+
+A file-based lock that can be used to synchronise access to a resource across
+multiple processes. The lock is implemented using a file at `path` that is
+created when the lock is acquired and deleted when the lock is released.
+"""
+struct LockFile <: Base.AbstractLock
+    path::String
+    owned::ReentrantLock
+end
+
+function LockFile(prefix::String, target)
+    path = BaseDirs.User.runtime(
+        PROJECT_SUBPATH,
+        prefix * "-" * string(hash(inv.file.path), base=32) * ".lock")
+    LockFile(path, ReentrantLock())
+end
+
+function Base.islocked(lf::LockFile)
+    islocked(lf.owned) && return true
+    isfile(lf.path) || return false
+    pid = open(io -> if eof(io) 0 else read(io, Int) end, lf.path)
+    iszero(@ccall uv_kill(pid::Cint, 0::Cint)::Cint)
+end
+
+function Base.trylock(lf::LockFile)
+    islocked(lf.owned) && return trylock(lf.owned)
+    islocked(lf) && return false
+    ispath(dirname(lf.path)) || mkpath(dirname(lf.path))
+    try
+        write(lf.path, UInt(getpid()))
+        chmod(lf.path, 0o444)
+    catch _
+        return false
+    end
+    lock(lf.owned)
+    true
+end
+
+function Base.lock(lf::LockFile)
+    backoff = 0.00001 # 10μs, given that it takes 5μs lock + unlock on my machine
+    while !trylock(lf)
+        quicksleep(backoff)
+        backoff = min(0.05, backoff * 2)
+    end
+end
+
+function Base.unlock(lf::LockFile)
+    backoff = 0.00001 # 10μs, given that it takes 5μs to lock + unlock on my machine
+    if !islocked(lf.owned)
+        while islocked(lf)
+            quicksleep(backoff)
+            backoff = min(0.05, backoff * 2)
+        end
+    end
+    rm(lf.path, force=true)
+    unlock(lf.owned)
+end
+
+# REVIEW: Only needed until something like <https://github.com/JuliaLang/julia/pull/55163> lands.
+"""
+    quicksleep(period::Real)
+
+Sleep for `period` seconds, but use a busy loop for short periods (< 2ms).
+"""
+function quicksleep(period::Real)
+    if period < 0.02
+        start = time()
+        while time() - start <= period
+            yield()
+        end
+    else
+        sleep(period)
+    end
+end
diff --git a/Store/src/types.jl b/Store/src/types.jl
index cac7baa6..bcecb579 100644
--- a/Store/src/types.jl
+++ b/Store/src/types.jl
@@ -57,6 +57,7 @@ end
 
 mutable struct Inventory
     const file::MonitoredFile
+    const lock::LockFile
     const merkles::CachedMerkles
     config::InventoryConfig
     collections::Vector{CollectionInfo}