Skip to main content

Blog · Spoke

Your Scheduled Job Says It Ran. It Didn't Do Anything. Here's the Difference.

A scheduled job that runs and accomplishes nothing is more dangerous than one that crashes. Here is why "success" lies, and how a heartbeat window catches it.

By Dima K. Published

Your Scheduled Job Says It Ran. It Didn’t Do Anything. Here’s the Difference.

A scheduled job that runs every hour and does nothing is more dangerous than one that crashes. Because the crash, at least, is honest.

The crash throws an error. It turns something red. It interrupts your day with a message you cannot ignore, and you go fix it, annoyed but informed. The silent one does the opposite. It runs on time, finishes clean, reports success, and produces nothing, and it does that every hour for as long as you let it, wearing the exact face of a job that is working perfectly. You will not go fix it, because nothing told you to. You will find out the slow way, the expensive way, the day you finally reach for the thing it was supposed to have been making all along.

Someone put the mechanism in one line.

“Cron was designed to run commands on a schedule. That’s it. It was never designed to tell you when those commands fail to run.”

Cronping, 2025

Run the command, move on. That is the whole job. Whether the command did anything useful is, as far as the scheduler is concerned, none of its business.

Ran is not the same as accomplished

Here is the part that trips everyone, and it is worth saying slowly. There are two different facts about every scheduled job, and they live in two different columns.

One: did the job run. Two: did the job accomplish its purpose. We treat these as the same thing. They are not even close. A backup script can run, find the source folder is empty because a path changed last Tuesday, dutifully back up nothing, and exit with a clean success. The scheduler sees a command that ran and finished without complaint, and records a flawless green. The fact that the backup contains zero bytes of your actual data lives in a column the scheduler never looks at.

A daily report job can run, query the database, get back zero rows because a filter quietly broke, and send a beautifully formatted email with nothing in it. Ran: yes. Accomplished: no. Two columns. Only one of them is being watched, and it is the wrong one.

“The job that appears healthy because it produces no output at all.”

Cronping, 2025

Appears healthy. That is the trap in three words. Health and output are not the same measurement, and the scheduler only measures the one that does not matter.

What is actually happening under the hood

Walk through the mechanism, because once you see it you cannot unsee it.

A scheduler fires your command at the appointed time. The command is a script. The script runs top to bottom and then it ends. When it ends, it hands back a single number, an exit code: zero for “I finished fine,” anything else for “something went wrong.” That number is the entire conversation between your job and the thing watching it. One number.

Now here is the problem. A script can finish fine and accomplish nothing. The loop it was supposed to run had nothing to loop over, so it ran zero times and ended cleanly. The query returned no rows, which is not an error, so the script processed no rows and ended cleanly. The folder it was supposed to read was empty, which is not an error, so it read nothing and ended cleanly. In every one of these the exit code is zero. The scheduler hears “I finished fine” and writes down success, and it is, narrowly, telling the truth. The command did finish fine. It just did not do the thing.

“Jobs that exit with code 0 after doing nothing useful.”

Cronping, 2025

Exit zero, did nothing useful. The scheduler cannot tell the difference between that and a job that moved a thousand records, because it never asked about the records. It asked about the exit code. And the exit code lied by telling a true thing about the wrong question. This is the same shape as a workflow that logs hundreds of clean runs while moving zero data, which is its own kind of silent failure covered in why your n8n workflow is silently failing.

A bad alert versus a good one

When a scheduled report finally gets noticed, the message you usually get, if you get one at all, looks like this:

cron: job report_daily completed. exit 0.

That is not an alert. That is the lie restated. It tells you the command ran and exited zero, which is exactly the information that hid the problem. The alert you actually want says something a person can act on:

Your daily sales report ran on time but sent 0 rows.
It has sent an empty report for 3 days. Last report with
real data: Monday, 412 rows.

That second one is checking the result, not the exit code. Sent zero rows. Three days. Last good one had 412. Now you know there is a problem, you know roughly when it started, and you know what “working” looked like. The difference between those two messages is the difference between finding out today and finding out the day a client asks where their numbers went.

Watch for absence, not for errors

So the fix is not a better error alert. You cannot alert on an error that never happens. The fix is to stop watching for failure and start watching for the missing success.

This is an old idea with a grim old name: the dead man’s switch. You flip the question around. Instead of waiting to hear “something broke,” you expect to hear “something worked,” on a schedule, and you raise the alarm when the expected signal does not arrive.

“It’s called a dead man’s switch because the alert triggers on absence of activity, not presence.”

OnlineOrNot, 2025

A job that runs every hour should check in every hour. Not “should throw an error if it fails,” because the dangerous failures throw no error. Should check in. A heartbeat. And the watcher’s job is to notice when the heartbeat is late, or missing, or arrives carrying no real output, and to tell you in plain words that the thing went quiet. The silence is the signal. But silence only works as a signal if something is sitting there expecting the sound and noticing when it does not come. This is the whole answer to the question of how to actually know if a scheduled workflow is running: not by checking, but by being told when the expected run goes missing.

That expecting-and-noticing is the part you cannot bolt onto the job itself, because a job that has stopped working cannot raise its own hand. It has to come from outside. This is the same reasoning behind watching a website from the outside instead of trusting the page you have already loaded, walked through in how to know when your site goes down without touching code. NoCrash watches for the heartbeat of your scheduled work and messages you the moment an expected run goes missing or comes back empty, in plain language, so you find out the same day it goes quiet instead of the day you finally need what it was supposed to be making. Connect your first watch free at nocrash.io.

A crash costs you an annoyed afternoon. A silent no-op costs you the backup you reach for and cannot find. You only get to pick which kind of failure you are set up to notice.

— NoCrash

Common questions

Frequently asked

What does it mean when a scheduled job ran but did nothing?
It means the job started, finished, and reported success, but the work it was supposed to do never happened. The backup ran and backed up an empty folder. The report job ran and sent zero rows. The export ran and wrote a file with nothing in it. The schedule fired, the command exited cleanly, and the result is missing. Running and accomplishing are two separate facts.
Why does a successful job lie?
Because most schedulers only check whether the command exited without an error code, not whether it produced the right result. A script can finish cleanly after doing nothing, an empty loop, a skipped step, a query that returned nothing, and the scheduler records a perfect success. The status reflects that the command ran, not that the work landed.
Why is a silent no-op worse than a crash?
A crash is honest. It throws an error, you see red, you fix it. A silent no-op shows green while the damage accumulates: weeks of missing backups, days of unsent reports, a slowly emptying pipeline. You discover it the worst possible way, when you finally need the thing that was never being produced.
What is a heartbeat window and how does it catch this?
A heartbeat window flips the question. Instead of waiting for an error, you expect a signal on a schedule, and you alert when the expected signal does not arrive. A job that runs every hour should check in every hour. If the check-in is late or missing, or carries no real output, something is wrong, even though no error was ever thrown. You watch for absence, not for failure.
How do I set this up without rebuilding everything?
You do not rebuild the job. You add an outside watcher that expects the job's check-in on a schedule and tells you when it is late or empty. NoCrash watches for the absence of an expected run and sends a plain-language message when the heartbeat does not arrive, so you find out the job went quiet the same day, not the day you finally need its output.

Stop finding out from your customers.

One morning message telling you what ran clean and what didn’t. Free forever on 3 things to watch.