Leader Election Plugin
Run singleton work — cron jobs, migrations, cleanup — on exactly one instance of your fleet.
Overview
Every multi-instance NestJS deployment hits the same problem: @Cron fires on every replica. Emails go out twice, cleanup jobs race, migrations collide. @nestjs-redisx/leader elects one leader per group over a Redis lease and gives you a one-line decorator to gate singleton work.
Key Features
@LeaderOnly()decorator — proxy-based, works on any regular@Injectablemethod (generators excluded); composes with@nestjs/schedule(@Cronfires everywhere, the body runs only on the leader)- Lease-based election — atomic acquire (
SET NX PX, re-claiming a still-held own lease), CAS Lua heartbeat, CAS release; single-key and cluster-safe - Automatic failover — instant on graceful shutdown (lease released), bounded by
ttlMs + retryIntervalMson a crash - Fail-safe local view —
isLeader()is true only while the last confirmed lease is unexpired - Independent groups — separate elections per concern; decorator-referenced groups start automatically
- Observability —
onElected/onLostcallbacks andredisx_leader_*counters with the Metrics plugin
Installation
bash
npm install @nestjs-redisx/core @nestjs-redisx/leader ioredisbash
npm install @nestjs-redisx/core @nestjs-redisx/leader redisBasic Configuration
typescript
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { RedisModule } from '@nestjs-redisx/core';
import { LeaderPlugin } from '@nestjs-redisx/leader';
@Module({
imports: [
// @Cron fires on EVERY instance; @LeaderOnly gates the body
ScheduleModule.forRoot(),
RedisModule.forRoot({
clients: {
host: 'localhost',
port: 6379,
},
plugins: [
new LeaderPlugin({
ttlMs: 15_000, // leadership lease; crash failover bound
renewIntervalMs: 5_000, // heartbeat while leader (must be < ttlMs)
retryIntervalMs: 2_000, // acquire retry while follower
}),
],
}),
],
})
export class AppModule {}Usage with @Cron
typescript
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { LeaderOnly } from '@nestjs-redisx/leader';
import { ReportBuilder } from './types';
@Injectable()
export class ReportJobs {
constructor(private readonly reports: ReportBuilder) {}
// The cron fires on every instance of the fleet;
// the body runs only where the lease is held.
@Cron(CronExpression.EVERY_HOUR)
@LeaderOnly()
async hourlyReport() {
await this.reports.build('hourly');
}
// Independent election group: a different instance may lead it.
@Cron(CronExpression.EVERY_5_MINUTES)
@LeaderOnly({ group: 'cleanup' })
async cleanupExpired() {
await this.reports.cleanup();
}
}Start two copies of the app and watch exactly one of them run the job. When that copy stops or crashes, the other takes over automatically.
Programmatic API
typescript
import { LEADER_SERVICE, ILeaderService } from '@nestjs-redisx/leader';
constructor(@Inject(LEADER_SERVICE) private leader: ILeaderService) {}
this.leader.isLeader(); // sync local view
await this.leader.getLeaderId(); // who leads (any instance)
await this.leader.runIfLeader(() => ...); // gated execution
await this.leader.stepDown(); // hand leadership overDocumentation
| Topic | Description |
|---|---|
| Core Concepts | The lease model, guarantees, and caveats |
| Configuration | All plugin options |
| @LeaderOnly Decorator | Gating methods by leadership |
| Service API | Programmatic leadership checks |
| Failover | Timings for graceful shutdown, crash, and step-down |
| Monitoring | Metrics, callbacks, and status endpoints |
| Testing | Testing without Redis on the in-memory driver |
| Recipes | Singleton crons, startup work, maintenance drains |
| Troubleshooting | Debugging common issues |