← Blog  ·  October 2026

Cron Job Dead Man's Switch: How to Alert When Jobs Stop Running

Silent failures are the worst kind. Here's how to catch them before your users do.

Friday afternoon: your invoice email job runs at 11 PM and has run every Friday for two years without incident. Thursday night it silently stops — a botched deploy removed an environment variable, and the scheduler skipped it without logging anything. Monday morning a customer emails asking where their invoice is. That's when you find out.

Error monitoring wouldn't have caught this. There was no error. There was no crash. No stack trace, no exit code, no log entry. The job simply stopped running, and the absence of output looked exactly like normal silence.

This is the problem a dead man's switch solves.


What is a dead man's switch for cron jobs?

The concept comes from safety engineering: a control that requires a human operator to actively confirm they are present and functioning. If the operator stops confirming — falls unconscious, steps away — the switch triggers a safety action. No signal = something is wrong.

Applied to cron jobs, the model flips how monitoring works. Instead of an external observer trying to watch the job from the outside (which requires knowing the schedule and just checking whether a process started), the job itself pings an endpoint at the end of each successful run. If the ping stops arriving within the expected window, you get alerted.

This is more reliable than external scheduling monitors for one critical reason: it knows the job actually ran to completion, not just that it started. A job that starts and crashes 30 seconds in looks identical to a healthy job from the outside. From the inside — from the heartbeat — it's obvious: the ping never came.

The inversion that matters: External monitors ask "did the job start?" A dead man's switch asks "did the job tell me it finished?" Those are very different questions with very different failure modes.

The three failure modes a dead man's switch catches that standard monitoring misses:


DIY implementation — 3 approaches

1. curl one-liner (simplest)

The smallest possible implementation: add a single line at the end of your cron script. The heartbeat only fires if the script reaches that line — meaning everything above it succeeded.

# At the end of your cron script
curl -fsS "https://your-monitoring-service.com/heartbeat/TOKEN" > /dev/null 2>&1

In /etc/cron.d/myjob, chain it with && so the heartbeat only fires on a clean exit:

0 3 * * * deploy /usr/bin/python /path/to/sync.py && curl -fsS "https://hb.example.com/ping/TOKEN" > /dev/null

The -fsS flags make curl fail silently on HTTP errors without producing output that would trigger a local mail notification. The redirect to /dev/null suppresses any remaining output. This pattern adds roughly 50ms to your job's runtime and zero additional dependencies.

The key insight: the absence of the ping IS the alert. You don't need to handle the error case — just don't ping, and the monitoring service handles the rest.

2. Python wrapper (with error handling)

For longer jobs or ones you want to wrap with retry logic, a Python helper function keeps the heartbeat pattern clean:

import subprocess
import requests

def run_with_heartbeat(cmd, heartbeat_url, timeout=300):
    try:
        result = subprocess.run(cmd, timeout=timeout, check=True)
        requests.get(heartbeat_url, timeout=5)  # ping on success only
    except Exception as e:
        # Don't ping — the absence of the ping IS the alert
        raise

Usage is straightforward:

run_with_heartbeat(
    ["python", "/path/to/sync.py"],
    heartbeat_url=os.environ["CRONWATCH_HEARTBEAT_URL"]
)

The check=True argument makes subprocess.run raise a CalledProcessError on non-zero exit codes, which the except block catches and re-raises without pinging. timeout=5 on the heartbeat request itself prevents a hung monitoring service from blocking your job indefinitely.

3. GitHub Actions (for scheduled workflows)

GitHub Actions workflows support a schedule: trigger using cron syntax. The if: success() condition on the heartbeat step ensures the ping only fires when all previous steps passed:

- name: Run job
  run: python sync.py

- name: Ping heartbeat
  if: success()
  run: curl -fsS "${{ secrets.HEARTBEAT_URL }}"

GitHub Actions has its own silent failure mode worth knowing: scheduled workflows stop running after 60 days of repo inactivity with no notification. A dead man's switch catches this case automatically — if GitHub stops triggering the workflow, the heartbeat stops arriving.


Building your own vs. using a service

You could build the server side yourself: a small web service that accepts GET requests and triggers an email after a configurable timeout. It's a weekend project. But it comes with operational cost you might not immediately account for:

DIY CronWatch
Setup time 30–60 min (initial), ongoing maintenance 2 minutes (one curl line)
Grace period config Manual code change UI slider per monitor
Alert channels Email (custom code required) Email, Slack
Dashboard None Visual timeline per job
Multi-job management More config per job One dashboard, all jobs
Uptime of monitor itself Your problem Managed

For a single job at a small company, the DIY path is reasonable. For five or more jobs at a company where missed runs have business consequences, the operational overhead of running your own monitoring infrastructure starts to outweigh the cost of a service.


Implementation checklist

Regardless of approach, a complete dead man's switch setup should include:

  1. Heartbeat at the end of the job only. Not at the start. The ping confirms completion, not initiation.
  2. Grace period set to interval + 25–50%. For a nightly job, 26 hours. For an hourly job, 75–90 minutes.
  3. Alert goes somewhere you actually check. Email to a shared ops inbox, Slack to a channel with real humans, or PagerDuty if it's critical enough.
  4. The heartbeat call itself is wrapped in error handling. A failed HTTP request to your monitoring service should not cause your job to report failure to the scheduler.
  5. You test it. Comment out the curl line, trigger a manual run, confirm the alert fires. A monitoring setup you haven't verified is not a monitoring setup.

Start monitoring your cron jobs in 2 minutes

CronWatch — heartbeat monitoring for scheduled jobs

Add one curl line to your cron job. We handle the grace periods, the alerts, and the dashboard. Works with any language, any scheduler: crontab, GitHub Actions, Kubernetes CronJob, Heroku Scheduler, Lambda, Celery Beat, and more.

Free to try. No credit card required.

Join the CronWatch waitlist Free cron expression parser →

Further reading