A lightweight, zero-dependency mutex built on the Web Locks API, with keyed locks, cancellation, timeouts, and explicit resource management.
- Automatic release: Use the
usingkeyword to release a lock when its scope ends. - Keyed locks:
acquire()calls using the sameMutexinstance and key are serialized in FIFO order, while different keys can run concurrently. - Timeouts and cancellation: Stop waiting with a timeout or an
AbortSignal. - Web Locks options: Use
Mutex.request()to passLockOptionsdirectly tonavigator.locks.request().
- Node.js 24+
- Chrome 134+
- Firefox 141+
Other runtimes must support using, Symbol.dispose, Promise.withResolvers, crypto.randomUUID, AbortSignal, and navigator.locks.request.
npm install using-muteximport { Mutex } from 'using-mutex'
const mutex = new Mutex()
async function updateResource() {
// The lock is released automatically when this scope exits.
using lock = await mutex.acquire()
// Perform mutually exclusive work here.
}
await Promise.all([updateResource(), updateResource()])MutexGuard.release() can also be used when the lock needs to be released before the scope ends. Calling it more than once has no effect.
const lock = await mutex.acquire()
try {
// Perform mutually exclusive work here.
} finally {
lock.release()
}async function updateUser(userId) {
using lock = await mutex.acquire(`user:${userId}`)
// Calls for the same user are serialized.
// Calls for different users can run concurrently.
}Keys are scoped to a Mutex instance. Two separate Mutex instances do not contend with each other, even when they use the same key.
async function withTimeout() {
// Stop waiting after 1,000 milliseconds.
using lock = await mutex.acquire('resource', 1_000)
// The timeout only applies while waiting to acquire the lock.
}
async function withCancellation(signal) {
// Or control cancellation with an AbortSignal.
using lock = await mutex.acquire('resource', signal)
}
const controller = new AbortController()
await withCancellation(controller.signal)A timed-out acquisition rejects with a DOMException named TimeoutError. An aborted acquisition rejects with the signal's reason.
const controller = new AbortController()
using lock = await mutex.request('resource', {
mode: 'exclusive',
signal: controller.signal,
})request() namespaces the key for the current Mutex instance, passes options to navigator.locks.request(), and holds the lock until the returned guard is released.
acquire(): Promise<MutexGuard>
acquire(timeout: number): Promise<MutexGuard>
acquire(signal: AbortSignal): Promise<MutexGuard>
acquire(key: string): Promise<MutexGuard>
acquire(key: string, timeout: number): Promise<MutexGuard>
acquire(key: string, signal: AbortSignal): Promise<MutexGuard>Acquires an exclusive lock. Omitting key uses the instance's default key. A positive numeric timeout is interpreted in milliseconds.
request(key: string, options?: LockOptions): Promise<MutexGuard>Acquires a keyed lock using the supplied Web Locks API options.
class MutexGuard implements Disposable {
get key(): string
release(): void
[Symbol.dispose](): void
}Represents an acquired lock. Release it with using, release(), or Symbol.dispose.