Cron jobs are the invisible infrastructure of most applications. They run reports, clean up old data, sync records, send scheduled emails, process queues. They usually work fine. When they stop working, they often do it in the least observable way possible: they just stop running. No error, no alert, no indication that anything is wrong.
This article explains why cron job failures are so hard to detect, what failure looks like across common platforms, and how to add the minimum monitoring necessary to catch it before your users do.
The core problem is that cron is a fire-and-forget system. The scheduler dispatches a job and records nothing about whether it succeeded. If your script exits with a non-zero code, cron might send an email to the local system user — but nobody's reading that. If the process crashes silently, or the underlying service it depends on is unavailable, cron just moves on to the next scheduled run.
This means the default state of a failed cron job is: the job stops producing output, and nothing tells you.
Three specific scenarios that cause silent failures:
Your job connects to a database, an external API, or a file path. The connection string changes, the API key rotates, the path moves. The job can't connect, throws an exception, exits with code 1. In crontab, that exception goes to /dev/null unless you've specifically redirected output to a log file. The next run, same thing. Nobody knows.
A data processing job that used to run in 2 minutes now takes 35 minutes because the dataset grew. The server or orchestration system kills it after a timeout. The job doesn't complete. The output from a partial run looks plausible until someone checks timestamps and realizes the last full run was three weeks ago.
On managed platforms, the scheduler process can crash, get OOMed, or fail to restart after a deployment. All the jobs that should have run in the gap simply don't run. There's no backfill, no catch-up, no alert — just a missing time window in your data.
Linux crontab sends output to the local mail spool by default. On most production servers, nobody reads the mail spool. The way to get visible output is to explicitly redirect:
# Redirects both stdout and stderr to a log file
0 3 * * * /usr/bin/python3 /opt/jobs/sync.py >> /var/log/nightly-sync.log 2>&1
But even with log files, you have to go looking. You need to check the log, notice the missing entries, and realize the gap is meaningful. There's no push notification. The job doesn't call you when it fails.
GitHub Actions cron jobs are more observable — failed workflows turn the workflow icon red and appear in the Actions tab. But: GitHub only emails the repository owner if a workflow fails, not if it simply stops running. If the cron expression stops triggering (which can happen if a branch is deleted or the repository goes dormant), you get nothing. And many teams mute GitHub notification emails entirely.
on:
schedule:
- cron: '0 3 * * *' # runs nightly
jobs:
sync:
runs-on: ubuntu-latest
steps:
- run: python sync.py
# if this silently succeeds but produces wrong output, GitHub won't tell you
Scheduled Lambda functions invoked via EventBridge can fail silently when the Lambda returns successfully (exit code 0) but the business logic inside throws a caught exception. CloudWatch Logs receives the invocation logs, but nobody is watching CloudWatch Logs unless you've set up a metric filter and an alarm. Most teams haven't.
The most effective pattern for detecting missed cron jobs is the dead man's switch (also called a heartbeat ping). The idea: your cron job sends an HTTP request to a monitoring service at the end of each successful run. If the monitoring service doesn't receive the ping within an expected window, it fires an alert.
Implementation takes one line at the end of your job:
# crontab
0 3 * * * /opt/jobs/sync.py && curl -s https://cron.watch/ping/your-job-id
# GitHub Actions
- name: Run sync
run: python sync.py
- name: Notify CronWatch
if: success()
run: curl -s https://cron.watch/ping/your-job-id
The && means the ping only fires if the job exits successfully. If the job fails, exits with non-zero, or never runs — the monitoring service notices the missing ping and alerts you.
For jobs with complex output, a second approach is log-based: check the timestamp of the most recent log entry against the expected schedule. If the last successful run was more than 2× the schedule interval ago, alert. This requires infrastructure (a checker job, log access) but catches subtle failures where the job "runs" but produces stale or incorrect output.
The fastest thing you can do today, in order of effort:
>> /var/log/job.log 2>&1 on every crontab entry. Takes 10 seconds. Gives you something to look at when investigating.CronWatch monitors your scheduled jobs across every platform — crontab, GitHub Actions, Heroku Scheduler, AWS EventBridge, Kubernetes CronJob. Get alerted in Slack or email within minutes of a missed window or error.
Join the CronWatch waitlist → Parse a cron expression