mrkeyoor.com_
Sat 08 Aug 22:49 UTC
npmUtilsupdated 08 Aug 2026

lockfile

lockfile is a small mutual-exclusion helper for processes on one machine. Acquiring a lock means creating a file with the exclusive-create flag, so exactly one caller wins and everyone else gets EEXIST; releasing means unlinking it. On top of that it adds polling with a timeout, a retry count, and an age-based staleness rule that lets a caller steal a lock left behind by a process that died. It has nothing to do with package-manager lockfiles such as package-lock.json, despite living under the npm organisation, and it registers a process-exit hook so its own locks get cleaned up on a normal shutdown.

Verdict

A clear, well-scoped exclusive-create lock that does exactly what its README says, on one local filesystem, and has been archived since 2020 with no release since 2018. For new code proper-lockfile covers the same ground with promises, ownership checks and a heartbeat, and is still maintained.

API stability5/5Six functions, lock and lockSync, their two release counterparts, check and checkSync, with the same five options since 1.0.0 in August 2014. The 1.0.4 publish in April 2018 was the last change, and the repository is archived, so nothing can shift underneath you. Anything written against it a decade ago behaves identically today. The one wart that will never be fixed is that lock mutates the options object you hand it, which is a stability guarantee working against you.
Docs3/5The README covers every function and every option in about eighty lines, and the descriptions are accurate. What it leaves out is what actually bites: that the options object is mutated, that the lock file is empty so you cannot identify the holder, that check with stale set reports an old lock as free, that exclusive create is unreliable on network filesystems, and that exit cleanup depends on the process getting a chance to run. All of that is visible in lockfile.js, which is short enough to read in full.
Maintenance1/5The GitHub repository has been archived since 2020-12-18 and is read-only with twelve open issues. The last npm publish was 1.0.4 on 2018-04-17, and the only dependency, signal-exit, is on the 3.x line that has since had a 4.x. It sits under the npm organisation, which reads as an endorsement it no longer carries, since the npm CLI itself has moved on. Roughly 4.3 million weekly downloads reflect old dependency trees rather than active investment.
Ecosystem2/5About 4.3 million weekly downloads, mostly transitive through older build tooling and generators. There are no TypeScript declarations bundled and no DefinitelyTyped package in wide use, no promise wrapper published alongside it, and no plugins or adapters. Anything you learn is about the exclusive-create pattern rather than about this library, and it transfers directly to proper-lockfile, which is where most of the maintained ecosystem around file locking now sits.

Use it if

  • Two or more processes on the same filesystem must take turns over a resource such as a cache directory, a generated file or a migration step
  • You want the lock file itself as the coordination artefact so an operator can see it, and delete it, without any daemon or service running
  • The callback and sync API suits you, because lockSync and checkSync fit build scripts and CLI tools that are not built around promises
  • You need best-effort cleanup on exit and on signals without wiring signal handling yourself
Skip it if

Setup reality

npm install lockfile brings in one dependency, signal-exit, and no native build. Writing the first lock takes two minutes; the surprises are all in the options. The biggest one is that lock and lockSync mutate the options object you pass in. Calling lock with { wait: 500, retries: 2, stale: 10000 } leaves that same object as { wait: 500, retries: 0, stale: 10000, req: 1, start: <timestamp> }, so a shared config object silently loses its retries after the first call and every later acquisition tries exactly once. Build a fresh object per call. Next, the lock file carries no information. It is created and immediately closed, size zero, so there is no pid to check and no way to ask whether the holder is alive; the only signal is age. That makes opts.stale a guess, and getting it wrong in either direction hurts: too low and you steal a lock from a process that is merely slow, too high and a crashed process blocks everyone until the timeout expires. Staleness compares against ctime on POSIX and mtime on Windows, exposed as exports.filetime, and lockSync rounds the threshold up to whole seconds when the filesystem looks like it lacks sub-second resolution. Note also what check means once stale is set: a lock older than the threshold reports as not locked, so check is answering can I take this, not is a file there. The sync functions refuse anything that would need to wait, throwing 'opts.wait not supported sync for obvious reasons' if you pass wait or retryWait. Cleanup is best effort: locks acquired in this process are unlinked from a signal-exit hook, which covers normal exits and most signals but not SIGKILL, an OOM kill or a power loss, and the release call will happily remove a lock file that some other process created. Because the whole mechanism rests on fs.open with the exclusive-create flag, correctness ends at the boundary of a local filesystem.

Patterns

Take a lock, do work, release itacquire-and-release

const lockFile = require('lockfile')

lockFile.lock('build.lock', { wait: 5000 }, (err) => {
  if (err) return done(err)
  doWork((workErr) => {
    lockFile.unlock('build.lock', (unlockErr) => done(workErr || unlockErr))
  })
})

Release in every exit path. The exit hook is a safety net for crashes, not a substitute for releasing when your work finishes.

Never reuse one options objectfresh-options-per-call

const lockFile = require('lockfile')

// wrong: retries is reset to 0 after the first call
const shared = { wait: 500, retries: 2, stale: 10000 }

// right
const opts = () => ({ wait: 500, retries: 2, stale: 10000 })
lockFile.lock('a.lock', opts(), onLocked)
lockFile.lock('b.lock', opts(), onLocked)

lock() writes req and start into the object and sets retries to 0. Verified on 1.0.4: a shared object loses its retries silently.

Wrap the callbacks in promisespromise-wrapper

const lockFile = require('lockfile')
const { promisify } = require('node:util')

const lock = promisify(lockFile.lock)
const unlock = promisify(lockFile.unlock)

await lock('build.lock', { wait: 5000, stale: 60000 })
try {
  await doWork()
} finally {
  await unlock('build.lock')
}

promisify works because the callbacks are error-first. Keep building the options object inline so the mutation cannot leak between calls.

Queue behind another processwait-with-polling

const lockFile = require('lockfile')

lockFile.lock('migrate.lock', { wait: 30000, pollPeriod: 250 }, (err) => {
  if (err) return console.error('gave up waiting:', err.code)
  runMigrations(release)
})

wait is the total budget and pollPeriod defaults to 100 ms. On timeout the callback gets the original EEXIST error, not a distinct timeout error.

Take over a lock left by a dead processsteal-stale-lock

const lockFile = require('lockfile')

lockFile.lock('job.lock', { stale: 60000, wait: 5000 }, (err) => {
  if (err) return bail(err)
  runJob(release)
})

Age is compared against ctime, or mtime on Windows. Set stale well above your worst-case runtime, or a slow run gets its lock stolen while it is still working.

Ask whether a lock is heldcheck-before-locking

const lockFile = require('lockfile')

lockFile.check('job.lock', { stale: 60000 }, (err, isLocked) => {
  if (err) return bail(err)
  console.log(isLocked ? 'held' : 'free or stale')
})

With stale set, an old lock reports false. Treat the answer as advisory: another process can take the lock between your check and your lock call.

Lock synchronously in a build stepsync-in-scripts

const lockFile = require('lockfile')

try {
  lockFile.lockSync('build.lock', { retries: 3, stale: 30000 })
  buildEverything()
} finally {
  lockFile.unlockSync('build.lock')
}

lockSync throws 'opts.wait not supported sync for obvious reasons' if you pass wait or retryWait. retries is the only backoff available here, and it spins without sleeping.

Expect EEXIST from your own processnot-reentrant

const lockFile = require('lockfile')

lockFile.lock('a.lock', {}, (err) => {
  lockFile.lock('a.lock', {}, (again) => {
    console.log(again && again.code) // 'EEXIST'
  })
})

There is no owner tracking, so the second call fails the same way a different process would. Guard in-process reentry with your own flag or a mutex.

See how little is in the lock fileinspect-the-lock-file

const fs = require('node:fs')
const lockFile = require('lockfile')

lockFile.lockSync('a.lock')
console.log(fs.statSync('a.lock').size)   // 0
console.log(lockFile.filetime)            // 'ctime', or 'mtime' on win32

Zero bytes means no pid and no hostname. If an operator needs to know who is holding it, write a sidecar file yourself.

Know what the exit hook does and does not covercleanup-on-exit

const lockFile = require('lockfile')

lockFile.lockSync('a.lock')
// signal-exit unlinks locks taken in this process on normal exit,
// on uncaught exceptions and on catchable signals.
// SIGKILL, an OOM kill and power loss leave the file behind.
process.on('SIGTERM', () => process.exit(0))

Set opts.stale so a leftover file is eventually recoverable; without it a hard kill blocks every future acquisition forever.

Keep the lock on a local filesystemavoid-network-fs

const os = require('node:os')
const path = require('node:path')
const lockFile = require('lockfile')

// not on an NFS or SMB mount
const lockPath = path.join(os.tmpdir(), 'my-app.lock')
lockFile.lockSync(lockPath, { stale: 30000 })

The whole guarantee is fs.open with the exclusive-create flag, which network filesystems do not implement reliably. Two hosts can both believe they won.

Move to a maintained equivalentmigrate-to-proper-lockfile

const lockfile = require('proper-lockfile')

const release = await lockfile.lock('resource-dir', {
  stale: 30000,
  retries: { retries: 5, minTimeout: 100 },
})
try {
  await doWork()
} finally {
  await release()
}

It locks a path you already have rather than a separate file, refreshes mtime while you hold it so a slow job is not judged stale, and checks ownership before releasing.

Alternatives

PackageRegistryPick it when
proper-lockfilenpmYou want the same file-based idea with promises, an mtime heartbeat so live holders refresh their lock, and releases that verify ownership first
async-mutexnpmThe contention is between async tasks inside one Node process, where a filesystem lock is the wrong tool entirely
redlocknpmThe processes are on different machines and you need a lock backed by Redis rather than by a shared filesystem
fs-extnpmYou want real advisory locking through flock, with kernel-released locks when the process dies, and can accept a native build step