Node.js

How to Monitor Cron Jobs in Node.js with Cronaman

March 12, 2026·7 min read

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:

emailDigest.js
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 fetch does 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:

emailDigest_axios.js
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:

scheduler.js
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:

emailDigest_robust.js
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:

ecosystem.config.cjs
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:

terminal
node emailDigest.js

Open 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:

Start 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