How to Monitor Cron Jobs in Node.js with Cronaman
The silent failure problem in Node.js
Node.js scheduled tasks — whether you use node-cron, setInterval, or a custom scheduler — are silent by default. When your job function throws, node-cron logs to stderr (if you're watching it). No email. No Slack. No way to know unless you grep the logs at exactly the right moment.
It gets worse: if your Node process restarts — a deploy, a crash, a server reboot — your scheduled tasks stop running entirely. No alert fires. Your data pipeline, email digests, or cleanup jobs just quietly stop, and you find out when a user complains.
How heartbeat monitoring works
Heartbeat monitoring flips the model. Instead of waiting for an error signal (which may never come), you configure an expected interval in Cronaman — say, every hour — and your job sends an HTTP ping at the end of every successful run. If Cronaman doesn't receive that ping on schedule, it alerts you. Missed run, crashed process, bad deploy: all caught.
No npm package to install. No agent on the server. One fetch call at the end of your job function.
Create a Cronaman monitor
First, create the monitor:
- Sign up at cronaman.dev — free plan, no credit card required
- Click New Monitor
- Set the name (e.g., "Email digest") and interval to match your job schedule
- Copy your ping URL:
https://cronaman.dev/ping/node-emailer
Pinging with native fetch (Node 18+)
Node.js 18 ships with the fetch API built in — no extra dependencies needed. Here's a standalone job script:
const PING_URL = "https://cronaman.dev/ping/node-emailer";
async function sendEmailDigest() {
// Your job logic here
console.log("Sending daily digest emails...");
// ... fetch recipients, render templates, call Resend/SES ...
}
(async () => {
try {
await sendEmailDigest();
const res = await fetch(PING_URL, {
signal: AbortSignal.timeout(10000),
});
if (!res.ok) console.warn("Ping returned:", res.status);
} catch (err) {
console.error("Job failed:", err);
process.exit(1);
}
})();A few important details:
- Use
AbortSignal.timeout(10000)instead of a raw timeout — it's the idiomatic way to set a deadline on fetch in Node 18+ - Native
fetchdoes not throw on 4xx/5xx — log the status code manually so you catch configuration errors early - Call the ping after the job logic succeeds — this way it's only a success signal
Using Axios
If your project already uses Axios — the same library Cronaman's own frontend uses for API calls — the ping is straightforward:
const axios = require("axios");
const PING_URL = "https://cronaman.dev/ping/node-emailer";
async function sendEmailDigest() {
console.log("Sending daily digest emails...");
// ... your job logic here ...
}
(async () => {
try {
await sendEmailDigest();
await axios.get(PING_URL, { timeout: 10000 });
} catch (err) {
console.error("Job failed:", err);
process.exit(1);
}
})();Unlike native fetch, Axios throws on non-2xx responses by default — so you don't need to check the status manually. The timeout option here is in milliseconds.
Integrating with node-cron
If you use node-cron to schedule jobs within a long-running Node process, add the ping inside the scheduled callback:
const cron = require("node-cron");
const axios = require("axios");
const PING_URL = "https://cronaman.dev/ping/node-emailer";
const FAIL_URL = "https://cronaman.dev/ping/node-emailer/fail";
cron.schedule("0 8 * * *", async () => {
try {
// Your job logic here
console.log("Running email digest...");
// ... send emails ...
await axios.get(PING_URL, { timeout: 10000 });
} catch (err) {
console.error("Job failed:", err);
await axios.get(FAIL_URL, { timeout: 10000 }).catch(() => {});
}
});The .catch(() => {}) on the fail ping prevents a network error from surfacing as an unhandled rejection. You don't want a temporary Cronaman outage to crash your scheduler process.
Success vs. failure pings
Appending /fail to your ping URL tells Cronaman to mark the run as failed immediately — no waiting for the grace period to expire. This is the recommended pattern for jobs where you handle errors explicitly:
const PING_URL = "https://cronaman.dev/ping/node-emailer";
const FAIL_URL = "https://cronaman.dev/ping/node-emailer/fail";
async function sendEmailDigest() {
console.log("Sending emails...");
// ... your job logic here ...
}
(async () => {
try {
await sendEmailDigest();
await fetch(PING_URL, { signal: AbortSignal.timeout(10000) });
} catch (err) {
console.error("Job failed:", err);
await fetch(FAIL_URL, { signal: AbortSignal.timeout(10000) }).catch(() => {});
process.exit(1);
}
})();Running with PM2
PM2's cron restart mode is a common way to run scheduled Node.js scripts in production. Add a cron_restart field to your ecosystem file:
module.exports = {
apps: [{
name: "email-digest",
script: "./emailDigest.js",
cron_restart: "0 8 * * *",
autorestart: false,
}],
};Set autorestart: false so PM2 doesn't restart the script on exit (it's a one-shot job, not a daemon). In Cronaman, match the monitor interval to cron_restart and add a grace period for startup time.
Verify your setup
Run the script manually to confirm the ping fires:
node emailDigest.jsOpen the Cronaman dashboard. Within a few seconds the monitor should turn "healthy" with a "Last ping" timestamp. If it stays grey, verify outbound HTTPS access to cronaman.dev from your server and that the ping URL matches exactly.
From here, any silent failure — crashed process, failed deploy, killed container — shows up on your dashboard and in your inbox before your users notice.
More cron monitoring guides
Using a different language? The same pattern works everywhere:
- How to Monitor Cron Jobs in Python — covers urllib, requests, and crontab
- How to Monitor Cron Jobs in PHP — covers file_get_contents, cURL, and Laravel schedulers
More guides
What Is Cron Job Monitoring? (And Why You Need It)
5 min readProBeyond Timing: Catch Silent Successes with Semantic Ping Payloads
5 min readPythonHow to Monitor Cron Jobs in Python with Cronaman
6 min readPHPHow to Monitor Cron Jobs in PHP with Cronaman
6 min readBashHow to Monitor Cron Jobs in Bash with Cronaman
5 min readRubyHow to Monitor Cron Jobs in Ruby with Cronaman
6 min readGoHow to Monitor Cron Jobs in Go with Cronaman
6 min readLaravelHow to Monitor Laravel Cron Jobs with Cronaman
6 min readStart monitoring your Node.js cron jobs
Free forever for up to 3 monitors. No credit card required. Set up in under 2 minutes.
Start monitoring free