redlock-universal
    Preparing search index...

    redlock-universal

    redlock-universal

    Universal distributed locks for Redis and Valkey

    npm version Node.js TypeScript Test Coverage License: MIT Downloads Sponsor

    The only distributed lock library supporting all three Redis clients:

    node-redis • ioredis • Valkey GLIDE

    NestJS NestJS Integration: Check out nestjs-redlock-universal for decorator-based integration with dependency injection.

    import { createLock, IoredisAdapter } from 'redlock-universal';
    import Redis from 'ioredis';

    const lock = createLock({
    adapter: new IoredisAdapter(new Redis()),
    key: 'my-resource',
    ttl: 30000,
    });

    // Automatic lock management with auto-extension
    await lock.using(async () => {
    // Critical section - lock auto-extends and releases
    });
    npm install redlock-universal
    

    Same performance as the fastest libraries, with universal client support they lack.

    Library Latency node-redis ioredis Valkey Monitoring
    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.

    npm install redlock-universal

    # Install your preferred Redis client
    npm install ioredis # or
    npm install redis # or
    npm install @valkey/valkey-glide

    Auto-extends lock and guarantees cleanup:

    const result = await lock.using(async (signal) => {
    // Long-running operation
    await processData();

    // Check if lock was lost (optional)
    if (signal.aborted) {
    throw new Error('Lock lost');
    }

    return 'done';
    });
    const handle = await lock.acquire();
    try {
    await doWork();
    } finally {
    await lock.release(handle);
    }
    const handle = await lock.acquire();
    // Extend lock by 10 seconds
    const extended = await lock.extend(handle, 10000);
    if (extended) {
    // Continue working with extended TTL
    }
    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');

    // Create a lock instance using the handle's strategy
    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:

    // Process A: acquire and pass handle to another process
    const handle = await manager.acquireLock('my-resource');
    queue.publish(JSON.stringify(handle));

    // Process B: receive handle and extend
    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().

    const locked = await lock.isLocked('my-resource');
    if (!locked) {
    // Safe to acquire
    }

    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 () => {
    // Distributed consensus - survives node failures
    });

    Fully supports Redis Cluster via both ioredis and node-redis.

    // ioredis
    import Redis from 'ioredis';
    const adapter = new IoredisAdapter(new Redis());

    // ioredis Cluster
    import { Cluster } from 'ioredis';
    const cluster = new Cluster([{ host: 'node-1', port: 6379 }]);
    const adapter = new IoredisAdapter(cluster);

    // node-redis
    import { createClient } from 'redis';
    const client = createClient();
    await client.connect();
    const adapter = new NodeRedisAdapter(client);

    // node-redis Cluster
    import { createCluster } from 'redis';
    const cluster = createCluster({ rootNodes: [{ url: 'redis://node-1:6379' }] });
    await cluster.connect();
    const adapter = new NodeRedisAdapter(cluster);

    // Valkey GLIDE
    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.

    interface CreateLockConfig {
    adapter: RedisAdapter;
    key: string;
    ttl?: number; // Default: 30000ms
    retryAttempts?: number; // Default: 3
    retryDelay?: number; // Default: 100ms
    performance?: 'standard' | 'lean' | 'enterprise';
    logger?: ILogger; // Optional structured logging
    circuitBreaker?: boolean | CircuitBreakerConfig; // Default: enabled
    }

    interface CreateRedlockConfig {
    adapters: RedisAdapter[];
    key: string;
    ttl?: number; // Default: 30000ms
    retryAttempts?: number; // Default: 3
    retryDelay?: number; // Default: 200ms
    quorum?: number; // Default: Math.floor(adapters.length / 2) + 1
    clockDriftFactor?: number; // Default: 0.01
    logger?: ILogger; // Optional structured logging
    }
    Advanced: Batch Lock Acquisition

    Acquire multiple locks atomically (all-or-nothing):

    import { LockManager } from 'redlock-universal';

    const manager = new LockManager({ nodes: [adapter] });

    // Atomic batch - prevents deadlocks via automatic key sorting
    const handles = await manager.acquireBatch(['user:1', 'user:2', 'order:3']);
    try {
    await processTransaction();
    } finally {
    await manager.releaseBatch(handles);
    }

    // Or with auto-extension (supports retryAttempts, retryDelay options)
    await manager.usingBatch(['key1', 'key2'], async (signal) => {
    // All locks auto-extend and release
    });
    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:

    Logger Works Directly Adapter Needed
    Winston Yes No
    Console Yes No
    Pino Via Adapter createPinoAdapter()
    Bunyan Via Adapter createBunyanAdapter()
    // Pino
    import { createPinoAdapter } from 'redlock-universal';
    const logger = createPinoAdapter(pinoLogger);

    // Bunyan
    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 });

    // Use in tests
    const handle = await lock.acquire();
    await lock.release(handle);

    // Cleanup
    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';

    // Multiple locks with shared config
    const locks = createLocks(adapter, ['user:123', 'account:456'], {
    ttl: 15000,
    retryAttempts: 5,
    });

    // Lock with automatic key prefixing
    const userLock = createPrefixedLock(adapter, 'locks:user:', '123', {
    ttl: 10000,
    });
    // Key: "locks:user:123"

    // Multiple distributed locks
    const redlocks = createRedlocks(
    [adapter1, adapter2, adapter3],
    ['resource1', 'resource2'],
    { ttl: 15000, quorum: 2 }
    );
    Advanced: Performance Modes

    Choose the optimal mode for your use case:

    // Standard (default) - full monitoring and observability
    const lock = createLock({ adapter, key: 'resource', performance: 'standard' });

    // Lean - memory-optimized, minimal overhead (~3% faster)
    const lock = createLock({ adapter, key: 'resource', performance: 'lean' });

    // Enterprise - standard with advanced observability
    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

    // Enabled by default — customize thresholds:
    const lock = createLock({
    adapter, key: 'resource',
    circuitBreaker: {
    failureThreshold: 5, // Consecutive failures to trip (default: 5)
    resetTimeout: 60000, // Ms before probing recovery (default: 60000)
    healthCheckInterval: 30000 // Ms between health checks (default: 30000)
    }
    });

    // Disable entirely:
    const lock = createLock({ adapter, key: 'resource', circuitBreaker: false });

    // Check breaker state:
    const health = lock.getHealth();
    // { healthy, lastCheck, connected, circuitBreaker: { state, failures, openedAt } }

    The circuit breaker is available in standard and enterprise performance modes. The lean mode omits it for minimal memory overhead.

    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) {
    // Lock is held by another process
    } else if (error instanceof LockExtensionError) {
    // Extension failed (lock expired or lost)
    } else if (error instanceof LockReleaseError) {
    // Release failed (handle mismatch or connection issue)
    }
    }

    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.

    • 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
    npm test                    # Unit tests
    npm run test:integration # Integration tests (requires Redis)
    npm run test:coverage # Coverage report
    npm run test:docker # Docker-based tests (all services)
    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.

    From node-redlock
    // Before (node-redlock)
    const redlock = new Redlock([ioredis], { retryCount: 3 });
    const lock = await redlock.acquire(['resource'], 30000);
    await lock.release();

    // After (redlock-universal)
    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
    // Before (redis-semaphore)
    const mutex = new Mutex(ioredis, 'resource');
    await mutex.acquire();
    await mutex.release();

    // After (redlock-universal)
    const lock = createLock({
    adapter: new IoredisAdapter(ioredis),
    key: 'resource',
    });
    const handle = await lock.acquire();
    await lock.release(handle);

    See CONTRIBUTING.md. Issues and PRs welcome.

    MIT