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 🙏

© 2026 – Pkg Stats / Ryan Hefner

redlock-universal

v0.8.4

Published

Production-ready distributed locks for Redis and Valkey with support for node-redis, ioredis, and Valkey GLIDE

Readme

redlock-universal

Universal distributed locks for Redis and Valkey

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

The only distributed lock library supporting all three Redis clients:

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

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

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
}

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

Debug stuck locks:

const inspection = await adapter.inspect('my-resource');
if (inspection) {
  console.log('Owner:', inspection.value);
  console.log('TTL:', inspection.ttl, 'ms');
}

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.

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

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

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

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