# How to Know If a Cron Job Ran (or Failed)

By Dan Ochoa · Published 2026-07-09 · Updated 2026-10-24
Canonical: https://cronradar.com/blog/how-to-know-if-cron-job-ran

> Four ways to check whether a cron job ran and succeeded: cron's log, output redirection, exit codes, and heartbeat monitoring, and what each can prove.

**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&ndash;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](https://sources.debian.org/src/rsyslog/8.2504.0-1+deb13u2/debian/rsyslog.conf/) and the [Debian 12 release notes](https://www.debian.org/releases/bookworm/amd64/release-notes/ch-information.en.html#changes-to-system-logging), which say rsyslog "is no longer installed by default"; Ubuntu 24.04's [50-default.conf](https://git.launchpad.net/ubuntu/+source/rsyslog/tree/debian/50-default.conf?h=applied/ubuntu/noble), installed because `ubuntu-minimal` [recommends rsyslog](https://git.launchpad.net/ubuntu/+source/ubuntu-meta/tree/minimal-recommends-amd64?h=applied/ubuntu/noble); CentOS Stream 10's [rsyslog.conf](https://gitlab.com/redhat/centos-stream/rpms/rsyslog/-/blob/c10s/rsyslog.conf) and [core package group](https://gitlab.com/redhat/centos-stream/release-engineering/comps/-/blob/c10s/comps-centos-stream-10.xml.in), which includes rsyslog; Fedora's [NoDefaultSyslog](https://fedoraproject.org/wiki/Changes/NoDefaultSyslog) change in Fedora 20; and BusyBox 1.37.0's [crond.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/miscutils/crond.c#L1049-L1053) and [syslogd.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/sysklogd/syslogd.c#L143).

```bash
# 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)](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html)), 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](https://cronradar.com/blog/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)](https://www.freedesktop.org/software/systemd/man/latest/journald.conf.html)). systemd 259 made `persistent` the compiled-in default ([NEWS](https://github.com/systemd/systemd/blob/v262.1/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)](https://www.freedesktop.org/software/systemd/man/latest/systemd-journald.service.html) describes, then move the current logs into it:

```bash
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo journalctl --flush
```

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." 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)](https://manpages.debian.org/trixie/cron/cron.8.en.html)). cronie logs a `CMDEND` line when a job finishes, without its exit status ([do_command.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/do_command.c#L662-L667)), 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"](https://cronradar.com/blog/cron-no-mta-installed) covers each implementation). Capture it by appending a redirect to the crontab line:

```bash
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](https://cronradar.com/blog/scheduled-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:

```bash
0 * * * * /opt/sync.sh; rc=$?; echo "$(date) exit $rc" >> /var/log/sync.log
```

The 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](https://cronradar.com/blog/cron-dead-mans-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](https://cronradar.com) 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](https://docs.cronradar.com/sdks) and read its published-version limitations. Other runtimes can use explicit [HTTP reporting](https://docs.cronradar.com/getting-started?setup=direct), which does not discover schedules automatically; the [dead man's switch guide](https://cronradar.com/blog/cron-dead-mans-switch) 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](https://docs.cronradar.com/verify) 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)](https://man7.org/linux/man-pages/man8/cron.8.html)), and Debian's and Ubuntu's cron log `No MTA installed, discarding output` and delete it ([cron "No MTA installed"](https://cronradar.com/blog/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](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/risk_reduction_and_recovery_operations/troubleshooting-problems-by-using-log-files)); 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](https://cronradar.com/blog/prevent-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](https://cronradar.com/blog/cron-job-not-running) walks through the common causes.
