Chapter 40 — Cron, environment variables, and gluing things together
In one sentence
cron runs a command on a schedule, completely unattended — which means it
also runs with a much SMALLER environment than your interactive shell,
silently, and that gap is the single most common reason "it works when I run
it myself" and "it fails under cron" are both true at once.
Why this matters
A silently-failing scheduled job (a backup, a cleanup task, a report) can go unnoticed for weeks — genuinely common, and genuinely costly when the job turns out to have mattered.
How it actually works
crontab -e # edit YOUR OWN scheduled jobs
crontab -l # list them
# format: minute hour day-of-month month day-of-week command
0 2 * * * /path/to/backup.sh # every day at 2:00 AM
*/15 * * * * /path/to/healthcheck.sh # every 15 minutes
0 0 1 * * /path/to/monthly.sh # midnight, first of every
# month
ENVIRONMENT VARIABLES are named values available to a process and
everything it launches -- `$PATH` (where to look for commands),
`$HOME`, `$USER`, and anything else you or a program has set:
export MY_VAR="value" # set it, and make it available to
# any CHILD process this shell launches
echo $MY_VAR # read it
env # list every variable in the CURRENT
# environment
printenv PATH # just one specific variable
WHY CRON JOBS BEHAVE DIFFERENTLY: cron runs your job with a MINIMAL,
often DIFFERENT environment than your interactive login shell --
frequently a much SHORTER `$PATH`, no shell customisations from your
`.bashrc`, and none of the environment variables your normal
terminal session has accumulated. A script that calls a command by
NAME alone (relying on `$PATH` to find it) can work perfectly when
YOU run it, and fail silently (or with a confusing "command not
found") under cron, purely because cron's `$PATH` doesn't include
wherever that command actually lives.
Real-world example
"The cron job has been silently failing for weeks and nobody
noticed" is one of the most universally-experienced real operational
patterns in the industry -- documented across countless sysadmin
forums, blog postmortems, and "lessons learned" writeups, precisely
because the FAILURE MODE ITSELF is silent by design: cron, by
default, only EMAILS output if the system's mail delivery is
actually configured (which, on a huge number of real servers, it
simply isn't) -- so a script that starts failing, whether from an
environment mismatch (this chapter's exact `$PATH` gotcha), a
changed file location, an expired credential, or any other cause,
can fail EVERY SINGLE RUN, indefinitely, with the job's log showing
nothing but a string of quiet non-zero exit codes nobody is watching.
THE STANDARD, WIDELY-RECOMMENDED REAL FIXES, all addressing the same
underlying "silent by default" problem:
- set `PATH` EXPLICITLY at the top of the crontab, or use full,
absolute paths for every command inside the script itself,
rather than relying on cron's minimal default `$PATH`.
- redirect the job's output EXPLICITLY to a log file
(`>> /var/log/myjob.log 2>&1`) so there's always somewhere to
actually look.
- add MONITORING that alerts if the job HASN'T reported success
recently (a "dead man's switch" pattern -- if a heartbeat/success
signal stops arriving, THAT absence is the alert), rather than
relying on the job itself to notice and report its own failure,
which is exactly the assumption that fails when the job is
broken badly enough to not run at all.
Try it yourself
# See the environment difference directly -- this is the whole lesson
echo "interactive PATH: $PATH"
# Simulate cron's minimal environment and see a script behave
# differently
env -i /bin/bash -c 'echo "minimal PATH: $PATH"; which curl || echo "curl not found!"'
# Write a small, properly defensive cron-style script
cat > ~/lab/playground/cronjob.sh << 'EOF'
#!/bin/bash
set -euo pipefail
LOG="/tmp/cronjob.log"
echo "$(date): job started" >> "$LOG"
/usr/bin/echo "doing the actual work" >> "$LOG" # absolute path --
# doesn't rely on
# cron's $PATH at all
echo "$(date): job finished successfully" >> "$LOG"
EOF
chmod +x ~/lab/playground/cronjob.sh
# Install it to run every minute, briefly, to see it work (remove
# afterward)
(crontab -l 2>/dev/null; echo "* * * * * ~/lab/playground/cronjob.sh") | crontab -
sleep 65
cat /tmp/cronjob.log
crontab -l | grep -v cronjob.sh | crontab - # remove the test entry
rm /tmp/cronjob.log ~/lab/playground/cronjob.sh
Common mistakes
- Calling commands by name in a cron script, relying on
$PATH. Use absolute paths, or explicitly setPATHat the top of the crontab. - Assuming "no error email" means "it's working." Mail delivery often isn't configured at all — redirect output to a log file explicitly, and monitor for the job's ABSENCE of success, not just its presence of error.
- Never testing a cron job's ACTUAL cron execution, only running the script manually. The environment difference means "works when I run it" is genuinely weak evidence it'll work under cron — test it for real.
Check yourself
- Why can a script work perfectly when run manually but fail under cron?
- What are two concrete, standard fixes for the "cron job silently fails" problem?
- What is a "dead man's switch" monitoring pattern, and why does it fit this specific failure mode better than "alert on error output"?
Further reading
- Reference:
man 5 crontab,man 1 crontab,man 1 env. - Tool:
crontab.guru— an interactive tool for reading/writing cron schedule expressions, genuinely useful even for experienced engineers. - Article: search "cron job silent failure best practices" for current, practical monitoring-pattern writeups.