How to Know If a Cron Job Ran (or Failed)
Updated
Contents
How can you tell if a cron job ran? There are four methods, in increasing reliability: (1) check cron's log — journalctl -u cron on Debian and Ubuntu, journalctl -u crond on RHEL and Fedora — which shows that cron started 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 Cron's Log#
The fastest look. Cron logs each command it starts to syslog, and where that ends up depends on the cron implementation, the distribution, and whether a syslog daemon such as rsyslog writes text files:
| System | Cron and its log tag | Journal | Text file |
|---|---|---|---|
| Debian 12 and 13 | Debian cron, CRON |
journalctl -u cron |
Only if you install rsyslog: /var/log/syslog and /var/log/cron.log |
| Ubuntu 24.04 | Debian cron, CRON |
journalctl -u cron |
/var/log/syslog; rsyslog is installed by default, but its /var/log/cron.log rule ships commented out |
| RHEL 9 and 10, rebuilds | cronie, CROND |
journalctl -u crond |
/var/log/cron, and not /var/log/messages |
| Fedora | cronie, CROND |
journalctl -u crond |
None by default |
| Arch | cronie, CROND |
journalctl -u cronie |
Only if you add a syslog daemon |
| Alpine and BusyBox images | BusyBox crond, crond |
No journal | /var/log/messages from BusyBox syslogd, or crond's own file with -L; with -d 8, stderr instead |
Sources: Debian's rsyslog.conf and the Debian 12 release notes, which say rsyslog "is no longer installed by default"; Ubuntu 24.04's 50-default.conf, installed because ubuntu-minimal recommends rsyslog; CentOS Stream 10's rsyslog.conf and core package group, which includes rsyslog; Fedora's NoDefaultSyslog change in Fedora 20; and BusyBox 1.37.0's crond.c and syslogd.c.
# Debian and Ubuntu: everything the cron service logged, then only the job starts
journalctl -u cron --since today
journalctl -t CRON --since today | grep CMD
# RHEL and Fedora (-u cronie on Arch)
journalctl -u crond --since today
journalctl -t CROND --since today | grep CMD
# Text files written by rsyslog
sudo grep CRON /var/log/syslog # Debian and Ubuntu
sudo grep CMD /var/log/cron # RHEL-u shows everything from the cron service, including the daemon's own complaints about a crontab it refused to load. -t matches the tag that each job's process logs under (journalctl(1)), so it shows only job lines. A container rarely runs a syslog daemon, so BusyBox crond there needs -f -d 8 to put its lines in docker logs, as cron in Docker shows.
The journal may not reach back past the last reboot. With Storage=auto, systemd's default before version 259, journald keeps logs on disk only if /var/log/journal exists; otherwise it keeps them in /run/log/journal, which is lost at reboot (journald.conf(5)). systemd 259 made persistent the compiled-in default (NEWS), and a distribution's configuration can override either. journalctl --list-boots shows how many boots the journal holds. To keep logs across reboots under Storage=auto, create the directory as systemd-journald.service(8) describes, then move the current logs into it:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo journalctl --flushYou'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." Debian's cron logs only job starts by default; its -L option can also log job ends and jobs that exit with a non-zero status (cron(8)). cronie logs a CMDEND line when a job finishes, without its exit status (do_command.c), and BusyBox crond logs only the start. Use this to answer "did the schedule fire?" — not "did the job succeed?"
Method 2 — Redirect the Job's Output#
By default, cron emails a job's output, and on a server without a working mail setup it's discarded or buried in syslog (cron "No MTA installed" covers each implementation). 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. Cron job logging covers what each run should record and how to rotate the files.
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. Save it before anything else runs, then log it:
0 * * * * /opt/sync.sh; rc=$?; echo "$(date) exit $rc" >> /var/log/sync.logThe order matters. In the common one-liner job && echo OK || echo "FAIL $(date) exit $?", some shells, bash among them, run the $(date) substitution before reading $?, so every failure is logged as exit 0. And if the OK echo itself fails, the same line logs a failure for a job that succeeded.
Now the log records every run with its result. 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. This is a dead man's switch, and 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 |
|---|---|---|---|---|---|
| Cron's log | 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 implements method 4: report real job runs, configure the expected schedule and an alert destination, then verify receipt of the alert. A missed check-in can make the monitor critical with a timeout execution result; an explicit failure can also trigger an incident. Check the job, network, and monitoring capacity when investigating either result.
For Hangfire or Quartz.NET, use the current integration guide and read its published-version limitations. Other runtimes can use explicit HTTP reporting, which does not discover schedules automatically; the dead man's switch guide includes a shell wrapper that reports start, success, and failure without changing the job's exit code. To monitor an existing scheduled command, schedule that wrapper in its place; there is no CronRadar SDK or auto-discovery for plain cron, so each job reports with its own HTTP check-in. Whichever path you use, verify a real run and the alert destination before relying on coverage.
Frequently Asked Questions#
Does cron email me its output?#
Cron mails a job's output to the crontab's owner, or to the MAILTO address if one is set, but only when the server can send mail. Many servers can't; cronie then writes the output to syslog instead (cron(8)), and Debian's and Ubuntu's cron log No MTA installed, discarding output and delete it (cron "No MTA installed"). Redirect output to a file rather than relying on cron email.
Where are cron logs stored?#
Cron logs the commands it starts to the system log. Read them with journalctl -u cron on Debian and Ubuntu or journalctl -u crond on RHEL and Fedora. With rsyslog installed, they're also in /var/log/syslog on Debian and Ubuntu and in /var/log/cron on RHEL (RHEL); Alpine's BusyBox crond logs to syslog, which BusyBox syslogd writes to /var/log/messages. 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. Some failures still exit 0; preventing errors in scheduled automation covers how to check a run's result.
Can I tell if a cron job is still running?#
On the host, ps -ef | grep for the command shows it. With lifecycle tracking (a start ping plus a complete ping), the monitor also knows the run began and hasn't finished. CronRadar flags it as stuck once the job's next scheduled time plus its grace period passes with that start still the last event; a new start from the next run resets that window, so give the job its own timeout to turn a hang into a reported failure.
Why didn't my cron job run at all?#
Check whether the cron service is running, which user's crontab holds the job, and whether the job's environment differs from your terminal's. Cron job not running walks through the common causes.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.