GUIDE · NODE.JS · FREE

Monitor Node.js scripts: one fetch call, zero dependencies

Node 18+ ships fetch built in — so the whole integration is: ping on success, POST the error to /fail on failure. Absence detection covers what your catch can't: the OOM kill, the crashed process, the cron entry that vanished. Free, no monitor cap.

The snippets

Create a monitor in the console (its schedule = how often the job should run, plus a grace window), copy the ping URL, and wrap your entry point.

ESM (top-level await)

// job.mjs — Node 18+ (built-in fetch, no dependencies) const PING = "https://ping.lastping.dev/<your-monitor-id>"; async function main() { // your job } try { await main(); await fetch(PING); } catch (err) { // report the failure; the body is stored with the ping (10 KB max) await fetch(`${PING}/fail`, { method: "POST", body: String(err && err.stack ? err.stack : err).slice(0, 10000), }); process.exitCode = 1; }

CommonJS

// job.js — CommonJS variant const PING = "https://ping.lastping.dev/<your-monitor-id>"; async function main() { // your job } main() .then(() => fetch(PING)) .catch((err) => fetch(`${PING}/fail`, { method: "POST", body: String(err).slice(0, 10000), }).finally(() => { process.exitCode = 1; }) );

Optional: overrun detection and run duration

A /start ping arms the hung-job alarm (no success inside the grace window → incident), and a shared rid pairs start with finish so each run's duration is recorded:

const PING = "https://ping.lastping.dev/<your-monitor-id>"; const rid = Date.now().toString(); await fetch(`${PING}/start?rid=${rid}`); try { await main(); await fetch(`${PING}?rid=${rid}`); } catch (err) { await fetch(`${PING}/fail`, { method: "POST", body: String(err).slice(0, 10000), }); process.exitCode = 1; }

Why process.exitCode = 1 matters

The non-zero exit keeps the failure visible to whatever runs the script — cron, systemd, a Kubernetes CronJob — so their retry semantics still apply. Using process.exitCode instead of process.exit() lets the fail ping finish sending first.


What this catches

The catch reports what it can; the silence reports the rest.

  • A thrown error — the /fail ping opens an incident immediately, with the stack trace stored on the ping.
  • A crash your code never sees — OOM kill, unhandled hard crash, kill -9. No success ping arrives; the grace window raises the alert.
  • The job never ran — missing cron entry, broken deploy, dead host. Absence → incident. This also covers in-process schedulers like node-cron: if the whole process dies, the scheduler dies with it, and only an external monitor notices.
  • A hung run — with the /start ping armed, a job stuck on a dead connection opens an incident inside the grace window.
  • Recovery — the next successful run closes the incident and sends a recovery notice automatically.

Questions people ask

Front-loaded answers — the most important fact first.

  • Do I need an npm package?

    No. Node 18 and later ship fetch built in, so the integration is zero-dependency. Any HTTP client works — the ping accepts GET, HEAD, or POST.

  • What if the process crashes before it can ping?

    A crashed or OOM-killed process sends no ping — and that missing ping is itself the alert. LastPing expects a signal on your schedule plus a grace window; silence opens an incident, so the failure path requires no working code.

  • Does this work with node-cron or BullMQ-style schedulers?

    Yes — wrap the scheduled task body in the same try/catch pattern. The external monitor also covers the failure the in-process scheduler can't see: the whole Node process dying and taking the scheduler with it.

  • Is this free?

    Yes. LastPing is free and fully hosted with no monitor cap and no paid tier. See how it compares to Healthchecks.io and Cronitor.

FREE · FULLY HOSTED · NO PAID TIER

A dead process can't reject a promise.

One fetch call now beats debugging an empty table later. First alert wired up in about a minute — and LastPing monitors itself the same way.