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




NestJS wrapper for redlock-universal, providing distributed Redis and Valkey locks through NestJS decorators, modules, and dependency injection.
- 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+
- Installation
- Quick Start
- Configuration
- Using ioredis
- Using Valkey GLIDE
- API Reference
- Advanced Usage
- Testing
- Troubleshooting
- License
This package wraps redlock-universal, so you need to install both packages:
``bash`
npm install nestjs-redlock-universal redlock-universal
You'll also need a Redis/Valkey client:
`bashFor node-redis
npm install redis
Quick Start
$3
`typescript
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 {}
`$3
`typescript
import { Injectable } from '@nestjs/common';
import { Redlock } from 'nestjs-redlock-universal';@Injectable()
export class PaymentService {
@Redlock({ key: 'payment:processing' })
async processPayment(orderId: string): Promise {
// 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 {
// Lock key is dynamically generated from method arguments
// Each user gets their own lock
}
}
`$3
`typescript
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 {
// 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 {
// 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
$3
`typescript
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
})
`$3
`typescript
import { ConfigService } from '@nestjs/config';
import { RedlockModule, NodeRedisAdapter } from 'nestjs-redlock-universal';RedlockModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
const redisUrls = configService.get('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],
})
`$3
`typescript
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),
],
})
`$3
`typescript
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),
],
})
`$3
The module supports external loggers for lock operations. Winston works directly, while Pino and Bunyan require adapters:
#### Winston (Direct Support)
`typescript
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)
`typescript
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)
`typescript
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
$3
Automatically wraps a method with lock acquisition and release.
`typescript
@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:
`typescript
// 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) { }
`$3
Injectable service for programmatic lock management.
####
acquire(key: string, ttl?: number): PromiseAcquire a lock manually. Returns a handle that must be passed to
release().`typescript
const handle = await redlockService.acquire('resource:123');
try {
// Critical section
} finally {
await redlockService.release('resource:123', handle);
}
`####
release(key: string, handle: LockHandle): PromiseRelease a previously acquired lock using its handle.
####
usingExecute a function with automatic lock management. Recommended for most use cases.
`typescript
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
$3
For advanced features like batch operations, health checks, and metrics, import directly from
redlock-universal:`typescript
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);
}
}
`$3
For development or single-instance deployments:
`typescript
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
})
`$3
`typescript
// ✅ 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
$3
`typescript
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();
`$3
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
$3
`typescript
@Redlock({ key: (jobId) => job:${jobId}:process })
async processJob(jobId: string) {
// Ensures only one worker processes this job
}
`$3
`typescript
@Redlock({ key: (userId) => user:${userId}:wallet })
async updateWallet(userId: string, amount: number) {
// Prevents race conditions in balance updates
}
`$3
`typescript
@Redlock({ key: 'api:external:call', ttl: 1000 })
async callRateLimitedAPI() {
// Ensures max 1 call per second across all instances
}
`$3
`typescript
@Redlock({ key: 'cache:rebuild' })
async rebuildCache() {
// Only one instance rebuilds cache at a time
}
`Troubleshooting
$3
Problem:
LockAcquisitionError: Failed to acquire lockSolutions:
- 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
$3
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$3
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$3
Problem:
LockManager not initialized errorSolutions:
- Ensure Redis clients are connected before module initialization
- Check for errors in
forRootAsync factory function
- Verify onModuleInit lifecycle hook completes successfullyFor more help, see:
- redlock-universal documentation
- Report an issue
Why nestjs-redlock-universal?
$3
❌ 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
$3
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`MIT
- redlock-universal - The underlying distributed lock library
Issues and pull requests are welcome! Please see our contributing guidelines.
- Report bugs
- Request features
- Documentation
- Star on GitHub