Distributed read/write locks with Redis - queue-based, no polling, immediate acquisition
npm install redis-rwlock


A production-ready distributed read/write lock library for Node.js using Redis. Features queue-based waiting with immediate lock acquisition on release (no polling or retrying).
- 🔐 Read/Write Locks - Multiple readers OR single writer
- 📋 Queue-Based Waiting - FIFO ordering, no polling needed
- ⚡ Immediate Acquisition - Waiters notified instantly when lock is released
- ⏱️ Per-Acquisition Timeouts - Customize lock duration and wait timeout per call
- 🔄 Lock Extension - Extend lock duration while holding it
- ✍️ Writer Priority - Prevent writer starvation (configurable)
- 🛡️ Auto-Release - Locks expire automatically to prevent deadlocks
- 🔌 Connection Recovery - Handles Redis reconnection gracefully
- 📦 TypeScript - Full type definitions included
``bash`
npm install redis-rwlock ioredis
`typescript
import Redis from 'ioredis';
import { createLockManager } from 'redis-rwlock';
const redis = new Redis();
const lockManager = createLockManager(redis);
// Acquire a write lock
const lock = await lockManager.acquire('my-resource', 'write');
try {
// Critical section - you have exclusive access
await updateDatabase();
} finally {
await lock.release();
}
`
`typescript
import { createLockManager } from 'redis-rwlock';
const lockManager = createLockManager(redis, {
keyPrefix: 'rwlock', // Redis key prefix (default: 'rwlock')
defaultLockDuration: 30000, // Default lock TTL in ms (default: 30000)
defaultWaitTimeout: 10000, // Default max wait time in ms (default: 10000)
writerPriority: true, // Block new readers when writer waiting (default: true)
cleanupInterval: 30000, // Background cleanup interval (default: 30000)
debug: false, // Enable debug logging (default: false)
});
`
`typescript
// Write lock (exclusive)
const writeLock = await lockManager.acquire('resource', 'write');
// Read lock (shared)
const readLock = await lockManager.acquire('resource', 'read');
// With custom options
const lock = await lockManager.acquire('resource', 'write', {
lockDuration: 60000, // Hold for 60 seconds
waitTimeout: 5000, // Wait max 5 seconds
onQueued: (position) => console.log(Queued at position ${position}),`
});
`typescript
const lock = await lockManager.tryAcquire('resource', 'write');
if (lock) {
// Got the lock
await lock.release();
} else {
// Lock not available
}
`
`typescript
// Check if still valid
if (lock.isValid()) {
// Still holding the lock
}
// Get remaining time
const remainingMs = lock.remainingTime();
// Extend the lock
const newExpiry = await lock.extend(30000); // Add 30 seconds
// Release the lock
await lock.release();
`
`typescript
// Check if locked
const isLocked = await lockManager.isLocked('resource');
// Get detailed info
const info = await lockManager.getLockInfo('resource');
// Returns: { type, holders, expiresAt, queueLength, hasWriterWaiting }
// Get queue length
const queueLength = await lockManager.getQueueLength('resource');
`
`typescript`
// Releases all locks and cleans up
await lockManager.shutdown();
`typescript
import {
AcquisitionTimeoutError,
NotHeldError,
LockExpiredError
} from 'redis-rwlock';
try {
const lock = await lockManager.acquire('resource', 'write', {
waitTimeout: 1000,
});
} catch (error) {
if (error instanceof AcquisitionTimeoutError) {
console.log(Timed out waiting for ${error.resource});`
}
}
Unlike traditional lock implementations that use polling/retrying, redis-rwlock uses:
1. Sorted Sets for the waiter queue (score = arrival time for FIFO)
2. Pub/Sub for instant notification when locks are released
3. Lua Scripts for atomic operations
When a lock is released:
1. The release script atomically grants the lock to the next waiter(s)
2. Notifications are published to wake up the waiters
3. Waiters verify their status and return the lock handle
- Read locks are shared - multiple readers can hold simultaneously
- Write locks are exclusive - only one writer, no readers
- When a write lock is released, consecutive read locks at the queue head are granted together
With writerPriority: true (default):
- Once a writer is waiting, new readers must queue behind it
- Prevents writer starvation in read-heavy workloads
`typescript`
const lock = await lockManager.acquire('resource', 'write');
try {
await doWork();
} finally {
await lock.release();
}
`typescript
// Short operations: shorter lock duration
const lock = await lockManager.acquire('resource', 'write', {
lockDuration: 5000, // 5 seconds max
waitTimeout: 2000, // Don't wait too long
});
// Long operations: longer duration, extend if needed
const lock = await lockManager.acquire('resource', 'write', {
lockDuration: 60000,
});
// Extend before expiring
if (lock.remainingTime() < 10000) {
await lock.extend(30000);
}
`
`typescript``
try {
const lock = await lockManager.acquire('resource', 'write', {
waitTimeout: 1000,
});
// ... use lock
} catch (error) {
if (error instanceof AcquisitionTimeoutError) {
// Return 503 or retry later
return res.status(503).json({ error: 'Resource busy' });
}
throw error;
}
Contributions are welcome! Please read our Contributing Guide for details.
MIT © [Your Name]