How to Run Cron Jobs Across Multiple Servers
You deploy three replicas of your Node.js app behind a load balancer. All three have the same code, the same schedule, and the same cron job. At 3 a.m., the nightly backup runs three times instead of once.
This happens in every multi-instance setup: PM2 cluster mode, Kubernetes Deployments, blue/green rollouts, auto-scaling groups. The scheduler doesn't know about other copies of itself.
The problem
instance A: 0 3 * * * → runs backup
instance B: 0 3 * * * → runs backup (duplicate)
instance C: 0 3 * * * → runs backup (duplicate)The solution: distributed: true
node-cron has built-in distributed coordination. Set distributed: true and only one instance executes each scheduled fire:
import cron from 'node-cron';
cron.schedule('0 3 * * *', runNightlyBackup, {
name: 'nightly-backup',
distributed: true,
});instance A: 0 3 * * * → runs backup ✓
instance B: 0 3 * * * → skipped (not elected)
instance C: 0 3 * * * → skipped (not elected)Two things are required:
- A
namefor the task (it forms the coordination key shared across instances) - A way to decide which instance runs: the default uses an env-var flag, or you can plug in a Redis coordinator for high availability
Option 1: Designated runner (env-var)
The simplest setup. Designate one instance as the runner:
# instance A (the runner)
NODE_CRON_RUN=true node app.js
# instances B, C, D
NODE_CRON_RUN=false node app.jsThis works well when your orchestrator already decides which pod is special: a StatefulSet, a single-replica Deployment, a dedicated worker dyno.
If NODE_CRON_RUN is unset, node-cron throws at schedule time (on startup, not silently at 3 a.m.), so a misconfigured deploy fails loudly.
Trade-off
The env-var default is a single designated runner. If instance A is down at 3 a.m., the backup doesn't run. For high availability, use a coordinator.
Option 2: Redis coordinator (high availability)
With a Redis coordinator, every replica races for each fire, exactly one wins, and if the winner is down another takes over:
npm install @node-cron/redis-coordinatorimport { createClient } from 'redis';
import cron, { setRunCoordinator } from 'node-cron';
import { RedisLockCoordinator } from '@node-cron/redis-coordinator';
const redis = createClient();
await redis.connect();
setRunCoordinator(new RedisLockCoordinator(redis));
cron.schedule('0 3 * * *', runNightlyBackup, {
name: 'nightly-backup',
distributed: true,
distributedLease: 5 * 60_000,
});No special instance needed. Works with both ioredis and node-redis v4.
Knowing when an instance skips
When an instance is not elected to run, it emits execution:skipped:
task.on('execution:skipped', (ctx) => {
if (ctx.reason === 'coordinator-error') {
alert('coordination backend is down', ctx);
}
});| Reason | Meaning |
|---|---|
not-elected | Healthy. Another instance ran this fire. |
coordinator-error | The coordinator failed (e.g. Redis down). The fire may not have run anywhere. Alert on this. |
Custom coordinators
The Redis coordinator is one implementation of the RunCoordinator interface. You can back coordination with anything your fleet shares:
interface RunCoordinator {
shouldRun(key: string, ttlMs: number): boolean | Promise<boolean>;
onComplete?(key: string): void | Promise<void>;
}Postgres, etcd, DynamoDB, or any distributed lock mechanism works.
Combining with other features
With overlap prevention
cron.schedule('0 * * * *', syncInventory, {
name: 'sync-inventory',
distributed: true,
noOverlap: true,
});One instance runs each fire, and within that instance, overlapping runs are prevented.
With background tasks
setRunCoordinator(new RedisLockCoordinator(redis));
cron.schedule('0 3 * * *', './tasks/backup.js', {
name: 'nightly-backup',
distributed: true,
});The forked daemon asks the parent over IPC, and the parent runs the coordinator. No extra configuration.
When distributed coordination is not enough
node-cron's distributed mode guarantees no concurrent execution across instances when clocks are in sync. It is not a hard exactly-once: under a crash-and-retry or large clock skew, a fire could run more than once. Treat distributed tasks as idempotent.
For stronger guarantees:
Next steps
- Distributed Coordination: the full reference, including lease tuning and clock skew.
- How to prevent overlapping cron jobs: overlap prevention within a single instance.
- How to run background jobs in Node.js: isolate heavy work in forked processes.