async-lock

A lightweight mutex library that serializes asynchronous code in Node.js, with promise and callback support.

Library
npm
v1.4.1
426stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
41/100Fair
Development Activity0
Maintenance20
Community64
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
61/100Good
Architecture72
Code Quality65
Innovation60
Learning Curve45

async-lock solves a problem that’s easy to miss on a single-threaded runtime: code that looks atomic but actually spans multiple event loops (any await, callback, or timer inside it) is not concurrency-safe. A classic read-modify-write against Redis or a shared in-memory value can interleave between two concurrent callers and silently produce the wrong result. async-lock wraps that kind of critical section in a per-key queue so only one caller executes it at a time, while everything not sharing that key runs unimpeded.

The library is a single dependency-free module exposing one core method, acquire(key, fn, cb, opts), that works equally well with Node-style callbacks or as a promise depending on the arity of the function you pass in. It supports locking on multiple keys at once, per-call or per-instance timeouts so callers don’t hang forever waiting on a stuck section, and an opt-in domain-reentrant mode for code that needs to re-acquire a lock it already holds. It has been around since 2016, is widely used as a building block wherever Node.js code needs simple in-process mutual exclusion, and its API surface has stayed intentionally small and stable.

What You Get

  • A single acquire(key, fn, cb, opts) method that locks work on one key or an array of keys, auto-detecting callback vs. promise mode from the function you pass in.
  • Per-lock or per-instance timeout, maxOccupationTime, and maxExecutionTime options so a stuck critical section fails with an error instead of hanging indefinitely.
  • A maxPending cap plus a skipQueue option to bound how many callers can wait on a key and let priority work jump the queue.
  • Optional domainReentrant mode so code that already holds a lock within the same Node.js domain can safely re-enter it.
  • An isBusy(key) check to inspect whether a given key (or any key) currently has running or queued work.

Common Use Cases

  • Serializing read-modify-write sequences against a shared cache or database key so concurrent requests don’t race and overwrite each other’s updates.
  • Coordinating outbound calls to a rate-limited external API on a per-account or per-resource basis so overlapping requests from the same caller don’t fire simultaneously.
  • Guarding lazy singleton or cache-warming initialization so multiple concurrent requests don’t each trigger duplicate expensive setup work.
  • Protecting file or record updates in worker processes so parallel workers don’t produce lost updates on the same record.

Under The Hood

Architecture The entire library lives in lib/index.js behind a one-line re-export in the root index.js. AsyncLock is a constructor holding a plain-object map of per-key queues (this.queues); acquire() either runs a function immediately if the key’s queue is empty or pushes a closure onto that key’s queue otherwise, and the internal done() callback clears timers, resolves the caller’s callback or promise, and shifts the next queued closure off that key when the current one finishes. Multi-key locking (_acquireBatch) is not a separate mechanism — it composes nested single-key acquire() calls via Array.reduceRight, so a lock on [key1, key2, key3] is really three recursive single-key acquires chained together. There is no external state or persistence; everything lives in the instance’s in-memory queue map, which is what limits the library to single-process coordination.

Tech Stack Plain, dependency-free ES5-style JavaScript (‘use strict’, prototype methods, no build or transpile step) distributed as a CommonJS module. The project is built and tested with Grunt (gruntfile.js) driving grunt-contrib-jshint for linting and grunt-mocha-test for the test suite; bluebird, q, lodash, and should appear only as devDependencies used inside tests to exercise alternate promise libraries and assertions. It targets Node.js specifically, since the optional domainReentrant mode depends on the Node-only process.domain API.

Code Quality A single test/test.js mocha/should suite (roughly 300 lines) exercises single-key and multi-key locking, timeout, maxOccupationTime, maxExecutionTime, isBusy, and domain-reentrant scenarios, run through grunt test (lint then mochaTest) with a Travis CI badge in the README. Errors are handled explicitly — user function exceptions are caught and routed through the same done(locked, err) path as a normal callback error. There are no TypeScript types shipped in the package itself (consumers rely on community @types/async-lock definitions), and the README states plainly that the current maintainer only merges PRs and does not add features, which lines up with the repository’s low recent commit activity.

API Design The public surface is deliberately minimal: one acquire() method (plus isBusy()) that infers callback-vs-promise mode from the arity of the function argument, so the same call site adapts to either style without extra configuration. Multi-key locks reuse the exact same method by accepting an array instead of a single key, avoiding a second API for what is conceptually the same operation. This keeps the learning curve low — most usage is copy-pasteable from the README’s acquire(key, fn, cb) pattern — at the cost of a somewhat dated calling convention (arity-based mode detection) that a modern promise-first API might avoid.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search