async-lock vs lockfile vs proper-lockfile
Concurrency Control and File Locking Strategies in Node.js
async-locklockfileproper-lockfileSimilar Packages:

Concurrency Control and File Locking Strategies in Node.js

async-lock, lockfile, and proper-lockfile are essential utilities for managing concurrency in Node.js applications, but they solve different problems. async-lock is an in-memory mutex library designed to serialize asynchronous operations within a single running process, preventing race conditions on shared variables or resources. lockfile is a legacy utility for creating lock files on the filesystem to coordinate access between multiple processes, though it is now deprecated. proper-lockfile is the modern, robust successor to lockfile, offering reliable cross-process file locking with automatic stale lock detection, retry mechanisms, and updates, making it safe for production environments where multiple Node.js instances or scripts access the same files.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
async-lock042618.3 kB53 years agoMIT
lockfile0259-128 years agoISC
proper-lockfile0286-216 years agoMIT

Concurrency Control and File Locking Strategies in Node.js

Managing access to shared resources is a fundamental challenge in Node.js development. Whether you are preventing race conditions on an in-memory cache or ensuring two separate scripts don't corrupt the same log file, you need reliable locking mechanisms. The packages async-lock, lockfile, and proper-lockfile address these needs, but they operate at different levels: one manages code execution within a process, while the others manage file access across processes.

🧠 In-Memory Serialization vs. File System Locks

The most critical distinction is where the lock lives. async-lock operates entirely in memory within a single Node.js process. It acts as a mutex (mutual exclusion) to ensure that only one asynchronous function executes a critical section at a time. In contrast, lockfile and proper-lockfile create actual files on your disk (.lock files) to signal to the operating system and other processes that a resource is busy.

async-lock is used when you have shared variables in memory that multiple async functions might try to modify simultaneously.

// async-lock: Protecting in-memory state within one process
const AsyncLock = require('async-lock');
const lock = new AsyncLock();

let sharedCounter = 0;

async function increment() {
  await lock.acquire('counter-key', async () => {
    // Only one function can be here at a time
    const current = sharedCounter;
    await new Promise(r => setTimeout(r, 10)); // Simulate async work
    sharedCounter = current + 1;
  });
}

proper-lockfile creates a physical file on the disk to stop other processes from touching a target file. This works even if the processes are completely unrelated.

// proper-lockfile: Protecting a file across multiple processes
const properLockfile = require('proper-lockfile');
const fs = require('fs');

async function writeToFile() {
  // Create a lock for 'data.txt'
  const release = await properLockfile.lock('data.txt');
  
  try {
    // Safe to write; other processes are blocked
    fs.writeFileSync('data.txt', 'new content');
  } finally {
    // Always release the lock
    await release();
  }
}

lockfile attempts the same file-based approach but uses older, less reliable methods.

// lockfile: Legacy file locking (Deprecated)
const lockfile = require('lockfile');

lockfile.lock('data.txt', function (err) {
  if (err) return console.error('Could not lock');
  
  // Perform file operations
  
  // Must manually unlock
  lockfile.unlock('data.txt', function (err) {
    if (err) console.error('Failed to unlock');
  });
});

πŸ›‘οΈ Handling Crashes and Stale Locks

One of the biggest risks in file locking is the "stale lock" problem. If a process crashes or is killed (e.g., kill -9) while holding a lock, a naive locking system will leave the lock file behind forever. This blocks all future processes from ever accessing the resource again.

lockfile does not handle this well. It relies on simple file existence checks. If a process dies unexpectedly, the lock remains, and you often have to manually delete the lock file to recover. This makes it dangerous for production systems.

proper-lockfile solves this by using atomic operations and monitoring. It periodically updates the lock file's metadata while the lock is held. If the process dies, the updates stop. Other processes checking the lock will see it hasn't been updated recently and will treat it as stale, allowing them to safely steal the lock and proceed.

// proper-lockfile: Automatically handles stale locks via options
const release = await properLockfile.lock('data.txt', {
  stale: 10000, // Consider lock stale if not updated in 10s
  update: 2000, // Update lock every 2s to show we are alive
  retries: { 
    retries: 5, // Retry 5 times if lock is busy
    factor: 2,  // Exponential backoff
    maxTimeout: 1000 
  }
});

// If the previous process crashed, proper-lockfile detects it 
// and acquires the lock automatically after the 'stale' timeout.

There is no equivalent safety mechanism in async-lock because if a Node.js process crashes, all in-memory locks vanish instantly. However, async-lock does offer options to limit how long a task can wait before giving up, preventing indefinite hangs.

// async-lock: Preventing indefinite waits
await lock.acquire('key', async () => {
  // Critical section
}, {
  timeout: 5000, // Throw error if waiting longer than 5s
  maxOccupationTime: 2000 // Throw error if task runs longer than 2s
});

πŸ”„ Usage Patterns: Callbacks vs. Promises

Modern Node.js development heavily favors Promises and async/await. The API design of these libraries reflects their age and target use cases.

async-lock is built for Promises. It returns a Promise from the acquire method, making it seamless to use with modern async functions. You can also wrap the acquisition in a try/finally block to ensure locks are always released, even if an error occurs.

// async-lock: Native Promise support
async function safeOperation() {
  try {
    await lock.acquire('resource', async () => {
      await doSomethingAsync();
      await doAnotherThing();
    });
  } catch (err) {
    console.error('Lock acquisition failed or task threw', err);
  }
}

proper-lockfile also provides a clean Promise-based API. The lock method returns a Promise that resolves to a release function. This pattern is explicit and easy to reason about.

// proper-lockfile: Promise-based with explicit release function
async function safeFileWrite() {
  let release;
  try {
    release = await properLockfile.lock('config.json');
    await fs.promises.writeFile('config.json', '{"updated": true}');
  } catch (err) {
    console.error('Failed to lock or write', err);
  } finally {
    if (release) await release();
  }
}

lockfile was written in an era where callbacks were the standard. While you can wrap it in a Promise yourself, the native API requires nested callbacks, which can lead to "callback hell" and makes error handling more verbose.

// lockfile: Callback-based API (requires wrapping for async/await)
function lockFilePromise(file) {
  return new Promise((resolve, reject) => {
    lockfile.lock(file, (err) => {
      if (err) reject(err);
      else resolve(() => {
        return new Promise((res, rej) => {
          lockfile.unlock(file, (err) => err ? rej(err) : res());
        });
      });
    });
  });
}

🌐 Cross-Process Coordination Scenarios

Understanding when to use which tool comes down to your architecture.

Scenario 1: Rate Limiting API Calls in a Single Server

You have a single Node.js server that needs to call an external API, but the API only allows one request at a time per account. You need to queue these requests internally.

  • βœ… Best Choice: async-lock
  • Why? Everything happens in one process. You don't need file I/O overhead. async-lock efficiently queues the promises in memory.
// Using async-lock for rate limiting
const lock = new AsyncLock();

async function callExternalApi(userId) {
  return lock.acquire(`user-${userId}`, async () => {
    return fetch(`https://api.example.com/data?user=${userId}`);
  });
}

Scenario 2: Multiple Build Scripts Updating a Shared Cache

You have several CI/CD jobs or local developer terminals running build scripts that all read/write to the same node_modules/.cache file. These are separate processes.

  • βœ… Best Choice: proper-lockfile
  • Why? async-lock won't work because the processes don't share memory. lockfile is too risky if a build script crashes. proper-lockfile ensures only one script writes at a time and recovers automatically if a script is killed.
// Using proper-lockfile for cache safety
async function updateCache(data) {
  const release = await properLockfile.lock('cache.json', { stale: 60000 });
  try {
    const current = JSON.parse(await fs.promises.readFile('cache.json'));
    await fs.promises.writeFile('cache.json', JSON.stringify({ ...current, ...data }));
  } finally {
    await release();
  }
}

Scenario 3: Legacy Maintenance

You are maintaining an old tool written 8 years ago that uses lockfile.

  • ⚠️ Action Required: Refactor to proper-lockfile.
  • Why? Continuing to use lockfile invites hard-to-debug deadlocks in modern environments. The API is similar enough that migration is straightforward, but the reliability gain is massive.

πŸ“Š Summary: Key Differences

Featureasync-lockproper-lockfilelockfile
ScopeIn-Memory (Single Process)File System (Cross-Process)File System (Cross-Process)
Statusβœ… Active & Maintainedβœ… Active & Maintained❌ Deprecated
API StylePromises / Async-AwaitPromises / Async-AwaitCallbacks
Stale HandlingN/A (Memory clears on crash)βœ… Automatic Detection & Recovery❌ Manual Intervention Required
Retry LogicBuilt-in timeout optionsβœ… Configurable retries & backoff❌ Basic / Manual
Use CaseRace conditions on variablesCoordinating file access(Legacy only)

πŸ’‘ The Big Picture

Choosing the right locking mechanism prevents some of the hardest bugs to reproduce in distributed systems.

async-lock is your go-to for internal consistency. If your problem is "two async functions in my server are fighting over this variable," this is the lightweight, efficient solution. It adds almost no overhead and integrates perfectly with modern JavaScript syntax.

proper-lockfile is the industry standard for file safety. If your problem is "two different scripts might overwrite this file," you need a lock that lives on the disk. Its ability to detect crashed processes and clean up after them makes it robust enough for critical infrastructure, build tools, and multi-process applications.

lockfile belongs in the past. Its deprecation is a clear signal that its approach to file locking is insufficient for the reliability demands of modern software. Migrating away from it should be a priority for any team still relying on it.

Final Thought: Always match the lock scope to your problem scope. Don't use a heavy file lock for in-memory problems, and never trust a memory lock to solve cross-process conflicts.

How to Choose: async-lock vs lockfile vs proper-lockfile

  • async-lock:

    Choose async-lock when you need to prevent race conditions between asynchronous functions running within the same Node.js process. It is ideal for protecting shared in-memory state, limiting concurrency for specific tasks (like API calls), or serializing database transactions within a single server instance. Do not use it if you need to coordinate access between separate processes or different servers, as it relies entirely on memory.

  • lockfile:

    Do NOT choose lockfile for any new project. This package is officially deprecated and no longer maintained. It lacks critical features like automatic stale lock cleanup and robust error handling, which can lead to deadlocks where files remain locked forever if a process crashes. Existing projects using it should plan a migration to proper-lockfile immediately.

  • proper-lockfile:

    Choose proper-lockfile when you need to coordinate file access between multiple separate processes, such as different Node.js scripts, worker threads, or distinct server instances accessing a shared file system. It is the correct choice for build tools, cache managers, or any application where file integrity across process boundaries is critical. Its built-in retry logic and stale lock detection make it safe for production use where crashes might occur.

README for async-lock

async-lock

Lock on asynchronous code

Build Status

  • ES6 promise supported
  • Multiple keys lock supported
  • Timeout supported
  • Occupation time limit supported
  • Execution time limit supported
  • Pending task limit supported
  • Domain reentrant supported
  • 100% code coverage

Disclaimer

I did not create this package, and I will not add any features to it myself. I was granted the ownership because it was no longer being maintained, and I volunteered to fix a bug.

If you have a new feature you would like to have incorporated, please send me a PR and I will be happy to work with you and get it merged. For any bugs, PRs are most welcome but when possible I will try to get them resolved as soon as possible.

Why do you need locking on single threaded nodejs?

Nodejs is single threaded, and the code execution never gets interrupted inside an event loop, so locking is unnecessary? This is true ONLY IF your critical section can be executed inside a single event loop. However, if you have any async code inside your critical section (it can be simply triggered by any I/O operation, or timer), your critical logic will across multiple event loops, therefore it's not concurrency safe!

Consider the following code

redis.get('key', function(err, value) {
	redis.set('key', value * 2);
});

The above code simply multiply a redis key by 2. However, if two users run concurrently, the execution order may like this

user1: redis.get('key') -> 1
user2: redis.get('key') -> 1
user1: redis.set('key', 1 x 2) -> 2
user2: redis.set('key', 1 x 2) -> 2

Obviously it's not what you expected

With asyncLock, you can easily write your async critical section

lock.acquire('key', function(cb) {
	// Concurrency safe
	redis.get('key', function(err, value) {
		redis.set('key', value * 2, cb);
	});
}, function(err, ret) {
});

Get Started

var AsyncLock = require('async-lock');
var lock = new AsyncLock();

/**
 * @param {String|Array} key 	resource key or keys to lock
 * @param {function} fn 	execute function
 * @param {function} cb 	(optional) callback function, otherwise will return a promise
 * @param {Object} opts 	(optional) options
 */
lock.acquire(key, function(done) {
	// async work
	done(err, ret);
}, function(err, ret) {
	// lock released
}, opts);

// Promise mode
lock.acquire(key, function() {
	// return value or promise
}, opts).then(function() {
	// lock released
});

Error Handling

// Callback mode
lock.acquire(key, function(done) {
	done(new Error('error'));
}, function(err, ret) {
	console.log(err.message) // output: error
});

// Promise mode
lock.acquire(key, function() {
	throw new Error('error');
}).catch(function(err) {
	console.log(err.message) // output: error
});

Acquire multiple keys

lock.acquire([key1, key2], fn, cb);

Domain reentrant lock

Lock is reentrant in the same domain

var domain = require('domain');
var lock = new AsyncLock({domainReentrant : true});

var d = domain.create();
d.run(function() {
	lock.acquire('key', function() {
		//Enter lock
		return lock.acquire('key', function() {
			//Enter same lock twice
		});
	});
});

Options

// Specify timeout - max amount of time an item can remain in the queue before acquiring the lock
var lock = new AsyncLock({timeout: 5000});
lock.acquire(key, fn, function(err, ret) {
	// timed out error will be returned here if lock not acquired in given time
});

// Specify max occupation time - max amount of time allowed between entering the queue and completing execution
var lock = new AsyncLock({maxOccupationTime: 3000});
lock.acquire(key, fn, function(err, ret) {
	// occupation time exceeded error will be returned here if job not completed in given time
});

// Specify max execution time - max amount of time allowed between acquiring the lock and completing execution
var lock = new AsyncLock({maxExecutionTime: 3000});
lock.acquire(key, fn, function(err, ret) {
	// execution time exceeded error will be returned here if job not completed in given time
});

// Set max pending tasks - max number of tasks allowed in the queue at a time
var lock = new AsyncLock({maxPending: 1000});
lock.acquire(key, fn, function(err, ret) {
	// Handle too much pending error
})

// Whether there is any running or pending async function
lock.isBusy();

// Use your own promise library instead of the global Promise variable
var lock = new AsyncLock({Promise: require('bluebird')}); // Bluebird
var lock = new AsyncLock({Promise: require('q')}); // Q

// Add a task to the front of the queue waiting for a given lock
lock.acquire(key, fn1, cb); // runs immediately
lock.acquire(key, fn2, cb); // added to queue
lock.acquire(key, priorityFn, cb, {skipQueue: true}); // jumps queue and runs before fn2

Changelog

See Changelog

Issues

See issue tracker.

License

MIT, see LICENSE