Skip to content

Recipes ​

Singleton Cron Across the Fleet ​

The canonical use case — schedule everywhere, execute once:

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();
  }
}

Run-Once Startup Work ​

Warmups or maintenance tasks that should run on at most one instance when a group starts leaderless (fresh deploy, scale-from-zero, disaster recovery):

typescript
@Injectable()
export class StartupTasks implements OnApplicationBootstrap {
  constructor(@Inject(LEADER_SERVICE) private readonly leader: ILeaderService) {}

  async onApplicationBootstrap() {
    // The first election round completes during module init, so this is
    // already meaningful here.
    await this.leader.runIfLeader(() => this.warmCaches());
  }

  private async warmCaches() {
    /* ... */
  }
}

At most once — and usually ZERO times in a rolling update

onApplicationBootstrap fires once per process, and during a rolling update the lease is normally still held by an old pod when each new pod boots — every new pod evaluates runIfLeader as a follower and skips, and when the old leader finally exits, the seat goes to an instance whose hook already ran. For work that must run once per rollout/version, don't gate it on leadership: make it idempotent and key it by version (e.g. @WithLock + a done:{version} marker), or run it in your deploy/migration step. "Leader runs the migration" is a scheduling convenience, not a safety proof — during rollouts old and new versions briefly coexist; make the migration itself idempotent, or wrap it with @WithLock / track applied versions.

Maintenance Drain ​

Hand leadership over before restarting a node:

typescript
import { Injectable, Inject } from '@nestjs/common';
import { LEADER_SERVICE, ILeaderService } from '@nestjs-redisx/leader';

@Injectable()
export class MaintenanceService {
  constructor(
    @Inject(LEADER_SERVICE)
    private readonly leaderService: ILeaderService,
  ) {}

  /**
   * Before planned maintenance of this node: release the lease and abstain
   * from candidacy for one ttlMs, so another instance takes over immediately
   * instead of waiting for the lease to expire.
   */
  async drainThisNode(): Promise<void> {
    await this.leaderService.stepDown();
    // Optionally also for named groups:
    await this.leaderService.stepDown('cleanup');
  }
}

Spreading Singleton Workloads ​

Different groups elect independently — heavy singleton jobs can land on different instances:

typescript
@Cron(CronExpression.EVERY_HOUR)
@LeaderOnly({ group: 'reports' })
async buildReports() {}

@Cron(CronExpression.EVERY_10_MINUTES)
@LeaderOnly({ group: 'reindex' })
async reindexSearch() {}

Fencing Critical Sections ​

Election chooses who starts work; a lock guarantees exclusivity inside it:

typescript
@Cron(CronExpression.EVERY_5_MINUTES)
@LeaderOnly()
@WithLock({ key: 'billing:charge-run', ttl: 240_000 })
async chargeRun() {
  // Never overlaps, even across a leadership handover with work in flight
}

Leadership-Aware Health Output ​

Expose who leads for dashboards and on-call debugging:

typescript
import { Injectable, Inject } from '@nestjs/common';
import { LEADER_SERVICE, ILeaderService } from '@nestjs-redisx/leader';

@Injectable()
export class LeadershipStatusService {
  constructor(
    @Inject(LEADER_SERVICE)
    private readonly leaderService: ILeaderService,
  ) {}

  // Synchronous local view — safe to call on every request.
  isThisInstanceTheLeader(): boolean {
    return this.leaderService.isLeader();
  }

  // Works on ANY instance: reads the election key from Redis.
  async whoLeads(): Promise<string | null> {
    return this.leaderService.getLeaderId();
  }

  // Execute singleton work programmatically (without a decorator).
  async maybeCompact(): Promise<number | undefined> {
    return this.leaderService.runIfLeader(async () => {
      // ...heavy singleton work...
      return 42;
    });
  }

  status() {
    return {
      instanceId: this.leaderService.instanceId,
      isLeader: this.leaderService.isLeader(),
      groups: this.leaderService.getGroups(),
    };
  }
}

Next Steps ​

Released under the MIT License.