Monitor cron jobs with a dead man’s switch
A cron job that dies stops emitting errors - that’s the trap. Pulses alert you when a scheduled job goes quiet, and can run the scheduled HTTP call for you with stored history.
A cron job that crashes writes an error somewhere. A cron job that silently stops running writes nothing at all - and nothing is exactly what your error-based monitoring sees. The server rebooted and the crontab didn't survive. The container that ran the job got rescheduled away. Someone commented out one line during an incident. Every one of these failure modes is invisible to alerting that watches for errors, because a dead job doesn't err. It just stops.
The fix is old and good: a dead man's switch. Instead of listening for failure, demand proof of life - the job must check in after every successful run, and silence becomes the alert.
One command to arm it
An OtterKit pulse with --expect hands you a URL that something must ping within the window:
$ npx otterkit pulse --expect 15m
# → check in with: curl -X POST https://pulse-a1b2c3d4.otterkit.app/checkinSilence past the window sends the alert email; the next check-in sends the all-clear. Everything runs server-side - no daemon on your machine, nothing to deploy, nothing that can itself die quietly. Alerts fire on transitions only: one email when the job goes silent, one when it recovers, never a flood.
Wire it into the job
Append the check-in to the job with &&, so it only fires when the job actually succeeded - a backup that exits non-zero stays silent, and silence is what pages you:
# crontab: nightly backup, then proof of life - but only on success
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -X POST https://pulse-a1b2c3d4.otterkit.app/checkinCheck-ins are captured like webhook requests, body included. POST the job's summary and the switch doubles as a run log:
backup.sh && curl -X POST https://pulse-a1b2c3d4.otterkit.app/checkin \
-d "$(du -sh /backups/latest)"Now "did the backup run?" and "what did it report?" are the same question, answered in one place:
npx otterkit requests pulse-a1b2c3d4 # every check-in, newest firstThe other direction: let the pulse do the calling
Some "cron jobs" are just an HTTP call on a timer - hit the cleanup endpoint, trigger the digest, poke the queue. Flip the pulse around and it makes the call on an interval, storing every response as history:
# Call your cron endpoint every 10 minutes - responses stored as run history
npx otterkit pulse 10m https://api.your-app.com/cron
# POST with a body and headers, hourly
npx otterkit pulse 1h https://api.your-app.com/digest --method POST \
--data '{"kind":"hourly"}' -H 'Authorization: Bearer …'Intervals run from 30 seconds to a day. If runs start failing you get one email on the transition and one on recovery - same discipline as the switch. This replaces the micro-service whose entire job was "curl something on a schedule", along with its deploy pipeline and its own monitoring problem.
Schedules change; history shouldn't. --edit updates a running pulse in place - interval, URL, method, body - and the run history and billing window stay:
npx otterkit pulse --edit pulse-a1b2c3d4 5mWhat it costs
Pulses bill 1 credit ($0.01) per 100 runs - a run is a scheduled tick or a check-in - with the same cap as every OtterKit endpoint: never more than $3 per rolling 30 days. In practice: a nightly cron monitor costs about a penny a month; an every-10-minutes monitor about 44¢. Pulses default to --ttl never, because a monitor you have to remember to renew is its own dead-job problem.
pulse_create provisions either mode, and the run history reads back through the same tools as webhook captures - a durable scheduler an agent can give itself. Details in the pulses docs.