← all posts

Where are cron logs? And why your job's output isn't in them

Cron logs to the journal, /var/log/syslog, /var/log/cron or /var/log/messages, depending on the distro. We ran the same failing job on six of them. None logged its exit code, and two threw its output away.

9 min read

You want to know why last night’s backup didn’t run. You type grep CRON /var/log/syslog and get No such file or directory, or this:

CRON[82]: (root) CMD (/usr/local/bin/backup.sh)
CRON[80]: (CRON) info (No MTA installed, discarding output)

Cron logged that it started the job, and that it threw away whatever the job printed. It never says how the job ended. Where cron logs and how much depends on the distro, so we ran the same job on six of them: one line to stdout, one to stderr, exit 3, like a backup whose database is down.

0 of 6distros that logged the exit code by default
2 of 6threw the job's output away
2 of 6had no cron log file, only the journal

Short answer

  • Any systemd box: the journal. journalctl -t CRON on Debian and Ubuntu, journalctl -t CROND on Fedora, RHEL and Arch. On Debian 12+ and Arch that’s the only place.
  • Ubuntu, or Debian with rsyslog: /var/log/syslog.
  • RHEL, AlmaLinux, Rocky, Fedora Server: /var/log/cron.
  • Alpine: /var/log/messages.

That log shows a job started. The job’s output is only in it on cronie distros, and the exit code isn’t in it anywhere. To keep both, redirect the output in the crontab line and wrap the job to log its exit code, or run it under something that records runs: a systemd timer, or RunWisp, an open-source cron replacement that keeps every run’s output and exit code from three lines of config per job. Both setups are below.

CronCron’s own logJob’s stdout and stderrExit code
Ubuntu 26.04Debian cronjournal + /var/log/syslog✗ discarded-L 4 only
Debian 13Debian cronjournal onlymailed to /var/mail, or discarded-L 4 only
AlmaLinux 10croniejournal + /var/log/cron✓ CMDOUT lines✗
Fedora 44croniejournal + /var/log/cron✓ CMDOUT lines✗
Archcroniejournal only✓ CMDOUT lines✗
Alpine 3.24BusyBox crond/var/log/messages✗ discarded, not logged✗
RunWisp 1.7 (ours)replaces cronWeb UI, TUI, runwisp logs✓ one log file per run✓ per run

Find it

On systemd, ask the journal for the job lines by name. A name that doesn’t exist matches nothing, so this works on all five systemd distros above:

$ journalctl -t CRON -t CROND --since today

Add -g backup.sh to narrow it to one job. Without systemd, grep -i cron the files from the short answer.

Most guides say journalctl -u crond. On Fedora that showed only the daemon starting up. The job lines were there, filed under session-c17.scope, session-c19.scope and so on, because Fedora’s cron PAM config opens a login session for every job. AlmaLinux ships the same rule. -t CROND matches the name the lines were logged under, so it finds them either way.

What each cron logs, and what to turn on

Debian and Ubuntu log a CMD line per job and, with no mail server, the “discarding output” line. Set EXTRA_OPTS="-L 15" in /etc/default/cron and restart cron, and failures get the only exit code any of these six crons will log:

CRON[125]: (CRON) error (grandchild #127 failed with exit status 3)

Debian 13 has no syslog daemon until you apt install rsyslog, and then you also get /var/log/cron.log. Ubuntu ships that rule commented out in /etc/rsyslog.d/50-default.conf.

cronie (RHEL, Fedora, Arch) logs the most:

CROND[50]: (root) CMD (/usr/local/bin/backup.sh)
CROND[46]: (root) CMDOUT (backup: wrote 1204 rows)
CROND[46]: (root) CMDOUT (pg_dump: connection refused)
CROND[46]: (root) CMDEND (/usr/local/bin/backup.sh)

CMDEND doesn’t say the job failed, and there’s no option to make it. And the CMDOUT lines only happen while there’s no /usr/sbin/sendmail. We added one on the Fedora box, and the next run’s output went to root’s mailbox instead. So installing postfix for some other app quietly moves your cron output. CRONDARGS=-s in /etc/sysconfig/crond keeps it in the log either way.

BusyBox crond (Alpine) logs USER root pid 242 cmd ... and nothing else, even at its most verbose level. The output goes to its stdout, which OpenRC points at /dev/null.

Keep the output yourself

A log file

0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

2>&1 has to come after the >>, or stderr still goes to cron’s mail. That keeps the output, but the file has no timestamps, no run boundaries and no exit code, and nothing rotates it. A wrapper fixes the first three. Save it as /usr/local/bin/cronlog:

#!/bin/sh
# usage: cronlog NAME COMMAND [ARGS...]
log="/var/log/cron-jobs/$1.log"
shift
{
    echo "=== $(date "+%F %T") start: $*"
    "$@"
    rc=$?
    echo "=== $(date "+%F %T") exit $rc"
} >> "$log" 2>&1
exit $rc
0 3 * * * /usr/local/bin/cronlog backup /usr/local/bin/backup.sh
=== 2026-10-16 10:37:33 start: /usr/local/bin/backup.sh
backup: wrote 1204 rows
pg_dump: connection refused
=== 2026-10-16 10:37:33 exit 3

It exits with the job’s code, so a || curl alert after it still fires. For rotation, mkdir /var/log/cron-jobs and add /etc/logrotate.d/cron-jobs:

/var/log/cron-jobs/*.log {
    weekly
    rotate 8
    compress
    missingok
}

The journal

The journal already has timestamps, rotation and a size cap. Let systemd-cat run the job:

0 3 * * * systemd-cat -t backup /usr/local/bin/backup.sh || logger -t backup "exit $?"

Then journalctl -t backup. systemd-cat exits with the job’s code, 3 in our test, so the || logger line records the failure. The version most answers give, backup.sh 2>&1 | logger, exited 0: the pipe reports logger’s exit code, so neither cron nor your alert ever sees the job fail.

In Docker

docker logs reads PID 1’s stdout and stderr, and cron jobs don’t write there. Debian’s cron as PID 1 showed nothing in podman logs until we redirected the job:

* * * * * /app/job.sh > /proc/1/fd/1 2>/proc/1/fd/2

BusyBox crond -f is the exception: its jobs inherit its stdout, so their output already shows up. For a whole compose stack, see Docker Compose cron jobs with a web UI.

Or run jobs under something that records runs

All of the above patches cron from the outside. Two schedulers keep the output and exit code themselves. Here’s the same nightly backup set up under each, and then a failure alert added to both.

systemd timers

A timer runs the job as a service, so systemd keeps its output in the journal and records how it ended.

1. The service, /etc/systemd/system/backup.service:

[Unit]
Description=Database backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

2. The timer, /etc/systemd/system/backup.timer. 0 3 * * * becomes:

[Unit]
Description=Run the database backup nightly

[Timer]
OnCalendar=*-*-* 03:00:00
AccuracySec=1s
Persistent=true

[Install]
WantedBy=timers.target

AccuracySec defaults to a minute, and our test timer set for :00 started at 10:57:38 and 10:58:04. Persistent=true runs a job missed while the box was off at the next boot. To translate other cron lines, check each with systemd-analyze calendar '*:0/15'.

3. Load it, start it, and delete the line from your crontab:

$ sudo systemctl daemon-reload
$ sudo systemctl enable --now backup.timer
$ crontab -e

4. Reading it. systemctl status has the last run. For the ones before it, grep the journal for systemd’s end-of-run lines, then pull one run’s output by its time:

$ systemctl status backup
× backup.service - Database backup
     Active: failed (Result: exit-code) since Fri 2026-10-16 10:58:04 UTC; 5s ago
TriggeredBy: ● backup.timer
   Main PID: 354 (code=exited, status=3)
$ journalctl -u backup -g "Finished|Failed to start"
Oct 16 10:57:38 web1 systemd[1]: Failed to start backup.service - Database backup.
Oct 16 10:58:04 web1 systemd[1]: Failed to start backup.service - Database backup.
$ journalctl -u backup --since 10:58 --until 10:59
Oct 16 10:58:04 web1 systemd[1]: Starting backup.service - Database backup...
Oct 16 10:58:04 web1 backup.sh[354]: pg_dump: dumping database app
Oct 16 10:58:04 web1 backup.sh[354]: pg_dump: error: connection refused
Oct 16 10:58:04 web1 systemd[1]: backup.service: Main process exited, code=exited, status=3/NOTIMPLEMENTED
Oct 16 10:58:04 web1 systemd[1]: backup.service: Failed with result 'exit-code'.

Steps 1 to 3 repeat for every line in your crontab. In return it’s already installed, needs nothing else running, and stops a job by killing its whole cgroup. More in RunWisp vs systemd timers.

RunWisp

RunWisp (our project, so weigh accordingly) is an open-source cron replacement that ships as a single binary. The same job:

1. Install it into /usr/local/bin, where sudo can find it:

$ curl -fsSL https://get.runwisp.com | sudo sh

2. The job, /etc/runwisp/runwisp.toml:

[tasks.backup]
cron = "0 3 * * *"
run  = "/usr/local/bin/backup.sh"

The schedule stays a cron line. runwisp validate --explain prints it as “At 03:00” with the next three run times.

3. Start it at boot, and delete the line from your crontab:

$ sudo runwisp service install
$ crontab -e

service install also generates a password for the web UI and prints it once. It’s kept in /etc/systemd/system/runwisp.service.d/password.conf if you lose it.

4. Reading it. sudo runwisp logs backup prints the last run:

$ sudo runwisp logs backup
pg_dump: dumping database app
pg_dump: error: connection refused
[WARN] run failed task=backup run=01M525WSW1B8DYHAMG216JW34P exit=3 reason=failed dur=35ms

For every run, sudo runwisp tui works over SSH, or tunnel the web UI with ssh -L 9477:localhost:9477 you@server and log in with that password:

localhost:9477/tasks/backup
every run, its exit code, and what it printed
RunWisp task page for backup, scheduled every minute: a run list where the 12:45 and 12:41 runs show exit 3 and the others succeeded in 4 to 5ms. The selected 12:45 run failed after 3ms, and its console output shows pg_dump failing to connect to the database server.

More jobs are three more lines each in the same file, then sudo runwisp reload. What you get without configuring anything else:

  • One log file per run, listed with its start time, duration and exit code.
  • Search through the output of every run of a task at once.
  • A size cap: each run’s log stops at 100 MB, keeping the newest output. Add keep_for = "30d" to a task to delete its old runs.
  • Docker: the RunWisp image prints every job’s output to docker compose logs as backup | line.

The limits: old runs are kept until you set keep_runs or keep_for, logs stay on that machine’s disk instead of going to Loki or ELK, and it’s one more service to keep running, which systemd timers aren’t.

Adding a failure alert

Both examples use ntfy, a free, open-source push notification service. Install the ntfy app on your phone and subscribe to a topic, which is just a name you make up. Anything posted to https://ntfy.sh/<topic> then shows up as a notification. There’s no account and no password, so anyone who knows the topic name can read it: pick one nobody will guess, like acme-ops-7f3k.

systemd needs a template unit that sends the message, written once, /etc/systemd/system/[email protected]:

[Unit]
Description=Failure alert for %i

[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS -d "%i failed on %H" https://ntfy.sh/acme-ops-7f3k

Then one line in the [Unit] section of every service that should alert, and a reload:

OnFailure=notify-failure@%n.service
$ sudo systemctl daemon-reload

When backup.service fails, systemd starts [email protected], and your phone shows backup.service failed on web1. Go to the journal for the output.

RunWisp needs the channel once, at the top of runwisp.toml, and one line on the task:

[notifiers.phone]
type  = "ntfy"
topic = "acme-ops-7f3k"

[tasks.backup]
cron   = "0 3 * * *"
run    = "/usr/local/bin/backup.sh"
notify = ["phone"]
$ sudo runwisp reload

The alert names the task, its exit code and how long it ran. Slack, Discord, Telegram, email and webhooks work the same way, with a different type; see the notification docs.

Common questions

Where are cron logs on Ubuntu?
In /var/log/syslog and the journal: grep CRON /var/log/syslog, or journalctl -t CRON. Each line only records that a job started. Without a mail server, Ubuntu's cron discards the job's output and logs (CRON) info (No MTA installed, discarding output). Add >> /var/log/job.log 2>&1 to the crontab line to keep it.
Where is the cron log on RHEL, Rocky, AlmaLinux or Fedora?
In /var/log/cron and the journal. Use journalctl -t CROND rather than journalctl -u crond: on Fedora 44 the job lines were filed under per-job login sessions, so -u crond only showed the daemon starting. cronie also logs the job's output as CMDOUT lines, but only while no mail server is installed.
Why is there no /var/log/syslog on my server?
Debian 12 and newer and Arch don't install a syslog daemon, so nothing writes that file. Cron logs to the systemd journal instead: journalctl -t CRON on Debian, journalctl -t CROND on Arch. apt install rsyslog brings back /var/log/syslog and, on Debian, /var/log/cron.log.
Does cron log the exit code of a job?
Not by default on any of the six distros we tested. Debian and Ubuntu's cron logs failures if you set EXTRA_OPTS="-L 15" in /etc/default/cron, as (CRON) error (grandchild #127 failed with exit status 3). cronie and BusyBox crond have no option for it.
How do I see cron job output in Docker?
Redirect each job to PID 1's streams, which is what docker logs reads: * * * * * /app/job.sh > /proc/1/fd/1 2>/proc/1/fd/2. BusyBox crond run in the foreground with -f is the exception: its jobs inherit its stdout, so their output already shows up.
How can I see a history of every cron run with its output?
Cron doesn't keep one. systemd timers keep the output in the journal and the last exit status in systemctl status. RunWisp, an open-source cron replacement that ships as a single binary, keeps a log file, exit code and duration for every run and shows them in a web UI and terminal UI. A job is three lines of TOML that keep the cron schedule as it is, plus notify = [...] for an alert. Its limits: logs stay on that machine's disk, and old runs are kept until you set keep_runs or keep_for.
  1. Ubuntu 26.04 Rust coreutils (uutils) vs GNU: what changes for cron and shell scripts

    Which uutils commands behave differently from GNU coreutils on Ubuntu 26.04 and 26.10, tested on 567 commands: mkdir locks, mv and mtime, timeout, date and DST, df, chown. How to check which you have and how to use GNU instead.

  2. Debian cron vs cronie vs BusyBox crond: CRON_TZ, /etc/cron.d, env vars and syntax compared

    How cron differs on Ubuntu, Debian, Fedora, RHEL, Arch and Alpine: which crons support CRON_TZ, read /etc/cron.d, pass Docker env vars, handle a bad crontab line, and accept seconds, L, W and #. Tested on six distros.

Get started
GPL 3.0 · no accounts · free to self-host

Start in 30 seconds. Ship on your terms.

A single Go binary. If it doesn't earn the disk space, rm it. No account, no signup, no catch.

Install in 30 seconds
curl -fsSL https://get.runwisp.com | sh