Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open more actions menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

97 Commits
97 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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-redisioredisValkey GLIDE

NestJS 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,
});

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

Why 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.

Installation

npm install redlock-universal

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

Core API

using() - Recommended

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';
});

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();
// 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().

isLocked() - Check Lock Status

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

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

Adapters & Cluster Support

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.

Configuration

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.

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) {
    // 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)
  }
}

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                    # 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)

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

Links

Contributing

See CONTRIBUTING.md. Issues and PRs welcome.

License

MIT

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

Morty Proxy This is a proxified and sanitized view of the page, visit original site.