npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

redlock-universal

v0.7.6

Published

Production-ready distributed Redis locks for Node.js with support for both node-redis and ioredis clients

Readme

redlock-universal

Production-ready distributed Redis locks for Node.js with support for both node-redis and ioredis

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

Overview

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.

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

Features

  • 🔒 Distributed Locks: True Redlock algorithm for multi-instance Redis
  • 🔌 Client Universal: Works with both node-redis v4+ and ioredis 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 (0.48ms mean), fastest throughput among tested libraries (3300+ ops/sec)
  • 📊 Enhanced Monitoring: Built-in metrics, health checks, and structured logging
  • 🧪 Tested: 86%+ test coverage with 456 unit, integration, and E2E tests

Table of Contents

Installation

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

Quick Start

Automatic Lock Management - The Easy Way

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 (Fine Control)

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

Distributed Lock (Multiple Redis Instances)

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

Core Concepts

Configuration Constants

  • AUTO_EXTENSION_THRESHOLD_RATIO: 0.2 - Extension triggers at 80% TTL consumed
  • ATOMIC_EXTENSION_SAFETY_BUFFER: 2000ms - Minimum TTL for safe extension
  • MIN_EXTENSION_INTERVAL: 100ms - Prevents rapid retry loops

API Reference

Simple Lock

createLock(config)

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

Lock Methods

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

Performance Modes

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)

Logger Integration

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

External Logger Integration

Use your existing production logger (Winston, Pino, Bunyan, etc.) instead of the built-in Logger:

Winston (works directly):

import winston from 'winston';
import { createLock } from 'redlock-universal';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

const lock = createLock({
  adapter,
  key: 'resource',
  logger, // Winston works directly - no adapter needed!
});

Pino (requires adapter):

import pino from 'pino';
import { createLock, createPinoAdapter } from 'redlock-universal';

const pinoLogger = pino({ level: 'info' });
const logger = createPinoAdapter(pinoLogger); // Convert to ILogger

const lock = createLock({
  adapter,
  key: 'resource',
  logger, // Pino adapter provides full compatibility
});

Bunyan (requires adapter):

import bunyan from 'bunyan';
import { createLock, createBunyanAdapter } from 'redlock-universal';

const bunyanLogger = bunyan.createLogger({ name: 'myapp' });
const logger = createBunyanAdapter(bunyanLogger); // Convert to ILogger

const lock = createLock({
  adapter,
  key: 'resource',
  logger, // Bunyan adapter provides full compatibility
});

Compatible Loggers:

| Logger | Works Directly | Adapter Needed | | --------------- | -------------- | ---------------------------- | | Winston | ✅ Yes | No | | Console | ✅ Yes | No | | Built-in Logger | ✅ Yes | No | | Bunyan | ⚠️ Via Adapter | createBunyanAdapter() | | Pino | ⚠️ Via Adapter | createPinoAdapter() |

Note: Log4js uses util.format() for all arguments, which stringifies context objects instead of preserving structured data. For structured logging, use Winston, Bunyan (with adapter), or Pino (with adapter).

See examples/ directory for complete integration examples.

Lock Inspection

Inspect lock state atomically to debug stuck locks or monitor lock ownership:

import { NodeRedisAdapter } from 'redlock-universal';

const adapter = NodeRedisAdapter.from(client);

// Inspect lock state
const inspection = await adapter.inspect('my-resource');

if (inspection) {
  console.log('Lock owner:', inspection.value);
  console.log('TTL remaining:', inspection.ttl, 'ms');

  // Use for debugging stuck locks
  if (inspection.ttl < 1000) {
    console.warn('Lock expiring soon!');
  }
} else {
  console.log('Lock is not currently held');
}

Use Cases:

  • Debugging: Identify which process holds a lock
  • Monitoring: Track lock ownership and expiration
  • Diagnostics: Understand lock contention issues

Important Notes:

  • Returns null if lock doesn't exist
  • TTL is in milliseconds
  • Operation is atomic (uses Lua script)
  • Available on all adapters (NodeRedisAdapter, IoredisAdapter, MemoryAdapter)

MemoryAdapter (Testing)

For unit tests without Redis:

import { MemoryAdapter, createLock } from 'redlock-universal';

// Create in-memory adapter
const adapter = new MemoryAdapter();

// Use in tests
const lock = createLock({
  adapter,
  key: 'test-resource',
  ttl: 5000,
});

// Test lock behavior
const handle = await lock.acquire();
expect(handle.key).toBe('test-resource');
await lock.release(handle);

// Cleanup
adapter.clear(); // Remove all locks
await adapter.disconnect(); // Clean up timers

⚠️ TESTING ONLY: MemoryAdapter is NOT suitable for production use. It lacks:

  • Persistence (all locks lost on process restart)
  • Cross-process synchronization (single process only)
  • Network reliability (no Redis connection handling)

Features:

  • Full RedisAdapter interface compatibility
  • Proper TTL expiration (using setTimeout)
  • Atomic batch operations
  • No Docker or Redis dependencies

Test Setup Example:

describe('My Feature', () => {
  let adapter: MemoryAdapter;

  beforeEach(() => {
    adapter = new MemoryAdapter();
  });

  afterEach(async () => {
    adapter.clear(); // Clean locks between tests
    await adapter.disconnect(); // Clean up timers
  });

  it('should handle lock contention', async () => {
    const lock1 = createLock({ adapter, key: 'shared', ttl: 5000 });
    const lock2 = createLock({ adapter, key: 'shared', ttl: 5000 });

    await lock1.acquire();
    await expect(lock2.acquire()).rejects.toThrow(); // Contention!
  });
});

Distributed Lock (RedLock)

createRedlock(config)

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

Redis Adapters

Node-Redis Adapter

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

Ioredis Adapter

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

Factory Functions

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

Advanced Usage

Lock with Retry Logic

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

Lock Extension

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

Auto-Extension with using() API

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.

Simple Lock Auto-Extension

// 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 Auto-Extension

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

Real-World Examples

For implementation patterns including database transactions, cache warming, and job processing, see the examples directory.

Error Handling

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

Multiple Resource Locking

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

Batch Lock Acquisition

Acquire multiple locks atomically with all-or-nothing semantics using LockManager:

import { LockManager } from 'redlock-universal';

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

// Atomic batch acquisition - either all locks acquired or none
const handles = await manager.acquireBatch([
  'user:123',
  'account:456',
  'order:789',
]);

try {
  // All locks acquired atomically - perform multi-resource transaction
  await processMultiResourceTransaction();
} finally {
  // Release all locks
  await manager.releaseBatch(handles);
}

Batch with Auto-Extension

Combine batch acquisition with automatic lock renewal for long-running operations:

// Batch locks with auto-extension
await manager.usingBatch(
  ['user:123', 'account:456', 'order:789'],
  async signal => {
    // All locks acquired atomically and will auto-extend
    for (const task of longRunningTasks) {
      await processTask(task);

      // Check if any lock extension failed
      if (signal.aborted) {
        throw new Error('Lock extension failed - aborting operation');
      }
    }

    return 'all-tasks-completed';
  }
);
// All locks automatically released

Atomicity Guarantee

Batch acquisition uses Redis Lua scripts to ensure atomicity:

  • All-or-Nothing: Either all locks are acquired or the operation fails
  • No Partial States: Prevents race conditions from acquiring locks individually
  • Deadlock Prevention: Keys are automatically sorted to ensure consistent lock order
  • Performance: Single Redis round-trip instead of N sequential acquisitions
try {
  const handles = await manager.acquireBatch([
    'resource:1',
    'resource:2',
    'resource:3',
  ]);
  // SUCCESS: All 3 locks acquired
} catch (error) {
  if (error instanceof LockAcquisitionError) {
    // FAILURE: None of the locks were acquired
    console.error('Batch failed:', error.key, 'already locked');
  }
}

For complete examples, see examples/batch-locks.ts.

Batch Operations Performance

Batch lock acquisition delivers significant performance improvements over sequential locking:

Sequential vs Batch Comparison:

| Locks | Sequential | Batch | Speedup | | ----- | ---------- | ------ | --------- | | 3 | 2.34ms | 0.62ms | 3.8x | | 5 | 3.46ms | 0.60ms | 5.8x | | 10 | 4.98ms | 0.34ms | 14.7x |

†Benchmarked on local Redis 7 (macOS, Node.js 22). Performance varies between runs due to system load, network latency, and Redis configuration. The relative speedup advantage (3-15x) remains consistent across different systems.

Key Performance Metrics:

  • Throughput: 2,630 ops/sec for batch operations
  • Auto-Extension Overhead: 0.0% (negligible impact)
  • Scalability: Speedup increases with lock count

Why Batch is Faster:

  • Single Lua script execution (atomic operation)
  • Eliminates N network round-trips
  • Sub-millisecond performance even for 10+ locks
  • Automatic key sorting prevents deadlocks
// Benchmark example: 10 locks
// Sequential: ~5ms (10 Redis calls)
// Batch:      ~0.34ms (1 Lua script)
// Result:     14.7x faster ⚡

Best Practices

1. Always Use Try-Finally for Lock Release

const handle = await lock.acquire();
try {
  await doWork();
} finally {
  await lock.release(handle);
}

2. Choose Appropriate TTL

// Short-lived operations
const lock = createLock({ adapter, key: 'quick-task', ttl: 5000 });

// Long-running operations
const lock = createLock({ adapter, key: 'batch-job', ttl: 300000 });

3. Handle Lock Contention

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

4. Distributed Lock Quorum

// For 5 Redis instances, use quorum of 3
const redlock = createRedlock({
  adapters: [redis1, redis2, redis3, redis4, redis5],
  quorum: 3, // Majority consensus
  key: 'critical-resource',
});

Monitoring and Observability

Lock Metadata

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

Structured Logging (NEW!)

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

Architecture Improvements

  • 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

Performance

redlock-universal delivers industry-leading performance:

  • Lock acquisition: 0.48ms mean latency (P95: 0.75ms) in lean mode
  • Memory usage: <2KB per operation (60% reduction via buffer pooling)
  • Throughput: 3,329 ops/sec (42% faster than redis-semaphore, 95% faster than node-redlock)
  • Batch operations: 3.8x - 14.7x faster than sequential (scales with lock count)
  • Test coverage: 86%+ with 487 unit, integration, and E2E 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

Recent Optimizations (v0.6.5):

  • Buffer pooling reduces GC pressure by 60%
  • Fast-path optimizations for circuit breaker checks
  • Zero-allocation logging in production mode
  • 51% faster lean mode vs standard mode

Benchmarking

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

Comparison with Alternatives

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 Comparison

| Feature | redlock-universal | node-redlock | redis-semaphore | | ----------------------------------- | ----------------- | ------------ | ----------------- | | Client Support | | node-redis v4+ | ✅ Native | ❌ | ⚠️ Wrapper needed | | ioredis v5+ | ✅ Native | ✅ Required | ✅ Native | | Language & Developer Experience | | TypeScript | ✅ First-class | ✅ Native | ✅ Native | | Modern ESM | ✅ | ⚠️ CJS focus | ✅ | | API Design | ✅ Intuitive | ⚠️ Complex | ✅ 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 | ❌ | ❌ |

Technical Comparison (Verified Data)

| 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 | ⚠️ Stale (3y) | ✅ 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.48ms (P95: 0.75ms) | ~0.4-0.8ms | ~0.4-0.6ms | | Throughput (ops/sec)† | 3,329 | 1,702 | 2,340 | | Memory per Operation† | <2KB | ~8KB | ~6KB |

*Benchmarked on local Redis 7 (macOS, Node.js 22). Performance varies between runs due to system load, network latency, and Redis configuration. All tested libraries deliver competitive sub-millisecond performance. Focus on features and reliability over micro-optimizations.

Maintenance Analysis

| Package | Status | Assessment | | ------------------- | ----------------------- | ------------------------------------------------ | | node-redlock | Last updated March 2022 | Consider compatibility with newer Redis versions | | redis-semaphore | Actively maintained | Good feature set, reliable choice |

Why Choose redlock-universal?

Universal Compatibility

  • 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

Production-Ready Observability

  • 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

Modern Architecture & DX

  • 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

Proven Algorithm Implementation

  • Redis-spec compliant: Follows official Redlock specification
  • Clock drift handling: Proper time synchronization assumptions
  • Fault tolerance: Graceful degradation on partial failures
  • Performance optimized: Memory-efficient buffer pooling, sub-millisecond acquisition, and highest throughput among tested libraries (verified benchmarks included)

Migration Guide

From node-redlock

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

From redis-semaphore

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

Testing

# 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

FAQ

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

Troubleshooting

Common Issues

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

Examples

Quick examples are shown above. For detailed implementations:

Real-World Patterns:

Core Usage:

Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT © Alex Potapenko

Support


Made with ❤️ for the Node.js community