What is a deadbeat cron job?

A deadbeat cron job is a scheduled job that has quietly stopped doing its work - it no longer runs, or it runs and accomplishes nothing - while producing no error, no alert, and nothing in a log anyone reads. The schedule looks healthy. The work isn't happening.

It's a failure mode rather than a setting. Nothing in cron reports it, because from cron's point of view there is nothing to report: cron's job is to start a command at a time, and it did that, or it was never asked to. Whether the command achieved anything is outside its remit.

The damage is usually in the gap between when it broke and when someone noticed. A backup that stopped running in March is discovered in July, by someone trying to restore from it.

Why cron fails silently

Each of these is ordinary. Most production crontabs contain at least two.

The output goes nowhere

Classic cron mails a job's output to the owning user, but only if a mail transfer agent is installed and configured. Containers and modern cloud VMs usually have neither, so the output is generated and discarded. And the most common line in any crontab finishes the job itself:

0 * * * * /usr/local/bin/sync.sh >/dev/null 2>&1

That redirect was added to stop noise, and it works - it also throws away the only evidence that the job ever had anything to say.

Nothing checks the exit code

A non-zero exit means nothing to cron. The command failed, cron moved on, and it will start the same command again on the next tick, forever. There is no retry, no backoff, and no escalation, because there is nothing watching.

The environment isn't your shell

cron runs jobs with a deliberately minimal environment: a short PATH, no profile or rc file sourced, no virtualenv activated, none of the variables your terminal has. A script that works perfectly when you run it by hand can fail on its first scheduled tick with command not found - and, per the two points above, tell nobody.

A stale lock outlives the run that took it

Jobs that mustn't overlap are usually wrapped in flock or a hand-rolled PID file. If a run is killed hard enough - OOM killer, a SIGKILL, a host that disappeared - the lock can survive it. Every subsequent run then starts, finds the lock held, exits immediately, and looks like a clean, fast success. This one is particularly cruel: the job appears to be running normally, on time, indefinitely.

The machine moved on

The instance was replaced by an autoscaler. The VM was rebuilt and the crontab wasn't in the image. crond isn't running in the container because the entrypoint never started it. The disk filled. Nobody deleted the job; the thing that ran it simply isn't there anymore.

Clocks are worse than they look

A crontab interpreted in a local timezone has two ambiguous hours a year. Jobs scheduled in the window that daylight-saving time skips may not fire; jobs in the window it repeats may fire twice. Vixie cron has specific handling for this, which is itself a good sign that it's a real hazard. This is most of the argument for scheduling in UTC.

It runs, succeeds, and does nothing

The deepest version. The API token expired months ago; the script catches the exception, logs it somewhere nobody reads, and exits 0. The upstream file it syncs stopped being produced, so it faithfully copies zero rows every hour. Exit status 0 means "the process ended without complaint." It has never meant "the work was done."

Why you find out late

Every signal cron produces is an absence: no email, no log line, no row appearing downstream. Monitoring is built to notice things that happen - a request erroring, a queue growing, CPU spiking. Almost nothing is built to notice a thing that stopped happening, so the absence sits there until a human needs the output and finds it missing.

How to catch it

Roughly in order of how much they buy you per unit of effort.

Alert on the exit code

The cheapest real improvement. Wrap the job so a non-zero exit reaches a person, instead of being discarded by cron. This catches the loud failures - crashes, missing binaries, unhandled exceptions - and none of the quiet ones.

Use a dead man's switch

Invert the signal: the job pings a URL on success, and the monitor alerts when the ping doesn't arrive within the expected window. This is the only approach that catches "the job never ran at all," because it doesn't depend on the job being alive to report its own death. If you want this on its own, without changing where your jobs run, Healthchecks.io and Cronitor both do it well and work with any crontab.

Keep run history, not just the last state

"Is it running?" is the wrong question; "when did it last run, for how long, and what did it print?" is the useful one. A job whose duration quietly dropped from 40 seconds to 200 milliseconds is almost always the stale-lock case or the doing-nothing case, and you can only see that against its own history.

Assert on the work, not the process

The only defence against a job that exits 0 having done nothing is to make the job itself check. Fail loudly on zero rows written when zero is not a legitimate outcome. Exit non-zero when the token is rejected rather than logging and continuing. This is a change to your script, not to your scheduler, and no amount of external monitoring substitutes for it.

Where deadbeat fits

We named the product after the failure. deadbeat runs Python 3.12 scripts on a schedule in a sandboxed container, and the parts of the list above that are infrastructure problems stop being yours: there's no host to be replaced, no crond to not be running, no MTA to be missing, and no /dev/null in the path of your output. Every run keeps its exit code, duration, stdout and stderr, so the history is there to compare against. Schedules are UTC. A scheduled run that fails emails you on the transition into failure, rather than every tick.

It does not tell you why a run broke. There is no diagnosis step, AI or otherwise - you get the exit code and whatever your script printed, and reading them is your job. What it does not do either - and what nothing else can do for you - is decide whether your script's definition of success is honest. If it exits 0 while writing zero rows, deadbeat will report a healthy run, because that is what your script said happened. That check has to live in your code. Everything above the check, we'll handle.