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

v1.0.0

Published

Production-ready distributed locking for NestJS. Decorator-first, type-safe.

Downloads

616

Readme

nestjs-redlock

npm version npm downloads CI License: MIT

Production-ready distributed locking for NestJS. Decorator-first, type-safe.

Why this package?

| Feature | nestjs-redlock | @anchan828/nest-redlock (archived) | nestjs-redis-lock | |---|---|---|---| | Maintained | ✅ Yes | ❌ Archived | ⚠️ Unmaintained | | @Lock() decorator | ✅ Yes | ✅ Yes | ❌ No | | onFail: 'skip' | ✅ Yes | ❌ No | ❌ No | | Dynamic lock keys | ✅ Yes | ❌ No | ❌ No | | Dual CJS + ESM | ✅ Yes | ❌ No | ❌ No | | TypeScript strict mode | ✅ Yes | ⚠️ Partial | ⚠️ Partial |

Built from a production booking system handling 90,000+ route combinations.

Install

npm install nestjs-redlock ioredis

Quick Start

Register the module once in AppModule:

import { LockModule } from 'nestjs-redlock';
import Redis from 'ioredis';

@Module({
  imports: [
    LockModule.register({
      clients: [new Redis(process.env.REDIS_URL)],
    }),
  ],
})
export class AppModule {}

Use the @Lock() decorator on any route handler:

import { Lock } from 'nestjs-redlock';

@Controller('bookings')
export class BookingController {
  @Post()
  @Lock({ key: (args) => `booking:${args[0].propertyId}`, onFail: 'throw' })
  async create(@Body() dto: CreateBookingDto) {
    return this.bookingService.create(dto);
  }
}

Or use LockService directly:

import { LockService } from 'nestjs-redlock';

@Injectable()
export class PaymentService {
  constructor(private readonly lockService: LockService) {}

  async processPayment(orderId: string) {
    return this.lockService.withLock(
      `payment:${orderId}`,
      async () => this.chargeCard(orderId),
      10000,
    );
  }
}

Service API

withLock<T>(resource, callback, duration?): Promise<T>

Acquire → execute → release. The safe default for all locking. Always releases in finally, even if the callback throws.

const result = await lockService.withLock('inventory:update', async () => {
  return updateStock(productId, quantity);
});

tryLock(resource, duration?): Promise<Lock | null>

Non-throwing. Returns null if the lock is already held. Good for cron job deduplication.

const lock = await lockService.tryLock('cron:daily-sync', 30000);
if (!lock) return; // Another instance is already running
try {
  await runSync();
} finally {
  await lock.release();
}

extend(lock, duration): Promise<Lock>

Extend an existing lock's TTL. Throws LockExtendException if the lock has already expired.

lock = await lockService.extend(lock, 5000);

isLocked(resource): Promise<boolean>

Point-in-time check. Informational only — do not use for locking decisions.

const busy = await lockService.isLocked('payment:process');

Decorator Options

| Option | Type | Default | Description | |---|---|---|---| | key | string \| string[] \| ((args) => string \| string[]) | required | Lock resource key. Static, array, or dynamic function. | | duration | number | Module default (5000ms) | Lock TTL in milliseconds. | | onFail | 'throw' \| 'skip' | 'throw' | Behavior when lock is unavailable. 'skip' returns undefined silently. | | autoExtend | boolean | false | Automatically re-extend the lock every duration/2 ms until the callback completes. | | queue | boolean | false | FIFO queue via Redis List. Requests wait in order instead of competing with jitter. |

Static key

@Lock({ key: 'report:generate', duration: 30000 })
async generateReport(): Promise<Report> { ... }

Dynamic key

@Lock({ key: (args) => `booking:${args[0].propertyId}` })
async createBooking(@Body() dto: CreateBookingDto) { ... }

Skip on failure (cron deduplication)

// Only one instance in a cluster runs this job per tick
@Cron(CronExpression.EVERY_10_SECONDS)
@Lock({ key: 'cron:cleanup', onFail: 'skip' })
async cleanupExpiredSessions() { ... }

Lock groups (multi-resource atomic locking)

// Acquire all seats atomically. Sorted key order prevents deadlocks.
@Lock({ key: (args) => [`seat:${args[0].seatA}`, `seat:${args[0].seatB}`] })
async swapSeats(@Body() dto: SwapDto) { ... }

Auto-extend for long-running operations

// Lock automatically re-extends every duration/2 ms
@Lock({ key: 'report:generate', duration: 30000, autoExtend: true })
async generateHeavyReport() { ... }

FIFO queue (fair locking)

// Requests are served first-come-first-served, not random retry
@Lock({ key: 'checkout:process', queue: true })
async processCheckout(@Body() dto: CheckoutDto) { ... }

Configuration

Synchronous

LockModule.register({
  clients: [new Redis()],        // required: one or more ioredis instances
  duration: 5000,                // default lock TTL (ms)
  retryCount: 3,                 // acquisition retries
  retryDelay: 200,               // delay between retries (ms)
  retryJitter: 100,              // random jitter added to delay (ms)
  driftFactor: 0.01,             // clock drift compensation
  keyPrefix: 'lock',             // prefix for all lock keys
})

Async (with ConfigService)

LockModule.registerAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    clients: [new Redis(config.get('REDIS_URL'))],
    duration: config.get<number>('LOCK_TTL', 5000),
  }),
})

Production: Multi-node Redlock

LockModule.register({
  // 3+ Redis nodes for consensus-based locking
  clients: [
    new Redis({ host: 'redis-1', port: 6379 }),
    new Redis({ host: 'redis-2', port: 6379 }),
    new Redis({ host: 'redis-3', port: 6379 }),
  ],
})

| Option | Default | Description | |---|---|---| | clients | required | ioredis Redis instances (1 for dev, 3+ for prod) | | duration | 5000 | Default lock TTL in milliseconds | | retryCount | 3 | Number of acquisition retry attempts | | retryDelay | 200 | Base delay between retries (ms) | | retryJitter | 100 | Random jitter added to retry delay (ms) | | driftFactor | 0.01 | Clock drift compensation factor | | keyPrefix | 'lock' | Prefix for all Redis lock keys |

Events (Prometheus / DataDog / Sentry)

LockService extends Node's EventEmitter. Hook into lock lifecycle events for metrics and alerting:

import { LockService, LockEvent } from 'nestjs-redlock';

@Injectable()
export class MetricsService {
  constructor(lockService: LockService) {
    lockService.on(LockEvent.ACQUIRED, (resource: string, duration: number) => {
      prometheus.counter('lock_acquired_total').inc({ resource });
    });

    lockService.on(LockEvent.RELEASED, (resource: string, heldForMs: number) => {
      prometheus.histogram('lock_held_duration_ms').observe({ resource }, heldForMs);
    });

    lockService.on(LockEvent.FAILED, (resource: string, reason: Error) => {
      sentry.captureException(reason, { extra: { resource } });
    });

    lockService.on(LockEvent.EXTENDED, (resource: string, newDuration: number) => {
      prometheus.counter('lock_extended_total').inc({ resource });
    });
  }
}

| Event | Arguments | Fired when | |---|---|---| | LockEvent.ACQUIRED | resource, duration | Lock successfully acquired | | LockEvent.RELEASED | resource, heldForMs | Lock released (success or error) | | LockEvent.FAILED | resource, error | Lock acquisition failed | | LockEvent.EXTENDED | resource, newDuration | Lock TTL extended |

Testing (FakeLockService)

Replace LockService with FakeLockService in unit tests — no Redis required:

import { FakeLockService } from 'nestjs-redlock/testing';
import { LockService } from 'nestjs-redlock';

describe('PaymentService', () => {
  let service: PaymentService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        PaymentService,
        { provide: LockService, useClass: FakeLockService },
      ],
    }).compile();

    service = module.get(PaymentService);
  });

  it('processes payment', async () => {
    // withLock() simply runs the callback — no Redis, no retries
    await expect(service.processPayment('order-123')).resolves.toBeDefined();
  });
});

Error Handling

| Exception | HTTP Status | When | |---|---|---| | LockAcquisitionException | 409 Conflict | Lock unavailable after all retries (when onFail: 'throw') | | LockExtendException | 500 Internal Server Error | Lock expired before extend() could run |

Migrating from @anchan828/nest-redlock

@anchan828/nest-redlock has been archived and is no longer maintained. Here's how to migrate in 3 steps.

1. Update dependencies

npm uninstall @anchan828/nest-redlock
npm install nestjs-redlock

2. Update module registration

// Before
import { RedlockModule } from '@anchan828/nest-redlock';
RedlockModule.registerAsync({
  useFactory: () => ({
    clients: [new Redis()],
    settings: { retryCount: 3 },
  }),
})

// After
import { LockModule } from 'nestjs-redlock';
LockModule.register({
  clients: [new Redis()],
  retryCount: 3,        // flat options — no nested "settings" object
})

3. Update decorators

// Before
import { Redlock } from '@anchan828/nest-redlock';
@Redlock(['resource'])
async myMethod() { ... }

// After
import { Lock } from 'nestjs-redlock';
@Lock({ key: 'resource' })
async myMethod() { ... }

What you gain

  • Dynamic lock keyskey: (args) => \booking:${args[0].id}``
  • onFail: 'skip' — silent skip for cron jobs, no try/catch needed
  • Lock groups — atomic multi-resource locking with deadlock prevention
  • Event emitter — Prometheus/DataDog/Sentry hooks
  • FIFO queued locking — fair ordering instead of random retry stampede
  • FakeLockService — unit tests with zero Redis dependency
  • Dual CJS + ESM — works in modern ESM projects
  • Active maintenance — no archived package risk

License

MIT — Asim Neupane