Skip to content

Monitor Node.js scheduled jobs

Scheduled work in a Node.js app stops quietly. The process that holds the schedule restarts without it, a worker crashes, or the job starts throwing and the error is swallowed by a promise nobody awaits. A heartbeat check catches all of these: the job reports to Pingstack after it succeeds, and Pingstack alerts you when that report doesn’t arrive on time.

These examples use the built-in fetch in Node 18 and later.

1. Create a heartbeat check that matches the schedule

Section titled “1. Create a heartbeat check that matches the schedule”

Create a heartbeat check and set its schedule to Cron with the same expression your job uses, such as 0 2 * * *. Set the timezone explicitly in your scheduler (shown below) and choose the same one in Pingstack. See schedules and timezones.

Set a grace period longer than the job’s usual runtime, plus a margin. Attach a notification group.

Copy the location’s ping URL from the check in Pingstack and set it as an environment variable for the process that runs the job, such as PINGSTACK_NIGHTLY_URL, through your host’s secret settings. Don’t commit it to a .env file in the repository.

Add a small helper:

heartbeat.js
export async function reportToPingstack(url, value) {
if (!url) return;
const target = new URL(url);
if (value !== undefined) target.searchParams.set('value', String(value));
try {
const response = await fetch(target, { signal: AbortSignal.timeout(10_000) });
if (!response.ok) console.warn(`Pingstack heartbeat returned ${response.status}`);
} catch (error) {
console.warn(`Pingstack heartbeat failed: ${error.name}`);
}
}
import cron from 'node-cron';
import { reportToPingstack } from './heartbeat.js';
cron.schedule('0 2 * * *', async () => {
try {
await sendNightlyReport();
await reportToPingstack(process.env.PINGSTACK_NIGHTLY_URL);
} catch (error) {
console.error('Nightly report failed', error);
}
}, { timezone: 'Etc/UTC' });

Register the schedule once, then report from the end of the processor:

import { Queue, Worker } from 'bullmq';
import { reportToPingstack } from './heartbeat.js';
const connection = { host: 'localhost', port: 6379 };
const queue = new Queue('reports', { connection });
await queue.upsertJobScheduler('nightly-report', {
pattern: '0 2 * * *',
tz: 'Etc/UTC',
});
new Worker('reports', async (job) => {
await sendNightlyReport();
await reportToPingstack(process.env.PINGSTACK_NIGHTLY_URL);
}, { connection });

upsertJobScheduler is the current BullMQ API. On older versions, add the job with { repeat: { pattern: '0 2 * * *', tz: 'Etc/UTC' } } instead.

In both cases the report runs only after the work finishes without throwing, so a failed run sends nothing and Pingstack alerts you when the deadline passes. A failure to reach Pingstack is logged, not thrown, so it can’t make BullMQ retry a successful job.

Call the job’s function once, for example from a one-off script or a REPL with the same environment, and check that it reports. Open the check in Pingstack and confirm the location received a heartbeat and shows a next deadline.

Pass a count the job produces as a numeric value:

const sent = await sendNightlyReport();
await reportToPingstack(process.env.PINGSTACK_NIGHTLY_URL, sent);

Set warning and critical thresholds on the check, so a run that succeeds but sends nothing still raises an alert.

node-cron runs the schedule in every process that loads it. If you run several instances, each one runs the job and reports, which hides an instance that stopped. Run node-cron schedules in a single process, or move the job to BullMQ, where Redis makes sure each run happens once.

  • Process restarts. node-cron schedules live in memory. If the process is down at 02:00, that run never happens.
  • Busy workers. A BullMQ job waits behind other work. Lengthen the grace period, or give it its own queue.
  • Retries. A BullMQ job that succeeds on a later attempt reports late.
  • Timezone mismatch. Without a timezone option, both libraries use the server’s timezone.

If alerts still don’t match what you expect, see troubleshooting monitoring.