Cron on Linux: where the job lives, where output goes

06/08/2026 · Laurent Scheduled tasks
Cron on Linux: where the job lives, where output goes

You take over a server. Something emails a report every morning at six and the client wants it stopped. crontab -l prints nothing. The report arrives the next morning anyway.

This is not a mystery, it is an inventory problem. A Linux box has at least six places a recurring job can be defined, they are read by different mechanisms, and only one of them is what crontab -l shows you.

The six places

WhereShapeHow you see it
Per-user crontab5 fields, no user columncrontab -l, but only for the user you are
/etc/crontab5 fields plus a user columnRead the file. On Debian it mostly contains the run-parts lines.
/etc/cron.d/*Same, one concern per fileRead the directory. This is where packages install their schedules.
/etc/cron.{hourly,daily,weekly,monthly}Executable scripts, no expression at allList the directories. Files with a dot in the name are skipped.
systemd timersOnCalendar in a unit filesystemctl list-timers --all
Inside the applicationFramework scheduler, WP-Cron, a queue workerNothing on the system knows about it

The last row is the one that gets missed on WordPress work, because the schedule is a row in the database rather than a file on disk, and it fires on visitor traffic instead of on a clock. Nothing you grep on the filesystem will ever reveal it.

One pass that finds the rest

for u in $(cut -f1 -d: /etc/passwd); do
  crontab -l -u "$u" 2>/dev/null | sed "s/^/[$u] /"
done
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/
systemctl list-timers --all 2>/dev/null

Run that on any machine you have just inherited and paste the output into the client file. It takes a minute and it is the single most useful artefact you can produce on day one, because every later question about "what runs on this box" is answered from it instead of from memory.

Two things it will not catch. Jobs belonging to a user not listed in /etc/passwd, which happens with directory-backed accounts. And jobs on a hosting panel that keeps its own store and writes the real crontab only at deploy time; there, the panel is the source of truth and the file is a derived artefact, so editing the file gets your change overwritten without notice.

Where the output goes when you do not say

Cron collects everything a job writes to standard output and standard error and mails it to the crontab owner. That is the default, it dates from an era when every Unix machine had working local mail, and on a modern server it produces one of three outcomes.

There is a local mail transport. The output lands in /var/mail/<user> or the equivalent spool. It works, nobody reads it, and the file grows until somebody notices a partition filling up.

There is no mail transport. Common on containers and on minimal cloud images. Cron tries, fails, and typically logs the failure rather than the output. Your job's error message is gone.

MAILTO points at a real address. Now you get an email every time a job produces a single character on stderr, which for a chatty command is every run, and within a fortnight the whole thing is filtered into a folder nobody opens.

All three end the same way: output exists somewhere you are not looking. Redirect explicitly instead, and put the redirect on every line:

MAILTO=""

0 6 * * * /usr/local/bin/report.sh >> /var/log/report.log 2>&1

Order matters on that redirect. 2>&1 >> file and >> file 2>&1 are not the same thing: the first points stderr at wherever stdout was pointing before the file was attached, which under cron is nowhere. The correct form puts the file first. That, and the rest of what a crontab line does differently from your shell, is in everything after the fifth field.

If you want a log per run rather than one growing file, name it from the date, and remember that the percent sign has to be escaped:

0 6 * * * /usr/local/bin/report.sh >> /var/log/report-$(date +\%Y\%m\%d).log 2>&1

Reading the daemon's own log, and what it proves

Separate from your job's output is cron's log of what it launched. On Debian and Ubuntu that goes to syslog; on Red Hat family systems it goes to its own file.

journalctl -u cron --since today      # Debian, Ubuntu
journalctl -u crond --since today     # Fedora, RHEL, Alma
grep CRON /var/log/syslog             # where journald is not in use

You will see lines of the form CRON[12345]: (www-data) CMD (/usr/local/bin/report.sh). Read that sentence precisely. It says the daemon forked and executed the command. It says nothing about whether the command existed, whether it ran to completion, or what it returned. A job whose interpreter has been removed produces exactly the same CMD line as a job that worked perfectly.

This is the point where most cron debugging goes wrong. Somebody checks the log, sees the CMD line, concludes the job is running fine, and looks elsewhere for three hours. The log answers "did cron try", which is genuinely useful when the answer is no, and worthless as evidence of success.

The absence of a CMD line, on the other hand, is conclusive. If cron did not log an attempt, the schedule is not installed where you think it is, and you are back at the inventory.

What no log can tell you

Every mechanism above records something that happened. None of them records something that did not.

The crontab deleted during an account migration writes no line. The container rebuilt from an image that never had the crontab writes no line. The timer masked by a configuration management run writes no line. In all three cases the log for that job simply stops, and a log that stops looks identical to a log for a job that has nothing to say.

Which is why the only useful check on a scheduled job is one that runs somewhere else and expects to hear from it, rather than one that reads the machine's own records. The same reasoning applies to a website that returns a perfectly healthy status code while serving the wrong thing: a positive signal is not proof, and a missing signal is not nothing.

Back to contents

Share this post.
Stay up-to-date

Subscribe to our newsletter

Don't miss this

You might also like