redlock-universal
Universal distributed locks for Redis and Valkey

The only distributed lock library supporting all three Redis clients:
node-redis • ioredis • Valkey GLIDE
NestJS Integration: Check out nestjs-redlock-universal for decorator-based integration with dependency injection.
Quick Start
import { createLock, IoredisAdapter } from 'redlock-universal';
import Redis from 'ioredis';
const lock = createLock({
adapter: new IoredisAdapter(new Redis()),
key: 'my-resource',
ttl: 30000,
});
await lock.using(async () => {
});
npm install redlock-universal
Why redlock-universal?
Same performance as the fastest libraries, with universal client support they lack.
| redis-semaphore | 0.369ms | ❌ | ✅ | ❌ | ❌ |
| redlock-universal | 0.377ms | ✅ | ✅ | ✅ | ✅ |
| node-redlock | 0.398ms | ❌ | ✅ | ❌ | ❌ |
Benchmarked on local Redis 7 (macOS, Node.js 22). Results vary with system load, network, and Redis configuration. All libraries deliver competitive sub-millisecond performance.
Installation
npm install redlock-universal
npm install ioredis
npm install redis
npm install @valkey/valkey-glide
Core API
using() - Recommended
Auto-extends lock and guarantees cleanup:
const result = await lock.using(async (signal) => {
await processData();
if (signal.aborted) {
throw new Error('Lock lost');
}
return 'done';
});
acquire() / release() - Manual Control
const handle = await lock.acquire();
try {
await doWork();
} finally {
await lock.release(handle);
}
extend() - Extend Lock TTL
const handle = await lock.acquire();
const extended = await lock.extend(handle, 10000);
if (extended) {
}
await lock.release(handle);
Extending locks acquired via LockManager
When using LockManager, you can extend locks by creating a lock instance from the handle:
const manager = new LockManager({ nodes: [adapter], defaultTTL: 30000 });
const handle = await manager.acquireLock('my-resource');
const lock = handle.metadata?.strategy === 'redlock'
? manager.createRedLock(handle.key)
: manager.createSimpleLock(handle.key);
const extended = await lock.extend(handle, 30000);
This also works across processes — LockHandle is fully serializable:
const handle = await manager.acquireLock('my-resource');
queue.publish(JSON.stringify(handle));
const handle = JSON.parse(message);
const lock = handle.metadata?.strategy === 'redlock'
? manager.createRedLock(handle.key)
: manager.createSimpleLock(handle.key);
await lock.extend(handle, 30000);
createSimpleLock and createRedLock don't acquire anything — they create a lock object wired to the right adapter. The actual Redis operation only happens when you call .extend().
isLocked() - Check Lock Status
const locked = await lock.isLocked('my-resource');
if (!locked) {
}
Distributed Lock (Redlock Algorithm)
For fault-tolerant locking across multiple Redis instances using the Redlock algorithm:
import { createRedlock, IoredisAdapter } from 'redlock-universal';
const redlock = createRedlock({
adapters: [
new IoredisAdapter(redis1),
new IoredisAdapter(redis2),
new IoredisAdapter(redis3),
],
key: 'distributed-resource',
ttl: 30000,
quorum: 2,
});
await redlock.using(async () => {
});
Adapters & Cluster Support
Fully supports Redis Cluster via both ioredis and node-redis.
import Redis from 'ioredis';
const adapter = new IoredisAdapter(new Redis());
import { Cluster } from 'ioredis';
const cluster = new Cluster([{ host: 'node-1', port: 6379 }]);
const adapter = new IoredisAdapter(cluster);
import { createClient } from 'redis';
const client = createClient();
await client.connect();
const adapter = new NodeRedisAdapter(client);
import { createCluster } from 'redis';
const cluster = createCluster({ rootNodes: [{ url: 'redis://node-1:6379' }] });
await cluster.connect();
const adapter = new NodeRedisAdapter(cluster);
import { GlideClient } from '@valkey/valkey-glide';
const client = await GlideClient.createClient({ addresses: [{ host: 'localhost', port: 6379 }] });
const adapter = new GlideAdapter(client);
Valkey Users: See VALKEY.md for detailed Valkey setup guide.
[!IMPORTANT]
Cluster vs Redlock:
- Redis Cluster: Provides High Availability (HA). If a master fails, a replica takes over. Warning: Locks can be lost during failover (eventual consistency).
- Redlock: Provides Consensus. Locks are safe even if nodes crash. Use for critical consistency.
See Cluster Usage Examples for details.
Configuration
interface CreateLockConfig {
adapter: RedisAdapter;
key: string;
ttl?: number;
retryAttempts?: number;
retryDelay?: number;
performance?: 'standard' | 'lean' | 'enterprise';
logger?: ILogger;
circuitBreaker?: boolean | CircuitBreakerConfig;
}
interface CreateRedlockConfig {
adapters: RedisAdapter[];
key: string;
ttl?: number;
retryAttempts?: number;
retryDelay?: number;
quorum?: number;
clockDriftFactor?: number;
logger?: ILogger;
}
Advanced: Batch Lock Acquisition
Acquire multiple locks atomically (all-or-nothing):
import { LockManager } from 'redlock-universal';
const manager = new LockManager({ nodes: [adapter] });
const handles = await manager.acquireBatch(['user:1', 'user:2', 'order:3']);
try {
await processTransaction();
} finally {
await manager.releaseBatch(handles);
}
await manager.usingBatch(['key1', 'key2'], async (signal) => {
});
Advanced: Logger Integration
import { Logger, LogLevel } from 'redlock-universal';
const logger = new Logger({
level: LogLevel.INFO,
prefix: 'redlock',
enableConsole: true,
});
const lock = createLock({ adapter, key: 'resource', logger });
External loggers:
| Winston | Yes | No |
| Console | Yes | No |
| Pino | Via Adapter | createPinoAdapter() |
| Bunyan | Via Adapter | createBunyanAdapter() |
import { createPinoAdapter } from 'redlock-universal';
const logger = createPinoAdapter(pinoLogger);
import { createBunyanAdapter } from 'redlock-universal';
const logger = createBunyanAdapter(bunyanLogger);
Advanced: Lock Inspection
Debug stuck locks:
const inspection = await adapter.inspect('my-resource');
if (inspection) {
console.log('Owner:', inspection.value);
console.log('TTL:', inspection.ttl, 'ms');
}
Advanced: Testing with MemoryAdapter
Unit tests without Redis:
import { MemoryAdapter, createLock } from 'redlock-universal';
const adapter = new MemoryAdapter();
const lock = createLock({ adapter, key: 'test', ttl: 5000 });
const handle = await lock.acquire();
await lock.release(handle);
adapter.clear();
await adapter.disconnect();
[!WARNING]
MemoryAdapter is for testing only. Not suitable for production.
Advanced: Factory Functions
Create multiple locks or specialized configurations:
import { createLocks, createPrefixedLock, createRedlocks } from 'redlock-universal';
const locks = createLocks(adapter, ['user:123', 'account:456'], {
ttl: 15000,
retryAttempts: 5,
});
const userLock = createPrefixedLock(adapter, 'locks:user:', '123', {
ttl: 10000,
});
const redlocks = createRedlocks(
[adapter1, adapter2, adapter3],
['resource1', 'resource2'],
{ ttl: 15000, quorum: 2 }
);
Advanced: Performance Modes
Choose the optimal mode for your use case:
const lock = createLock({ adapter, key: 'resource', performance: 'standard' });
const lock = createLock({ adapter, key: 'resource', performance: 'lean' });
const lock = createLock({ adapter, key: 'resource', performance: 'enterprise' });
Advanced: Circuit Breaker
SimpleLock includes a built-in circuit breaker that fast-fails when Redis is persistently unavailable, avoiding long TCP timeouts on every acquire() call.
States: Closed (normal) → Open (fast-fail) → Half-Open (single probe) → Closed
const lock = createLock({
adapter, key: 'resource',
circuitBreaker: {
failureThreshold: 5,
resetTimeout: 60000,
healthCheckInterval: 30000
}
});
const lock = createLock({ adapter, key: 'resource', circuitBreaker: false });
const health = lock.getHealth();
The circuit breaker is available in standard and enterprise performance modes. The lean mode omits it for minimal memory overhead.
Error Handling
import {
LockAcquisitionError,
LockReleaseError,
LockExtensionError,
} from 'redlock-universal';
try {
const handle = await lock.acquire();
await lock.extend(handle, 10000);
await lock.release(handle);
} catch (error) {
if (error instanceof LockAcquisitionError) {
} else if (error instanceof LockExtensionError) {
} else if (error instanceof LockReleaseError) {
}
}
FAQ
Q: SimpleLock vs RedLock?
SimpleLock = single Redis (faster). RedLock = multiple Redis instances (fault-tolerant).
Q: What happens if Redis restarts?
Lua scripts auto-reload on NOSCRIPT errors. No action needed.
Q: Performance overhead of auto-extension?
Minimal (<1ms). Uses atomic Lua scripts.
Best Practices
- Use
using() over manual acquire/release - guarantees cleanup, handles auto-extension
- Set appropriate TTL - long enough for work, short enough for quick recovery
- Handle
signal.aborted - gracefully exit when lock is lost during long operations
- Use unique lock keys - namespace by resource type (e.g.,
user:123:cart)
- Monitor lock metrics - track acquisition failures and extension patterns
Testing
npm test
npm run test:integration
npm run test:coverage
npm run test:docker
Troubleshooting
[!WARNING]
Lock not releasing? Ensure handle matches stored value. Check if TTL expired before release.
[!WARNING]
High P99 latency? Check Redis server load. Consider performance: 'lean' mode.
Migration
From node-redlock
const redlock = new Redlock([ioredis], { retryCount: 3 });
const lock = await redlock.acquire(['resource'], 30000);
await lock.release();
const redlock = createRedlock({
adapters: [new IoredisAdapter(ioredis)],
key: 'resource',
ttl: 30000,
retryAttempts: 3,
});
const handle = await redlock.acquire();
await redlock.release(handle);
From redis-semaphore
const mutex = new Mutex(ioredis, 'resource');
await mutex.acquire();
await mutex.release();
const lock = createLock({
adapter: new IoredisAdapter(ioredis),
key: 'resource',
});
const handle = await lock.acquire();
await lock.release(handle);
Links
Contributing
See CONTRIBUTING.md. Issues and PRs welcome.
License
MIT