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.
2. Store the ping URL in the environment
Section titled “2. Store the ping URL in the environment”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.
3. Report after the job succeeds
Section titled “3. Report after the job succeeds”Add a small helper:
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}`); }}node-cron
Section titled “node-cron”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' });BullMQ
Section titled “BullMQ”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.
4. Confirm the first report
Section titled “4. Confirm the first report”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.
Optional: report a number
Section titled “Optional: report a number”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.
Several app instances
Section titled “Several app instances”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.
Why a healthy job might still alert
Section titled “Why a healthy job might still alert”- 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.