How to Know If a Cron Job Ran (or Failed)
How can you tell if a cron job ran? There are four methods, in increasing reliability: (1) check the system log — grep CRON /var/log/syslog shows that cron invoked the command, but not whether it succeeded; (2) redirect the job's own output to a log file and inspect it; (3) capture the exit code ($?) to distinguish success from failure; and (4) use external heartbeat monitoring, which is the only method that tells you when a job didn't run at all. Methods 1–3 require the job to have executed; only method 4 catches a job that silently stopped.
You added a line to your crontab, saved it, and walked away. A day later, the obvious question: did it actually run? Cron is famously quiet — it won't tell you, so you have to ask it. Here are the four ways, from "quick but shallow" to "reliable enough to trust in production," and exactly what each one can and can't prove.
Method 1 — Check the System Log
The fastest look. On Debian/Ubuntu, cron records each invocation to syslog:
grep CRON /var/log/syslog | tail
On systemd distros (RHEL, CentOS, Fedora, most cloud images):
journalctl -u crond --since "today"
You'll see a line every time cron fired the command. That's the ceiling of what this proves: cron launched the process. It says nothing about whether your script did its work or crashed one line in. A job that exits immediately with an error still appears here as a clean "ran." Use this to answer "did the schedule fire?" — not "did the job succeed?"
Method 2 — Redirect the Job's Output
By default, a cron job's output goes nowhere useful. Capture it by appending a redirect to the crontab line:
30 2 * * * /opt/backup.sh >> /var/log/backup.log 2>&1
>> appends, and 2>&1 folds stderr into the same file so errors land next to normal output. Add a timestamp inside the script (echo "run at $(date)") and you can read exactly what happened on each run. The limitation is human: you have to remember to open the file — and a job that never ran writes nothing, so an empty log is ambiguous.
Method 3 — Capture the Exit Code
Methods 1 and 2 blur "ran and succeeded" with "ran and failed." Your script's exit code is what separates them — 0 means success, anything else means failure. Record it yourself:
0 * * * * /opt/sync.sh && echo "OK $(date)" >> /var/log/sync.log || echo "FAIL $(date) exit $?" >> /var/log/sync.log
Now the log distinguishes the two cases explicitly. This is the most you can learn from inside the machine: you know it ran, and you know whether it worked. There's still one thing you can't see from here.
Method 4 — External Heartbeat Monitoring
Every method above shares one blind spot: they only produce evidence when the job runs. If the job is skipped entirely — the daemon stopped, a deploy overwrote the crontab, the server rebooted and cron didn't come back — there is nothing to grep, because nothing executed. An empty log looks identical to a job that ran and printed nothing.
Heartbeat monitoring inverts the logic. Instead of waiting for an error, it waits for a success signal and alerts on its absence. Your job pings an external service when it finishes; the service knows the schedule and raises an alert when the expected ping doesn't arrive within a grace window. It's the only one of the four that answers "my nightly backup silently stopped three weeks ago" — before you discover it the hard way.
Which Method to Use
They're complementary, not competing. Combine method 3 (exit-code logging, for local detail when you investigate) with method 4 (heartbeat monitoring, to catch missed runs and actually notify you). Together they cover both "what happened" and "did anything happen at all."
| Method | Confirms it ran | Confirms success | Catches a missed run | Alerts you | Effort |
|---|---|---|---|---|---|
| syslog / journalctl | Yes | No | No | No | None |
| Output redirect | Yes | Partial | No | No | Low |
| Exit code | Yes | Yes | No | No | Low |
| Heartbeat monitor | Yes | Yes | Yes | Yes | Low (one ping) |
Getting Alerted, Not Just Logged
cronradar.com">CronRadar implements method 4: your job sends a completion ping, you declare the schedule, and a missed or failed run turns the monitor critical and notifies you. A timeout is a named state — the monitor's execution result becomes timeout with "No ping received for X minutes," so a silent miss is as visible as an explicit failure. If your jobs run inside a framework scheduler (Laravel, Hangfire, Celery, Quartz, node-cron), the SDK auto-discovers them, so you don't wire pings by hand.
Frequently Asked Questions
Does cron email me its output?
Only if MAILTO is set and a mail transfer agent is configured. On most cloud servers neither is, so the output is discarded. Don't rely on cron email.
Where are cron logs stored?
Invocation is logged to /var/log/syslog (grep CRON) or journalctl -u crond. Your script's own output is not logged unless you redirect it.
How do I get alerted when a cron job fails?
Combine exit-code logging with external heartbeat monitoring — the exit code catches failures, and the heartbeat catches jobs that never ran.
Can I tell if a cron job is still running?
With lifecycle tracking (a start ping plus a complete ping), yes — a job that started but never completed shows as hung.