Blog

Where are cron logs? Finding cron output on Ubuntu, Debian, RHEL, Alpine and in containers

Cron logs live in a different place on every distribution, and the output of your job is not in any of them. Here is where to look for both, system by system, and how to make the next failure easy to find.

Published on October 13, 2026

“Where are cron logs?” is really two questions, and most of the confusion around cron comes from mixing them up. The first is: did cron start my job at all? That is answered by the cron daemon’s log, and where it lives depends on your distribution. The second is: what did my job print, and why did it fail? That is the job’s output, and cron does not write it to any log file. It mails it, and on most servers that mail goes nowhere.

This is the second post in our Cron in production series. The first one covered the reasons a job does not run. This one is about finding the evidence.

Part 1: the cron daemon log, by system

Cron itself logs through syslog. Whether that ends up in a file, in the systemd journal, or both, depends on whether a syslog daemon is installed. The fastest way to find out is to check for the file first and fall back to the journal.

Ubuntu and Debian

With rsyslog installed, cron lines go to /var/log/syslog:

grep CRON /var/log/syslog

If there is no /var/log/syslog, the machine has no syslog daemon (common on recent Debian and on minimal Ubuntu images) and everything is in the journal:

journalctl -u cron --since "1 hour ago"

A dedicated /var/log/cron.log exists only if someone enabled it. The line is in /etc/rsyslog.d/50-default.conf, commented out by default:

#cron.*                          /var/log/cron.log

Uncomment it and restart rsyslog, and cron gets its own file. The entries still appear in /var/log/syslog too.

Debian’s cron can log more than the start of each job. The -L option in /etc/default/cron is a bitmask: 1 logs job start, 2 logs job end, 4 logs failed jobs with their exit status, 8 adds the process id. The default is 1, which is why you normally never see a failure in the cron log. This makes failures visible:

# /etc/default/cron
EXTRA_OPTS="-L 15"

RHEL, Rocky, AlmaLinux, CentOS and Fedora

These ship cronie, and rsyslog routes its messages to a dedicated file:

grep CROND /var/log/cron
journalctl -u crond --since "1 hour ago"

On minimal Fedora or cloud images without rsyslog, only the second command works. Note the daemon is crond, not cron, so journalctl -u cron returns nothing and looks like “no logs” when it is really “wrong unit name”.

Alpine

Alpine uses the BusyBox crond. It logs through syslog, and with the BusyBox syslogd that means /var/log/messages:

grep crond /var/log/messages

BusyBox crond takes a log level with -l (lower is more verbose) and can write straight to a file with -L. The options live in /etc/conf.d/crond. If the default level is too quiet to show job starts, lower it there and restart the service with rc-service crond restart.

Inside Docker and other containers

In a container there is usually no syslog and no journal, so cron’s log goes nowhere unless you run the daemon in the foreground and point its logging at the terminal. Cronie:

crond -n

BusyBox (Alpine images), with log output on stderr:

crond -f -d 8

Either way the lines end up in docker logs <container>. Your jobs’ own output does not: it still goes to mail, which does not exist in the container. Redirect it to the container’s stdout explicitly:

*/5 * * * * /app/sync.sh > /proc/1/fd/1 2>&1

/proc/1/fd/1 is the stdout of process 1, which is the container’s main process. That is the only thing docker logs reads.

What the daemon log actually tells you

A healthy start looks like this on Debian or Ubuntu:

Oct 13 02:00:01 web1 CRON[18233]: (root) CMD (/opt/scripts/backup.sh)

And like this with cronie:

Oct 13 02:00:01 web1 CROND[18233]: (root) CMD (/opt/scripts/backup.sh)

That line means cron forked a process for the job. It does not mean the job succeeded, and by default nothing is logged when it fails. If the CMD line is missing at the time you expected, cron never started the job, and the causes are in the previous post. Check that the schedule means what you think with the cron expression parser first; it shows the next five runs in your own time zone.

Two other lines are worth recognising:

CRON[18233]: (CRON) info (No MTA installed, discarding output)

The job printed something, cron tried to mail it, there is no mail system, so the output was thrown away. You just lost the error message you are looking for.

CRON[18201]: pam_unix(cron:session): session opened for user root(uid=0) by (uid=0)

This is just cron opening a session before the job. It is noise, but a lot of noise: on a busy crontab it drowns the CMD lines, which is why the grep examples above filter on CMD or on the daemon name.

Part 2: where your job’s output went

Everything a job writes to stdout and stderr is collected by cron and sent as an email to the user whose crontab it is, or to the address in MAILTO. If a local mail system is installed, that mail is in the user’s mailbox:

mail                # or: mailx
less /var/mail/root

If no mail system is installed, the output is discarded, and the daemon log says so (see above). This is the default state of most servers built in the last ten years, which is why “cron has no logs” is such a common belief: the logs existed for a fraction of a second and were deleted.

You have three ways to keep the output. Pick one per job.

Redirect to a file. The simplest and the most common:

0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

The 2>&1 matters. Without it, stderr still goes to mail and the file only holds the uninteresting half. Add the file to logrotate, because a job that logs every five minutes will eventually fill the disk. Lines have no timestamps unless the script adds them; ts from moreutils does that cheaply: backup.sh 2>&1 | ts '%F %T' >> /var/log/backup.log.

Send it to syslog or the journal. Then the job’s output sits next to the daemon’s log, with timestamps, rotation and remote shipping already handled:

0 2 * * * /opt/scripts/backup.sh 2>&1 | logger -t backup

Read it with journalctl -t backup or grep backup /var/log/syslog. One trap: the exit status cron sees is now logger’s, which is always 0. If you also alert on exit codes, wrap the job in bash -o pipefail -c '...' or put the pipe inside the script.

Keep the mail, but make it reach someone. Install a minimal relay such as msmtp or a local postfix configured to forward to your mail provider, and set MAILTO at the top of the crontab. This is the original design and it works, but you then get an email for every run that prints anything, and people learn to filter them within a week.

A quick diagnosis table

What you seeWhat it meansWhere to look next
No CMD line at the expected timeCron did not start the job12 causes a cron job does not run
CMD line, then No MTA installed, discarding outputJob ran and printed something, output lostRedirect output, rerun, read it
CMD line, job output looks fine, nothing happenedJob ran in a different environment than your shellPATH, variables, working directory (next post but one)
CMD line, then failed with exit status N (Debian with -L 4)Job ran and failedIts own log or redirected output
Output file exists but stops at some dateJob stopped running or the file was rotated without copytruncateDaemon log for that date, logrotate config

Part 3: knowing next time, without reading logs

Everything above is forensics. It tells you what happened after someone noticed that the report did not arrive. The question “did last night’s backup run?” should not require a login and a grep.

The usual fix is a heartbeat: the job tells a monitor when it starts and when it ends, and the monitor alerts when a start is late or an end is missing. With HelloCron that is two curl calls around the existing command, or one wrapper that also captures the exit code and the last lines of error output, so the “where did the output go” problem from Part 2 disappears as well:

0 2 * * * /usr/local/bin/hellocron run nightly-backup /opt/scripts/backup.sh

When the job fails, the alert contains the exit code and the error output. When it never starts, you get an alert anyway, which no log file can give you, because a job that did not run writes nothing. Monitor a cron job in 60 seconds walks through the setup, and the shell script guide covers the wrapper. The free plan includes 15 monitors, which covers most servers.

Quick checklist

  1. Daemon log: /var/log/syslog or journalctl -u cron (Debian, Ubuntu), /var/log/cron or journalctl -u crond (RHEL family), /var/log/messages (Alpine), docker logs with crond in the foreground (containers).
  2. Look for the CMD line at the expected time. No line means cron never started the job.
  3. Job output goes to mail, not to a log. No MTA installed, discarding output means it is gone.
  4. Redirect with >> file 2>&1, pipe to logger -t name, or install a mail relay. One of the three, for every job.
  5. On Debian, EXTRA_OPTS="-L 15" makes failures show up in the cron log.
  6. For the question “did it run last night”, use a heartbeat monitor instead of a log.

Next in the series: Crontab syntax explained covers every field and special character, with the examples that trip people up.

← Back to blog

Start monitoring in 60 seconds

Sign up, get an API key, paste the curl into your cron. That's it.