Production-ready distributed Redis locks for Node.js with support for both node-redis and ioredis
redlock-universal implements distributed Redis locks using the Redlock algorithm. It supports both node-redis and ioredis clients through a unified TypeScript API with automatic lock extension capabilities.
- 🔒 Distributed Locks: True Redlock algorithm for multi-instance Redis
- 🔌 Client Universal: Works with both
node-redis
v4+ andioredis
v5+ - 🤖 Auto-Extension:
using()
API with automatic lock extension for long-running operations - 📋 Structured Logging: Comprehensive Logger integration for production observability
- 🏢 Production Ready: Circuit breakers, health checks, error handling, and retries
- 🚀 TypeScript First: Full type safety and modern ESM support
- ⚡ Performance: Sub-millisecond lock acquisition, competitive with leading libraries
- 📊 Enhanced Monitoring: Built-in metrics, health checks, and structured logging
- 🧪 Tested: 85%+ test coverage with unit and integration tests
npm install redlock-universal
Peer Dependencies: Install your preferred Redis client
# For node-redis users
npm install redis
# For ioredis users
npm install ioredis
# Or both if you need mixed environments
npm install redis ioredis
import { createLock, NodeRedisAdapter } from 'redlock-universal';
import { createClient } from 'redis';
// Setup Redis client
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();
// Create lock
const lock = createLock({
adapter: new NodeRedisAdapter(client),
key: 'my-resource',
ttl: 30000, // 30 seconds
});
// Automatic lock management - the easy way
await lock.using(async signal => {
await processData();
// Lock auto-extends if needed, releases automatically
// Check signal.aborted if you need to know about extension failures
});
// Traditional approach (if you need fine control)
try {
const handle = await lock.acquire();
// Critical section - only one process can be here
await doSomeCriticalWork();
await lock.release(handle);
} catch (error) {
console.error('Lock operation failed:', error);
}
import {
createRedlock,
NodeRedisAdapter,
IoredisAdapter,
} from 'redlock-universal';
import { createClient } from 'redis';
import Redis from 'ioredis';
// Setup multiple Redis connections
const clients = [
createClient({ url: 'redis://redis1:6379' }),
createClient({ url: 'redis://redis2:6379' }),
createClient({ url: 'redis://redis3:6379' }),
];
// Connect all node-redis clients
await Promise.all(clients.map(client => client.connect()));
// Create adapters (ioredis connects automatically)
const adapters = [
new NodeRedisAdapter(clients[0]),
new NodeRedisAdapter(clients[1]),
new IoredisAdapter(new Redis('redis://redis3:6379')),
];
// Create distributed lock
const redlock = createRedlock({
adapters,
key: 'distributed-resource',
ttl: 30000,
quorum: 2, // Majority consensus
});
// Use distributed lock
try {
const handle = await redlock.acquire();
// Critical section with distributed guarantee
await processPayment();
await redlock.release(handle);
} catch (error) {
console.error('Distributed lock failed:', error);
} finally {
// Disconnect all clients
await Promise.all(clients.map(client => client.disconnect()));
}
AUTO_EXTENSION_THRESHOLD_RATIO
: 0.2 - Extension triggers at 80% TTL consumedATOMIC_EXTENSION_SAFETY_BUFFER
: 2000ms - Minimum TTL for safe extensionMIN_EXTENSION_INTERVAL
: 100ms - Prevents rapid retry loops
Creates a simple lock for single Redis instance.
interface CreateLockConfig {
adapter: RedisAdapter;
key: string;
ttl?: number; // Default: 30000ms
retryAttempts?: number; // Default: 3
retryDelay?: number; // Default: 100ms
performance?: 'standard' | 'lean' | 'enterprise'; // Default: 'standard'
logger?: Logger; // See [Logger Configuration](#logger-integration)
}
// Acquire lock
const handle = await lock.acquire();
// Release lock
const released = await lock.release(handle);
// Extend lock TTL
const extended = await lock.extend(handle, newTTL);
// Check if locked
const isLocked = await lock.isLocked(key);
// Auto-extending lock with routine execution (NEW!)
const result = await lock.using(async signal => {
// Your long-running operation here
// Lock automatically extends at 80% of TTL
// Check signal.aborted if extension fails
return 'operation-result';
});
// Advanced usage with abort signal checking
const result = await lock.using(async signal => {
for (let i = 0; i < 1000; i++) {
await processItem(i);
// Check for cancellation (e.g., if lock extension fails)
if (signal.aborted) {
console.log('Operation cancelled:', signal.error?.message);
break;
}
}
return { processed: i };
});
Choose the optimal performance mode for your use case:
// Standard mode (default) - Full features with monitoring
const lock = createLock({
adapter: new NodeRedisAdapter(client),
key: 'resource',
performance: 'standard', // Full monitoring, health checks
});
// Lean mode - Memory optimized for high-throughput scenarios
const leanLock = createLock({
adapter: new NodeRedisAdapter(client),
key: 'resource',
performance: 'lean', // Saves ~150KB memory, 3% faster
});
Performance Mode Comparison:
- Standard: Full monitoring, health checks, comprehensive error details
- Lean: Memory-optimized, pre-allocated errors, minimal overhead
- Enterprise: Standard + circuit breakers + advanced observability (future)
Configure structured logging for production observability:
import { Logger, LogLevel } from 'redlock-universal';
// Create logger instance
const logger = new Logger({
level: LogLevel.INFO,
prefix: 'redlock',
enableConsole: true, // Console output
enableCollection: true, // In-memory collection for metrics
maxEntries: 1000, // Limit memory usage
});
// Single-instance lock with logger
const lock = createLock({
adapter: new NodeRedisAdapter(client),
key: 'resource',
ttl: 30000,
logger, // Enhanced monitoring and error reporting
});
// Distributed lock with logger
const redlock = createRedlock({
adapters: [adapter1, adapter2, adapter3],
key: 'distributed-resource',
ttl: 30000,
logger, // Distributed lock state tracking
});
Logger Configuration:
interface LoggerConfig {
level: LogLevel; // DEBUG, INFO, WARN, ERROR
prefix?: string; // Log prefix for identification
enableConsole?: boolean; // Console output (default: true)
enableCollection?: boolean; // In-memory collection (default: false)
maxEntries?: number; // Max entries to keep (default: 100)
}
What Gets Logged:
- ✅ Lock acquisition attempts and failures
- ✅ Circuit breaker state changes (open/closed/half-open)
- ✅ Redis connection health checks and recovery
- ✅ Auto-extension successes and failures
- ✅ Redis adapter warnings (disconnect issues)
- ✅ Lock release errors and cleanup issues
Accessing Collected Logs:
// Get recent log entries for analysis
const entries = logger.getEntries();
console.log(`Collected ${entries.length} log entries`);
// Check for errors in the last hour
const recentErrors = entries.filter(
entry =>
entry.level === LogLevel.ERROR && entry.timestamp > Date.now() - 3600000
);
Creates a distributed lock using the Redlock algorithm.
interface CreateRedlockConfig {
adapters: RedisAdapter[];
key: string;
ttl?: number; // Default: 30000ms
quorum?: number; // Default: majority
retryAttempts?: number; // Default: 3
retryDelay?: number; // Default: 200ms
clockDriftFactor?: number; // Default: 0.01
logger?: Logger; // See [Logger Configuration](#logger-integration)
}
import { NodeRedisAdapter } from 'redlock-universal';
import { createClient } from 'redis';
const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();
// Basic adapter
const adapter = new NodeRedisAdapter(client);
// With logger support (NEW!)
const adapter = new NodeRedisAdapter(client, {
keyPrefix: 'myapp:', // Optional key prefix
timeout: 5000, // Redis operation timeout
logger: logger, // Structured logging for adapter operations
});
import { IoredisAdapter } from 'redlock-universal';
import Redis from 'ioredis';
const client = new Redis('redis://localhost:6379');
// Basic adapter
const adapter = new IoredisAdapter(client);
// With logger support (NEW!)
const adapter = new IoredisAdapter(client, {
keyPrefix: 'myapp:', // Optional key prefix
timeout: 5000, // Redis operation timeout
maxRetries: 3, // Redis operation retries
retryDelay: 100, // Delay between retries
logger: logger, // Structured logging for adapter operations
});
Redis Adapter Options:
interface RedisAdapterOptions {
keyPrefix?: string; // Prefix for all Redis keys
maxRetries?: number; // Max retries for failed operations (default: 3)
retryDelay?: number; // Delay between retries in ms (default: 100)
timeout?: number; // Operation timeout in ms (default: 5000)
logger?: Logger; // See [Logger Configuration](#logger-integration)
}
What Adapters Log:
⚠️ Redis disconnect warnings (connection cleanup issues)- 🔄 Operation retries and timeouts
- 🚫 Validation errors (invalid keys, TTL values)
- 🔗 Connection health and status changes
Convenient functions for creating multiple locks or specialized configurations:
import {
createLocks,
createPrefixedLock,
createRedlocks,
} from 'redlock-universal';
// Create multiple locks with shared configuration
const locks = createLocks(adapter, ['user:123', 'account:456'], {
ttl: 15000,
retryAttempts: 5,
performance: 'lean',
});
// Create lock with automatic key prefixing
const userLock = createPrefixedLock(adapter, 'locks:user:', '123', {
ttl: 10000,
});
// Results in key: "locks:user:123"
// Create multiple distributed locks
const redlocks = createRedlocks(
[adapter1, adapter2, adapter3],
['resource1', 'resource2'],
{
ttl: 15000,
quorum: 2,
retryAttempts: 5,
}
);
const lock = createLock({
adapter: new NodeRedisAdapter(client),
key: 'contested-resource',
ttl: 10000,
retryAttempts: 5, // Retry up to 5 times
retryDelay: 200, // Wait 200ms between retries
});
const handle = await lock.acquire();
// Extend lock by 10 more seconds
const extended = await lock.extend(handle, 10000);
if (extended) {
// Continue working with extended lock
await longRunningTask();
}
await lock.release(handle);
The using()
method provides automatic lock management with auto-extension for
long-running operations. It handles lock acquisition, automatic extension when
needed, and guaranteed cleanup.
// Auto-extending lock with routine execution
const result = await lock.using(async signal => {
// Long-running operation - lock automatically extends at 80% of TTL
await processLargeDataset();
// Check if extension failed (loss of lock)
if (signal.aborted) {
throw new Error(`Lock lost: ${signal.error?.message}`);
}
return 'processing-complete';
});
console.log(result); // 'processing-complete'
// Distributed lock with quorum-based auto-extension
const redlock = createRedlock({
adapters: [adapter1, adapter2, adapter3],
key: 'distributed-job',
ttl: 30000,
quorum: 2,
});
const result = await redlock.using(async signal => {
for (const item of largeJobQueue) {
// Process each item - lock extends automatically
await processItem(item);
// Abort if quorum lost (majority of Redis nodes failed)
if (signal.aborted) {
throw new Error(`Distributed lock lost: ${signal.error?.message}`);
}
}
return 'all-items-processed';
});
For implementation patterns including database transactions, cache warming, and job processing, see the examples directory.
import { LockAcquisitionError, LockReleaseError } from 'redlock-universal';
try {
const handle = await lock.acquire();
// ... work ...
await lock.release(handle);
} catch (error) {
if (error instanceof LockAcquisitionError) {
console.error('Failed to acquire lock:', error.message);
} else if (error instanceof LockReleaseError) {
console.error('Failed to release lock:', error.message);
}
}
// Lock multiple resources in consistent order (avoid deadlocks)
const userLock = createLock({ adapter, key: 'user:123' });
const accountLock = createLock({ adapter, key: 'account:456' });
const userHandle = await userLock.acquire();
const accountHandle = await accountLock.acquire();
try {
// Perform transaction requiring both resources
await transferFunds();
} finally {
// Release in reverse order
await accountLock.release(accountHandle);
await userLock.release(userHandle);
}
const handle = await lock.acquire();
try {
await doWork();
} finally {
await lock.release(handle);
}
// Short-lived operations
const lock = createLock({ adapter, key: 'quick-task', ttl: 5000 });
// Long-running operations
const lock = createLock({ adapter, key: 'batch-job', ttl: 300000 });
const lock = createLock({
adapter,
key: 'popular-resource',
retryAttempts: 3,
retryDelay: 100,
});
try {
const handle = await lock.acquire();
// ... work ...
} catch (error) {
if (error instanceof LockAcquisitionError) {
// Resource is busy, handle gracefully
await scheduleForLater();
}
}
// For 5 Redis instances, use quorum of 3
const redlock = createRedlock({
adapters: [redis1, redis2, redis3, redis4, redis5],
quorum: 3, // Majority consensus
key: 'critical-resource',
});
// Access lock metadata
const handle = await lock.acquire();
console.log('Lock acquired in:', handle.metadata.acquisitionTime, 'ms');
console.log('Attempts required:', handle.metadata.attempts);
// For distributed locks
const redlockHandle = await redlock.acquire();
console.log('Nodes locked:', redlockHandle.metadata.nodes.length);
console.log('Quorum achieved:', redlockHandle.metadata.nodes.length >= quorum);
import { Logger, LogLevel } from 'redlock-universal';
// Production logging setup
const logger = new Logger({
level: LogLevel.INFO,
prefix: 'redlock',
enableConsole: true, // For development
enableCollection: true, // For metrics collection
maxEntries: 1000, // Memory limit
});
// Configure locks with logger
const lock = createLock({ adapter, key: 'resource', logger });
// Monitor lock operations
const entries = logger.getEntries();
const errors = entries.filter(e => e.level === LogLevel.ERROR);
const warnings = entries.filter(e => e.level === LogLevel.WARN);
console.log(`Lock errors: ${errors.length}, Warnings: ${warnings.length}`);
- ✅ Race condition protection: Atomic extension scripts eliminate timing race conditions in auto-extension
- ✅ Consistent logging: All components use structured Logger instead of mixed console.* calls
- ✅ Zero NODE_ENV checks: Production code no longer depends on environment variables for behavior
- ✅ Configurable observability: Enable/disable console output and metrics collection independently
- ✅ Enhanced context: All log entries include relevant context (keys, correlation IDs, timestamps)
- ✅ Memory management: Built-in log rotation with configurable limits
- ✅ TTL feedback: Atomic operations provide real-time TTL information for intelligent scheduling
redlock-universal delivers competitive performance:
- Lock acquisition: Sub-millisecond latency (typically 0.4-0.8ms with local Redis)
- Memory usage: <7KB per operation (both standard and lean modes)
- Throughput: >1000 ops/sec (competitive with leading Redis lock libraries)
- Test coverage: 85%+ with unit and integration tests
Performance modes:
- Standard (default): Full monitoring and observability features
- Lean: Memory-optimized with minimal overhead for maximum speed
- Enterprise: Additional health checks and circuit breakers
We provide benchmarks to validate performance claims:
# Compare with leading Redis lock libraries
npm run benchmark:competitive
# Internal performance validation
npm run benchmark:performance
# Run all benchmarks
npm run benchmark
Benchmark Philosophy: We believe in honest, reproducible performance testing. Our benchmarks:
- Test against real Redis instances (not mocks)
- Include statistical analysis (mean, p50, p95, p99)
- Acknowledge performance variability between runs
- Focus on competitive positioning rather than absolute claims
Methodology: This comparison uses data from npm registry (July 2025) and architectural analysis. Performance estimates are based on implementation patterns and Redis operation complexity.
Feature | redlock-universal | node-redlock | redis-semaphore |
---|---|---|---|
Client Support | |||
node-redis v4+ | ✅ Native | ❌ | |
ioredis v5+ | ✅ Native | ✅ Required | ✅ Native |
Language & Developer Experience | |||
TypeScript | ✅ First-class | ✅ Native | ✅ Native |
Modern ESM | ✅ | ✅ | |
API Design | ✅ Intuitive | ✅ Clean | |
Error Types | ✅ Specific | ✅ Basic | ✅ Detailed |
Locking Capabilities | |||
Single Instance | ✅ Optimized | ❌ | ✅ |
Distributed (Redlock) | ✅ Full spec | ✅ Full spec | ✅ RedlockMutex |
Lock Extension | ✅ Manual/Auto | ✅ Watchdog | ✅ Auto-refresh |
Semaphores | ❌ Planned | ❌ | ✅ Advanced |
Production Features | |||
Retry Logic | ✅ Configurable | ✅ Built-in | ✅ Fair queue |
Monitoring | ✅ Built-in | ❌ | ❌ |
Health Checks | ✅ Built-in | ❌ | ❌ |
Structured Logging | ✅ Built-in | ❌ | ❌ |
Metric | redlock-universal | node-redlock | redis-semaphore |
---|---|---|---|
Maintenance & Adoption | |||
Weekly Downloads | New Package | 644,599 | 282,020 |
Last Updated | 2025 Active | Mar 2022 |
Mar 2025 ✅ |
Maintenance Status | ✅ Active | ✅ Active | |
Package Quality | |||
Runtime Dependencies | 0 (peer only) | 1 | 1 |
TypeScript Support | ✅ Native | ✅ Native | ✅ Native |
Test Coverage | 85%+ Unit + Integration | Unknown | Unknown |
Performance Characteristics | |||
Lock Acquisition† | ~0.4-0.8ms | ~0.4-0.8ms | ~0.4-0.6ms |
Distributed Latency* | ~3-8ms | ~5-15ms | ~4-10ms |
Memory per Operation† | <7KB | ~8KB | ~6KB |
*Estimated/measured with local Redis 7. Performance is competitive among actively maintained libraries. †Actual performance varies by network latency and Redis configuration.
Package | Status | Assessment |
---|---|---|
node-redlock | Last updated March 2022 | Consider compatibility with newer Redis versions |
redis-semaphore | Actively maintained | Good feature set, reliable choice |
- Only library supporting both node-redis v4+ and ioredis v5+ natively
- Future-proof: Works with latest Redis client versions
- Migration-friendly: Easy to switch between Redis clients
- Built-in metrics: Track lock performance, acquisition times, success rates
- Health monitoring: Redis connection health checks and statistics
- Structured logging: Configurable logging with context and levels
- Zero competitors offer these enterprise features
- TypeScript-first: Strict typing, excellent IntelliSense
- ESM native: Modern module system with CommonJS compatibility
- Zero runtime dependencies: Security and supply chain safety
- Code quality: 85%+ test coverage with unit and integration tests
- Redis-spec compliant: Follows official Redlock specification
- Clock drift handling: Proper time synchronization assumptions
- Fault tolerance: Graceful degradation on partial failures
- Performance optimized: Sub-millisecond acquisition, competitive performance
// Before (node-redlock) - Stale for 3 years
const redlock = new Redlock([redis1, redis2], { retryCount: 3 });
const resource = await redlock.acquire(['resource'], 30000);
await redlock.release(resource);
// After (redlock-universal) - Modern & maintained
const redlock = createRedlock({
adapters: [new IoredisAdapter(redis1), new IoredisAdapter(redis2)],
key: 'resource',
ttl: 30000,
retryAttempts: 3,
});
const handle = await redlock.acquire();
await redlock.release(handle);
// Before (redis-semaphore) - Good but limited to ioredis
const mutex = new Mutex(redis, 'resource', { acquireTimeout: 30000 });
const release = await mutex.acquire();
release();
// After (redlock-universal) - Universal client support + monitoring
const lock = createLock({
adapter: new NodeRedisAdapter(nodeRedisClient), // or IoredisAdapter
key: 'resource',
ttl: 30000,
});
const handle = await lock.acquire();
await lock.release(handle);
# Run unit tests
npm test
# Run integration tests (requires Redis)
npm run test:integration
# Run all tests with coverage
npm run test:coverage
# Run Docker-based tests
npm run test:docker
Q: What's the performance overhead of auto-extension? A: Minimal - typically <1ms using atomic operations.
Q: How does this handle Redis restarts? A: Lua scripts auto-reload on NOSCRIPT errors, no action needed.
Q: SimpleLock vs RedLock? A: SimpleLock = single Redis (faster). RedLock = multiple Redis (fault-tolerant).
Lock not releasing:
- Ensure the lock handle matches the stored value
- Check if TTL expired before release attempt
- Verify Redis connectivity
Auto-extension not working:
- Verify ATOMIC_EXTENSION_SAFETY_BUFFER is defined (2000ms default)
- Check that TTL is long enough for your operation
- Monitor the AbortSignal for extension failures
Circuit breaker opening frequently:
- Increase timeout values
- Check Redis server performance
- Review network latency
"NOSCRIPT" errors:
- Redis flushed Lua script cache
- Library automatically reloads scripts
- No action needed, but indicates Redis restart
Connection timeouts:
- Check Redis maxclients setting
- Review connection pool configuration
- Monitor network latency between app and Redis
Quick examples are shown above. For detailed implementations:
Real-World Patterns:
- Database Transactions - Transaction safety patterns
- Distributed Cache Warming - Distributed cache coordination
- Job Processing with Progress - Long-running job management
Core Usage:
- Simple Lock Usage - Basic locking patterns
- Distributed Lock (RedLock) - Multi-instance coordination
- Lock Extension Patterns - Manual extension strategies
- Retry Strategies - Contention handling
- Monitoring & Observability - Production monitoring
- Adapter Usage - Redis client integration
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
MIT © Alex Potapenko
Made with ❤️ for the Node.js community