Skip to content

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 @Injectable method (generators excluded); composes with @nestjs/schedule (@Cron fires 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 + retryIntervalMs on 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 / onLost callbacks and redisx_leader_* counters with the Metrics plugin

Installation ​

bash
npm install @nestjs-redisx/core @nestjs-redisx/leader ioredis
bash
npm install @nestjs-redisx/core @nestjs-redisx/leader redis

Basic 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 over

Documentation ​

TopicDescription
Core ConceptsThe lease model, guarantees, and caveats
ConfigurationAll plugin options
@LeaderOnly DecoratorGating methods by leadership
Service APIProgrammatic leadership checks
FailoverTimings for graceful shutdown, crash, and step-down
MonitoringMetrics, callbacks, and status endpoints
TestingTesting without Redis on the in-memory driver
RecipesSingleton crons, startup work, maintenance drains
TroubleshootingDebugging common issues

Released under the MIT License.