Skip to content

cron Environment and Logs

A command that works perfectly when typed at the terminal isn't guaranteed to behave the same way once it's added to a crontab. This is the single largest class of time-wasting cron problems: a script runs flawlessly by hand, and through cron it does apparently nothing at all. The cause is almost always the same one — cron runs the command in an environment that's much sparser than an interactive terminal session.

What You Will Learn

By the end of this article you will be able to:

  • explain how cron's environment differs from an interactive terminal's;
  • set PATH and other environment variables correctly inside a crontab;
  • configure MAILTO to receive output by email, or turn it off;
  • escape the % character correctly so date-style commands don't break under cron;
  • check a cron job's outcome through journalctl and syslog;
  • systematically diagnose a "works by hand, doesn't work under cron" problem.

How cron's Environment Differs From a Terminal's

When you log into a terminal, the shell (bash) reads your .bashrc and .bash_profile, which typically extend PATH and define aliases and functions. The cron daemon runs a command in a non-login, non-interactive minimal environment instead — none of those files are read.

echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/<username>/.local/bin

Now compare that with the PATH cron itself sees, by writing the output of env to a file through a temporary crontab job:

crontab -e
* * * * * env > /tmp/cron-env.txt

Wait a minute, check the result, then remove the job:

cat /tmp/cron-env.txt
SHELL=/bin/sh
PATH=/usr/bin:/bin
LOGNAME=<username>
HOME=/home/<username>

The difference is immediate: PATH is much shorter (/usr/bin:/bin), .local/bin and any other manually added directories are gone, and SHELL is set to /bin/sh — usually a more limited, POSIX-oriented shell than bash.

Tip

After testing, remove the * * * * * env > /tmp/cron-env.txt line with crontab -e — otherwise it keeps overwriting the file every minute for no benefit.

Fixing PATH Inside a Crontab

A command that works at the terminal but reports "command not found" under cron is almost always caused by this shorter PATH. There are two fixes:

Option 1 — use an absolute path in the command (recommended):

0 2 * * * /usr/local/bin/backup.sh

Option 2 — set PATH explicitly at the top of the crontab:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

0 2 * * * backup.sh

Lines written as KEY=value at the top of a crontab are environment variable assignments, and they apply to every job below them. The second approach can look more convenient, but the first is more reliable: the script behaves the same no matter which directory it's invoked from, and it doesn't stay dependent on a particular PATH setting if it's later moved to another server.

The Working Directory Isn't the Terminal's Either

cron typically starts a command from the user's $HOME directory, while the terminal you tested it in might have been sitting somewhere else. A script that relies on relative paths or relative file references can fail for exactly this reason:

0 2 * * * cd /opt/myapp && ./backup.sh

cd /opt/myapp && explicitly sets the working directory before the script runs, guaranteeing that relative paths inside the script (./config.yaml, for example) resolve correctly.

Controlling Output: MAILTO

By default, if a command produces any output (stdout or stderr), cron tries to send it by email (assuming local mail delivery is configured on the system):

[email protected]

0 2 * * * /usr/local/bin/backup.sh

Set at the top of the file, this line sends the output of every job below it to the given address. On many production servers local mail is never configured at all — in that case the email "attempts to send," goes nowhere, and simply piles up in a local mail queue.

If email isn't wanted at all, it can be turned off entirely:

MAILTO=""

Redirecting Output to a File

The most reliable approach in practice is not to rely on email at all, but to send output to a specific file:

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

>> /var/log/backup.log appends standard output to the file without truncating it, and 2>&1 redirects the error stream (stderr) into the same file. This is the same redirection mechanism covered in Streams, Pipes, and Redirection — the difference is only that it's running inside cron's environment rather than an interactive terminal.

Warning

If the script already does its own logging — the log() function in the backup script from the bash scripting module, for example — adding >> file 2>&1 on top is still useful, because it catches errors the script itself doesn't handle (the interpreter failing to start, for instance). The two don't conflict; they complement each other.

The % Character Is Special in a Crontab

Inside a crontab command an unescaped % is not a literal percent sign. cron replaces the first % with a newline, and passes everything after it to the command as standard input. This silently breaks any command that uses % — most often date:

0 2 * * * /usr/local/bin/backup.sh --out backup-$(date +%Y%m%d).tar.gz

Here cron turns %Y%m%d... into a newline plus stdin, so the script sees a truncated --out backup- and no date at all — with no error to explain why. Each % has to be escaped with a backslash:

0 2 * * * /usr/local/bin/backup.sh --out backup-$(date +\%Y\%m\%d).tar.gz

The cleaner fix is to move anything using % into the script, where ordinary shell rules apply and no escaping is needed; the crontab line then just invokes the script with no % in sight. This is one more reason to keep crontab lines thin and put real logic in a script file.

Tracking Output Through journalctl and syslog

Every time a cron job runs — whether it produces output or not — the daemon itself records the fact in the system log. On Ubuntu Server this is visible through journalctl:

journalctl -u cron.service --since today
Jul 26 02:00:01 myserver CRON[18422]: (root) CMD (/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1)

This entry confirms the job started, not that it finished successfully — cron logs the same kind of entry regardless of whether the command itself exited with an error. Knowing success from failure requires checking the script's own output or its exit code.

The same entry is also present in the traditional text log (as covered in Linux Logs, rsyslog reads messages from journald and writes them out to a file):

grep CRON /var/log/syslog | tail -n 5
Jul 26 02:00:01 myserver CRON[18422]: (root) CMD (/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1)

On the RHEL/CentOS/Rocky family these entries typically go to /var/log/cron instead, and syslog may not exist at all — since the exact location varies by distribution, starting with journalctl -u crond is the more universal first check.

Practical Scenario: "Works by Hand, Doesn't Work Under cron"

Problem. The /usr/local/bin/sync-reports.sh script runs without issue when started manually from a terminal, but the 0 3 * * * sync-reports.sh job set up in crontab produces no visible result at all.

Step 1 — confirm the job is running at all:

journalctl -u cron.service --since "1 hour ago" | grep sync-reports
Jul 26 03:00:01 myserver CRON[19011]: (root) CMD (sync-reports.sh)

The job did start — so the problem isn't cron itself, it's the conditions the command runs under.

Step 2 — check the PATH and relative-path suspicion:

which sync-reports.sh
/usr/local/bin/sync-reports.sh

The file is on PATH, but the crontab line just says sync-reports.sh — found through PATH at a terminal, but whether cron's shorter PATH also includes /usr/local/bin needs to be checked separately, using the env > /tmp/cron-env.txt technique from earlier.

Step 3 — retry with an absolute path and output redirected to a log:

0 3 * * * /usr/local/bin/sync-reports.sh >> /var/log/sync-reports.log 2>&1

Verify:

tail -n 20 /var/log/sync-reports.log
rsync: command not found

Conclusion. The real cause is now visible — the rsync command used inside the script isn't found under cron's shortened PATH. The fix is either to use an absolute path for rsync (/usr/bin/rsync) inside the script, or to extend PATH at the top of the crontab.

Common Mistakes

  • Logging "it doesn't work" without checking the log first. journalctl -u cron.service immediately separates two very different problems — did the job start at all, or did it start and then fail — and shouldn't be skipped as the first diagnostic step.
  • Relying on MAILTO without ever verifying it. Assuming a message "gets sent" on a server with no local mail service configured is a mistake — the message never arrives anywhere. Redirecting output to a file or a centralized log is more reliable in production.
  • Testing with an interactive shell, then running under a different one in production. If a script starts with #!/bin/bash but uses bash-specific constructs, that's usually fine — cron runs the script file directly, not through /bin/sh. The more common cause of trouble is the opposite: a script written assuming plain POSIX sh semantics that turns out to rely on a bash-only feature. Checking the script's execute permission (chmod +x) and its shebang line is a useful first step either way.

Exercises

  1. Using a * * * * * env > /tmp/cron-env.txt job, compare cron's PATH value against your terminal's $PATH and note the differences.
  2. Create a test job that runs every minute and appends its output to ~/cron-test.log with >> ~/cron-test.log 2>&1; after a few minutes, compare what you see in journalctl -u cron.service against the file itself.
  3. Add MAILTO="" to the top of a crontab and explain what it disables (understanding the setting's meaning is enough, even without a real mail server present).
  4. Reproduce the "works by hand, doesn't work under cron" scenario above using a script of your own — one that calls a command outside /usr/local/bin, such as rsync.

Summary

cron runs commands in a noticeably poorer environment than a terminal — a shorter PATH, a different working directory, sh instead of bash. Knowing this in advance prevents most "works by hand, doesn't work under cron" problems: absolute paths, an explicit PATH, output redirected to a file, and checking journalctl -u cron.service are all facets of the same underlying issue. The next article looks at a modern alternative to cron — the systemd timer — which solves this same observability problem differently.

References