How to Run Background Jobs in Node.js
A CPU-heavy cron job blocks the event loop. While it runs, your HTTP server stops responding, WebSocket connections drop, and other scheduled tasks miss their window. The job finishes, but the collateral damage is already done.
The problem
Node.js runs JavaScript on a single thread. When a scheduled task does heavy computation, file I/O, or a long synchronous operation, everything else in the process waits:
main thread: ──── cron fires ──── heavy job (3s) ──── blocked ────
↑ HTTP requests queue up
↑ other cron tasks miss their tick
↑ WebSocket pings time outThe solution: background tasks
node-cron can run any scheduled job in an isolated forked process. Pass a file path instead of a function, and the job runs in its own process, completely isolated from your main event loop:
// tasks/generate-report.js
export function task() {
// This runs in a separate process.
// Heavy computation here won't block your HTTP server.
return generateMonthlyReport();
}// app.js
import cron from 'node-cron';
cron.schedule('0 2 1 * *', './tasks/generate-report.js');main thread: ──── cron fires ──── continues serving requests ────
forked process: ──── heavy job (3s) ──── done ────Your HTTP server keeps responding. Other cron tasks fire on time. The background job runs to completion in its own process.
Same interface, same features
A background task implements the same ScheduledTask interface as an inline task. The only difference: control methods return Promises because they cross a process boundary.
const task = cron.schedule('0 2 * * *', './tasks/report.js');
await task.stop(); // terminates the child process
await task.start(); // re-forks and resumes
await task.destroy(); // kills the process and removes the task
task.getStatus(); // 'idle', 'running', etc.
task.getNextRun(); // next scheduled DateEvents and observability
Background tasks emit the same lifecycle events, relayed from the worker to the parent:
const task = cron.schedule('0 2 * * *', './tasks/report.js');
task.on('execution:started', () => console.log('report generation started'));
task.on('execution:finished', (ctx) => console.log('done:', ctx.execution?.result));
task.on('execution:failed', (ctx) => console.error('failed:', ctx.execution?.error));Manual execution
Trigger a background task immediately with execute():
const task = cron.schedule('0 2 * * *', './tasks/report.js');
const result = await task.execute();Guard against a worker that never reports back with executeTimeout:
const task = cron.schedule('0 2 * * *', './tasks/report.js', {
executeTimeout: 60_000,
});Combining with other features
With overlap prevention
cron.schedule('*/5 * * * *', './tasks/sync.js', {
noOverlap: true,
});The scheduler tracks whether the forked process is still active and skips the next tick if it is.
With distributed coordination
import { setRunCoordinator } from 'node-cron';
import { RedisLockCoordinator } from '@node-cron/redis-coordinator';
setRunCoordinator(new RedisLockCoordinator(redis));
cron.schedule('0 3 * * *', './tasks/backup.js', {
name: 'nightly-backup',
distributed: true,
});Coordination happens in the parent process over IPC. No extra configuration needed.
With timezones
cron.schedule('0 3 * * *', './tasks/backup.js', {
timezone: 'America/Sao_Paulo',
});Writing a task file
A task file exports a task function. It receives a TaskContext with scheduling metadata:
// tasks/cleanup.js
export function task(ctx) {
console.log('scheduled for:', ctx.dateLocalIso);
return cleanupOldRecords();
}The function can be sync or async. Its return value is available in the execution:finished event and through lastRun().
When background tasks are not enough
Background tasks isolate work from your main event loop, but the job still runs inside your Node.js application lifecycle. For scenarios that need more:
- Persistent job queues with retries: BullMQ or Sidequest persist jobs to Redis/database and retry on failure.
- Worker pools: for CPU-bound work that needs multiple parallel workers, consider piscina or workerpool.
- External job runners: for jobs that outlive your process, use a managed scheduler like AWS EventBridge or GCP Cloud Scheduler.
Next steps
- Background Tasks: the full reference, including
startTimeoutand limitations. - How to prevent overlapping cron jobs: prevent duplicate executions.
- How to run cron jobs across multiple servers: coordinate across instances.