Recipes
Singleton Cron Across the Fleet
The canonical use case — schedule everywhere, execute once:
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):
@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:
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:
@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:
@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:
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
- Failover — what happens during handovers
- Troubleshooting — when a job doesn't run