Cron job monitoring, the simple way
A cron job that fails loudly is easy — you see the error in your logs. A cron job that just stops running is much worse: no error, no crash, nothing in your dashboard. It just quietly stops happening.
Common ways scheduled jobs go quiet
- A Vercel Cron job is removed or misconfigured after a deploy, and nobody notices because there's no failure to see.
- A GitHub Actions scheduled workflow gets auto-disabled after 60 days of repo inactivity (GitHub does this silently).
- A Supabase Edge Function cron trigger is deleted during a migration.
- The job runs but exits early due to a bug, without raising an exception.
The pattern: a heartbeat check
This is sometimes called a "dead man's switch". Instead of monitoring whether your job exists, you monitor whether it recently ran. The job pings a unique URL after it finishes; if the ping doesn't arrive within your expected interval plus a grace period, you get alerted.
Vercel Cron (vercel.json)
{
"crons": [
{ "path": "/api/nightly-job", "schedule": "0 3 * * *" }
]
}
// at the end of /api/nightly-job, after success:
await fetch("https://silentwatch.app/api/ping/sw_your_token");GitHub Actions
name: nightly-job
on:
schedule:
- cron: "0 3 * * *"
jobs:
run:
runs-on: ubuntu-latest
steps:
- run: ./run-nightly-job.sh
- run: curl -fsS "https://silentwatch.app/api/ping/sw_your_token"Supabase Edge Function
Deno.serve(async () => {
// ... your job ...
await fetch("https://silentwatch.app/api/ping/sw_your_token");
return new Response("ok");
});Setting the right grace period
Set the grace period a little longer than your job's normal run-time variance. A daily job that usually takes 2 minutes but occasionally takes 20 should have at least a 30+ minute grace period, so a slow run doesn't trigger a false alert.