Scheduled jobs are the silent backbone of most production systems: database backups, report generation, cache warming, cleanup scripts, billing runs, queue processors. When they work, nobody thinks about them. When they stop working, the failure is invisible — there is no exception to catch, no HTTP 500 to log, no user complaint to triage. A cron job heartbeat monitor detects that silence and turns it into an alert.
Most application monitoring tools are reactive. They watch for errors your code raises, HTTP status codes your server returns, or thresholds your metrics breach. A cron job that stops running raises none of these signals — it simply produces no output and generates no activity.
By default, cron sends failed job output to the system mail spool of the user running the job. On most production servers, that mail spool goes unread indefinitely. Even when output is redirected to a log file, the log being empty is not distinguishable from the log being correct — a job that ran successfully also produces no output in many cases.
Cron only tracks whether a process was spawned and what exit code it returned. If your backup script exits 0 but produces a zero-byte archive, cron considers the job successful. If your report generation script exits 0 but fails to write the output because a database query timed out and the error was swallowed, cron considers the job successful.
Crontab files disappear during server reimaging. Package upgrades can overwrite systemd timer units. Configuration management tools can reset cron directories. A user-level crontab that worked for 18 months can silently vanish during a routine update, and the absence of a job running is indistinguishable from a job that ran but produced no output.
Heartbeat monitoring inverts the detection model. Instead of your monitoring system polling your job to check if it ran, your job pings your monitoring system after it runs. If no ping arrives within the expected window, the monitoring system fires an alert.
The implementation is minimal. At the end of your cron job, add a single curl or wget call to a unique URL assigned to that job. The monitoring service records the timestamp. If the URL isn't pinged within the expected interval — the job's period plus a grace window — you receive an alert.
This approach catches four distinct failure modes in one mechanism:
The simplest implementation: append a curl call to your existing cron job. The && operator ensures the ping only fires on success:
# In crontab: run backup every day at 2am, ping on success
0 2 * * * /opt/scripts/backup.sh && curl -fsS --retry 3 https://cronwatch.example/ping/YOUR-JOB-ID
The -fsS flags make curl fail silently on HTTP errors (-f), suppress progress output (-s), but still show errors (-S). The --retry 3 handles transient network issues without blocking your job.
For a job script you own, call the ping from inside the script after verifying the result — not merely that the process exited 0:
#!/bin/bash
set -euo pipefail
# Your actual job logic
pg_dump mydb | gzip > /backups/mydb-$(date +%Y%m%d).sql.gz
# Verify the output exists and is non-empty before pinging
BACKUP_FILE="/backups/mydb-$(date +%Y%m%d).sql.gz"
if [[ -f "$BACKUP_FILE" && -s "$BACKUP_FILE" ]]; then
curl -fsS --retry 3 "https://cronwatch.example/ping/YOUR-JOB-ID?bytes=$(stat -c%s "$BACKUP_FILE")"
else
echo "Backup file missing or empty — not pinging heartbeat" >&2
exit 1
fi
For jobs managed as systemd services, use the OnSuccess directive to fire a ping unit only when the service exits cleanly:
[Unit]
Description=Daily database backup
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
OnSuccess=backup-heartbeat.service
# backup-heartbeat.service
[Unit]
Description=Heartbeat ping for daily backup
[Service]
Type=oneshot
ExecStart=curl -fsS https://cronwatch.example/ping/YOUR-JOB-ID
The grace window is how long after the expected schedule the monitoring service waits before alerting. A 10-minute job scheduled hourly might use a 15-minute grace window. A 2-hour batch job running nightly might use a 30-minute grace window. Too tight a grace window produces false positives; too loose and you find out about failures hours late. Start with 10–20% of the job's typical duration and adjust based on observed variance.
You can build a basic heartbeat receiver yourself — a web endpoint that records pings, a background process that checks for missed pings, a script that sends an email. Developers do this regularly. The problems emerge at scale and over time:
A purpose-built cron job heartbeat monitor runs independently of your infrastructure, stores job history, handles alert routing, and gives you a dashboard to see all your scheduled jobs in one place. You focus on what the jobs do; the monitor handles whether they ran.
To validate your cron expression before wiring up monitoring, use the free CronWatch expression parser — paste any cron schedule and it shows you the next 10 run times in plain English, making it easy to verify your schedule is correct before you depend on it.
Cron jobs fail silently, and standard monitoring tools are blind to silence. Heartbeat monitoring solves this by having each job ping a URL on completion — if the expected ping doesn't arrive within the schedule window plus a grace period, you get an alert. The implementation is a single curl call appended to your existing job. For production reliability, a purpose-built cron job heartbeat monitor runs outside your infrastructure and handles the alert routing and history that a homegrown solution requires you to build and maintain yourself.
CronWatch monitors your scheduled jobs with heartbeat pings and alerts you the moment a job stops running. No agents to install — just add a curl call to your existing jobs.
Join the waitlist at https://lior-seats.github.io/cronwatch-waitlist/
Join the CronWatch Waitlist Parse Your Cron Expression (Free)© 2026 Developer Tools Blog