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

nestjs-redlock-universal

v0.3.0

Published

NestJS integration for redlock-universal - Distributed Redis and Valkey locks with decorators, modules, and dependency injection

Downloads

20,167

Readme

nestjs-redlock-universal

NestJS integration for redlock-universal - Distributed Redis and Valkey locks with decorators and dependency injection

npm version npm downloads TypeScript License: MIT

Overview

NestJS wrapper for redlock-universal, providing distributed Redis and Valkey locks through NestJS decorators, modules, and dependency injection.

Features

  • NestJS Native: First-class integration with dependency injection and lifecycle hooks
  • Distributed Locks: Redlock algorithm for multi-instance Redis/Valkey setups
  • Simple API: Method decorator for zero-boilerplate distributed locking
  • High Performance: <1ms lock acquisition with automatic extension
  • Type-Safe: Full TypeScript support with strict type checking
  • Universal: Works with node-redis v4+, ioredis v5+, and Valkey GLIDE v2+

Table of Contents

Installation

This package wraps redlock-universal, so you need to install both packages:

npm install nestjs-redlock-universal redlock-universal

You'll also need a Redis/Valkey client:

# For node-redis
npm install redis

# OR for ioredis
npm install ioredis

# OR for Valkey GLIDE (official Valkey client)
npm install @valkey/valkey-glide

Quick Start

1. Configure the Module

import { Module } from '@nestjs/common';
import { RedlockModule, NodeRedisAdapter } from 'nestjs-redlock-universal';
import { createClient } from 'redis';

// Create and connect Redis clients
const redis1 = createClient({ url: 'redis://localhost:6379' });
const redis2 = createClient({ url: 'redis://localhost:6380' });
const redis3 = createClient({ url: 'redis://localhost:6381' });

await Promise.all([redis1.connect(), redis2.connect(), redis3.connect()]);

@Module({
  imports: [
    RedlockModule.forRoot({
      nodes: [
        new NodeRedisAdapter(redis1),
        new NodeRedisAdapter(redis2),
        new NodeRedisAdapter(redis3),
      ],
      defaultTtl: 30000, // 30 seconds
    }),
  ],
})
export class AppModule {}

2. Use the @Redlock Decorator

import { Injectable } from '@nestjs/common';
import { Redlock } from 'nestjs-redlock-universal';

@Injectable()
export class PaymentService {
  @Redlock({ key: 'payment:processing' })
  async processPayment(orderId: string): Promise<void> {
    // This method is automatically protected by a distributed lock
    // Only one instance can execute at a time across all servers
  }

  @Redlock({ key: (userId: string) => `user:${userId}:update` })
  async updateUser(userId: string, data: unknown): Promise<void> {
    // Lock key is dynamically generated from method arguments
    // Each user gets their own lock
  }
}

3. Or Use the Service Directly

import { Injectable } from '@nestjs/common';
import { RedlockService } from 'nestjs-redlock-universal';

@Injectable()
export class OrderService {
  constructor(private readonly redlock: RedlockService) {}

  async processOrder(orderId: string): Promise<void> {
    // Recommended: Automatic lock management with using()
    await this.redlock.using(`order:${orderId}`, async () => {
      // Lock is automatically extended if operation takes longer than TTL
      // Lock is automatically released when done or on error
    });
  }

  async manualLocking(resourceId: string): Promise<void> {
    // Advanced: Manual acquire/release for fine-grained control
    const handle = await this.redlock.acquire(`resource:${resourceId}`);
    try {
      // Critical section
    } finally {
      await this.redlock.release(`resource:${resourceId}`, handle);
    }
  }
}

Configuration

Synchronous Configuration

import { RedlockModule, NodeRedisAdapter } from 'nestjs-redlock-universal';

RedlockModule.forRoot({
  nodes: [
    new NodeRedisAdapter(redis1),
    new NodeRedisAdapter(redis2),
    new NodeRedisAdapter(redis3),
  ],
  defaultTtl: 30000,        // Default lock TTL in milliseconds
  retryAttempts: 3,         // Number of retry attempts
  retryDelay: 200,          // Delay between retries in milliseconds
  quorum: 2,                // Minimum nodes for quorum (default: majority)
  logger: winstonLogger,    // Optional: Winston, Pino, or Bunyan logger
})

Asynchronous Configuration

import { ConfigService } from '@nestjs/config';
import { RedlockModule, NodeRedisAdapter } from 'nestjs-redlock-universal';

RedlockModule.forRootAsync({
  useFactory: async (configService: ConfigService) => {
    const redisUrls = configService.get<string[]>('redis.urls');
    const clients = await Promise.all(
      redisUrls.map(url => createClient({ url }).connect())
    );

    return {
      nodes: clients.map(client => new NodeRedisAdapter(client)),
      defaultTtl: configService.get('redis.lockTtl', 30000),
    };
  },
  inject: [ConfigService],
})

Using ioredis

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

const redis1 = new Redis({ host: 'localhost', port: 6379 });
const redis2 = new Redis({ host: 'localhost', port: 6380 });
const redis3 = new Redis({ host: 'localhost', port: 6381 });

RedlockModule.forRoot({
  nodes: [
    new IoredisAdapter(redis1),
    new IoredisAdapter(redis2),
    new IoredisAdapter(redis3),
  ],
})

Using Valkey GLIDE

import { RedlockModule, GlideAdapter } from 'nestjs-redlock-universal';
import { GlideClient } from '@valkey/valkey-glide';

// Create Valkey GLIDE clients
const valkey1 = await GlideClient.createClient({
  addresses: [{ host: 'localhost', port: 6379 }],
});
const valkey2 = await GlideClient.createClient({
  addresses: [{ host: 'localhost', port: 6380 }],
});
const valkey3 = await GlideClient.createClient({
  addresses: [{ host: 'localhost', port: 6381 }],
});

RedlockModule.forRoot({
  nodes: [
    new GlideAdapter(valkey1),
    new GlideAdapter(valkey2),
    new GlideAdapter(valkey3),
  ],
})

Logger Integration

The module supports external loggers for lock operations. Winston works directly, while Pino and Bunyan require adapters:

Winston (Direct Support)

import * as winston from 'winston';

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

RedlockModule.forRoot({
  nodes: [new NodeRedisAdapter(redis1)],
  logger, // Winston works directly
})

Pino (Requires Adapter)

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

const pinoLogger = pino({ level: 'info' });
const logger = createPinoAdapter(pinoLogger);

RedlockModule.forRoot({
  nodes: [new NodeRedisAdapter(redis1)],
  logger,
})

Bunyan (Requires Adapter)

import * as bunyan from 'bunyan';
import { createBunyanAdapter } from 'redlock-universal';

const bunyanLogger = bunyan.createLogger({ name: 'myapp', level: 'info' });
const logger = createBunyanAdapter(bunyanLogger);

RedlockModule.forRoot({
  nodes: [new NodeRedisAdapter(redis1)],
  logger,
})

Supported Loggers:

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

API Reference

@Redlock Decorator

Automatically wraps a method with lock acquisition and release.

@Redlock(options: RedlockDecoratorOptions)

interface RedlockDecoratorOptions {
  // Static key or function that generates key from arguments
  key: string | ((...args: unknown[]) => string);
  // Lock time-to-live in milliseconds (default: module defaultTtl)
  ttl?: number;
  // Number of retry attempts (default: module retryAttempts)
  retryAttempts?: number;
  // Delay between retries in milliseconds (default: module retryDelay)
  retryDelay?: number;
}

Examples:

// Static key
@Redlock({ key: 'global:config:update' })
async updateConfig() { }

// Dynamic key from arguments
@Redlock({ key: (id: string) => `resource:${id}:lock` })
async updateResource(id: string) { }

// Custom TTL
@Redlock({ key: 'long:operation', ttl: 300000 }) // 5 minutes
async longRunningTask() { }

// Multiple arguments
@Redlock({ key: (type: string, id: string) => `${type}:${id}:lock` })
async process(type: string, id: string) { }

RedlockService

Injectable service for programmatic lock management.

acquire(key: string, ttl?: number): Promise<LockHandle>

Acquire a lock manually. Returns a handle that must be passed to release().

const handle = await redlockService.acquire('resource:123');
try {
  // Critical section
} finally {
  await redlockService.release('resource:123', handle);
}

release(key: string, handle: LockHandle): Promise<void>

Release a previously acquired lock using its handle.

using<T>(key: string, fn: (signal?: AbortSignal) => Promise<T>, ttl?: number): Promise<T>

Execute a function with automatic lock management. Recommended for most use cases.

const result = await redlockService.using('resource:123', async (signal) => {
  // Lock is automatically acquired, extended, and released

  // Optional: Check if lock extension failed
  if (signal?.aborted) {
    throw new Error('Lock lost during operation');
  }

  return processResource();
});

Advanced Usage

Accessing Advanced Features

For advanced features like batch operations, health checks, and metrics, import directly from redlock-universal:

import { RedlockService } from 'nestjs-redlock-universal';
import { LockManager, HealthChecker, MetricsCollector } from 'redlock-universal';

@Injectable()
export class AdvancedService {
  constructor(private readonly redlock: RedlockService) {}

  async batchOperations() {
    // Use redlock-universal directly for batch locks
    const manager = new LockManager({ nodes: [...] });
    const handles = await manager.acquireBatch(['key1', 'key2', 'key3']);
    // ... process
    await manager.releaseBatch(handles);
  }
}

Single-Node Setup (Development)

For development or single-instance deployments:

import { RedlockModule, NodeRedisAdapter } from 'nestjs-redlock-universal';
import { createClient } from 'redis';

const redis = createClient({ url: 'redis://localhost:6379' });
await redis.connect();

RedlockModule.forRoot({
  nodes: [new NodeRedisAdapter(redis)],
  // Automatically uses SimpleLock instead of RedLock for single node
})

Lock Key Best Practices

// ✅ Good: Specific, hierarchical keys
@Redlock({ key: (userId) => `user:${userId}:profile:update` })

// ✅ Good: Include resource type
@Redlock({ key: (orderId) => `order:${orderId}:payment` })

// ❌ Bad: Too generic
@Redlock({ key: 'update' })

// ❌ Bad: No namespace
@Redlock({ key: (id) => id })

Lock Strategy Selection

The module automatically selects the optimal lock strategy:

  • 1-2 nodes: Uses SimpleLock (single-instance locking)
  • 3+ nodes: Uses RedLock (distributed Redlock algorithm)

For production deployments, always use 3 or more Redis instances for proper fault tolerance.

Testing

Mocking in Tests

import { Test } from '@nestjs/testing';
import { RedlockService } from 'nestjs-redlock-universal';

const module = await Test.createTestingModule({
  providers: [
    YourService,
    {
      provide: RedlockService,
      useValue: {
        using: vi.fn((key, fn) => fn()),
        acquire: vi.fn(),
        release: vi.fn(),
      },
    },
  ],
}).compile();

Integration Testing

See TESTING.md for complete integration testing guide with Docker.

Performance

Based on redlock-universal benchmarks:

  • Acquisition latency: <1ms mean (P95: <2ms)
  • Throughput: 3,300+ ops/sec (single node)
  • Batch operations: 500+ ops/sec (10-lock batches)
  • Memory: <2KB per lock operation

Common Use Cases

1. Prevent Duplicate Processing

@Redlock({ key: (jobId) => `job:${jobId}:process` })
async processJob(jobId: string) {
  // Ensures only one worker processes this job
}

2. Exclusive Resource Access

@Redlock({ key: (userId) => `user:${userId}:wallet` })
async updateWallet(userId: string, amount: number) {
  // Prevents race conditions in balance updates
}

3. Rate Limiting Critical Operations

@Redlock({ key: 'api:external:call', ttl: 1000 })
async callRateLimitedAPI() {
  // Ensures max 1 call per second across all instances
}

4. Coordinated Cache Invalidation

@Redlock({ key: 'cache:rebuild' })
async rebuildCache() {
  // Only one instance rebuilds cache at a time
}

Troubleshooting

Lock Acquisition Fails

Problem: LockAcquisitionError: Failed to acquire lock

Solutions:

  • Check Redis connectivity: Ensure all nodes are reachable
  • Verify quorum settings: Need majority of nodes (or configured quorum)
  • Check lock contention: Another process may hold the lock
  • Increase retry attempts or delay in configuration

Lock Released Too Early

Problem: Lock expires during long operation

Solutions:

  • Use using() method instead of manual acquire()/release() - it auto-extends
  • Increase defaultTtl in module configuration
  • Check if operation can be split into smaller atomic operations

Memory Leaks

Problem: Memory usage grows over time

Solutions:

  • Ensure proper module cleanup (we handle this automatically via onModuleDestroy)
  • Check for uncaught errors that prevent lock release
  • Use using() method to guarantee cleanup

Module Not Initializing

Problem: LockManager not initialized error

Solutions:

  • Ensure Redis clients are connected before module initialization
  • Check for errors in forRootAsync factory function
  • Verify onModuleInit lifecycle hook completes successfully

For more help, see:

Why nestjs-redlock-universal?

vs. Raw Redis SET NX

❌ Manual lock release ❌ No automatic extension ❌ No distributed consensus ❌ Race conditions in cleanup

✅ Automatic lifecycle management ✅ Auto-extension for long operations ✅ Distributed locking with quorum ✅ Enhanced error handling

vs. Other NestJS Redis Libraries

Most NestJS Redis libraries focus on caching. This library is purpose-built for distributed locking:

  • ✅ Redlock algorithm implementation
  • ✅ Automatic lock extension via using()
  • ✅ NestJS decorator for zero-boilerplate
  • ✅ Built on redlock-universal
  • ✅ Universal Redis/Valkey client support (node-redis, ioredis, Valkey GLIDE)

License

MIT

Related Projects

Contributing

Issues and pull requests are welcome! Please see our contributing guidelines.

Support