Build the line instead of debugging it
The Cron Job Builder writes the crontab line with logging, PATH, MAILTO and flock already in it, warns about the day-of-month/day-of-week trap, and exports the same schedule as a systemd timer. The Cron Wrapper Generator adds timeout, retry and alerts. Background on why cron fails quietly: Bash Scripts That Survive Cron.
A cron job that does not run produces no error message anywhere you would look. A job that runs and fails usually produces one, and cron throws it away. Either way you find out days later, when the backup you needed is not there. The causes are few and they are always the same seven, so check them in order instead of guessing.
How Do I Know Whether Cron Ran the Job at All?
First, is there a cron daemon? Then, did it start the job? cron writes one CMD line to its log for every command it starts:
No CMD line for your job means cron never started it: the daemon is down, the line does not parse, or the schedule is not what you think. A CMD line means cron did its part and the job itself failed, which is checks 4 to 7.
Why Does the Schedule Not Fire When I Expect?
Five fields, then the command: minute hour day-of-month month day-of-week. Two traps cause most surprises. A line with only four schedule fields is not an error cron reports to you; it simply never runs. And when both day-of-month and day-of-week are restricted, cron runs on days matching either: 0 1 1 * 0 means the 1st of every month and every Sunday, not "the 1st, if it is a Sunday" (man 5 crontab). The systemd timers guide proves that one on this machine, and the Cron Job Builder warns about it as you build.
What Else Breaks a Job That Cron Did Start?
- The script itself. It must exist at the exact absolute path, be executable (
chmod +x), and have Unix line endings: a CRLF shebang fails withbad interpreter. Fix "Permission denied" and Fix bad interpreter cover both. - PATH. cron's
PATHis/usr/bin:/bin. Anything in/usr/local/bin,~/.local/binor/snap/biniscommand not found, exit 127. Use full paths or aPATH=line at the top of the crontab; Fix "bash: command not found" reproduces it. %. cron turns an unescaped%into a newline and feeds the rest of the line to stdin, sodate +%Fbreaks. Write\%, or put the command in a script.- The environment. No
~/.bashrc, no exported variables,/bin/shunlessSHELL=is set, working directory$HOME. Bash Environment Variables explains what a child process inherits and what cron does not give it. - Overlap. A job slower than its schedule starts a second copy on top of the first.
flock -nskips the overlapping run;timeoutstops a hung one. flock and timeout have the patterns.
Where Did the Error Message Go?
cron mails a job's output to its owner. With no mail transfer agent installed, which is the default on most desktops and many cloud images, it cannot, so it discards the output and logs one line. This machine has no MTA, and between 2026-09-09 (the oldest journal entry) and 2026-10-03 cron logged this 117 times, every one a job's output, possibly an error, thrown away:
(Hostname and PID replaced.) The fix costs nothing: end every job with >> /var/log/jobname.log 2>&1, so stdout and stderr land in a file. For alerts on failure, Send Slack Alerts From Bash or Send an Email Alert.
The Script
Save as cron-doctor.sh. It checks the daemon and mail delivery on this machine, then every job line of a crontab (yours by default, or a file you pass) for the problems above.
Prerequisites
bash 4 or later and systemd for the daemon check (on systems without it, that check reports no daemon; read the rest). journalctl access to the cron unit needs membership of adm or systemd-journal, or sudo. No root otherwise.
What Does the Script Print?
Against a demonstration crontab with one healthy job and one example of each problem. The scripts live in a scratch directory, shown as /opt/jobs:
The first two lines are this machine's real state: cron is running, there is no MTA, and 58 job outputs were discarded in the past week. Line 3 is the healthy job. Running it with no argument checks your own crontab the same way. It exits 1 when it finds anything, so it can run from cron itself and write its report to a log.
How Do I Stop It Happening Again?
Absolute paths, PATH= at the top of the crontab, \% for every percent sign, >> log 2>&1 on every line, and flock -n plus timeout around anything that can run long. Or move the job to a systemd timer, which logs to the journal, catches up runs missed while the machine was off, and never overlaps itself; the Cron Job Builder exports any crontab line as a timer. The Production Bash Toolkit ships a cron wrapper that does the flock, timeout and logging for you.
Frequently Asked Questions
How do I check if a cron job ran?
Look in the cron log. On systemd distros, journalctl -u cron (Debian, Ubuntu) or journalctl -u crond (Fedora, RHEL) shows one CMD line for every command cron started, with the user and the time; on older systems grep CRON /var/log/syslog or /var/log/cron. A CMD line proves cron started the job. It says nothing about whether the job succeeded, which is why every job should also write its own log.
Why does my cron job work manually but not in cron?
cron runs jobs with a minimal environment: PATH=/usr/bin:/bin, no ~/.bashrc, no exported variables, /bin/sh instead of bash unless SHELL= is set, and your home directory as the working directory. Commands installed in /usr/local/bin, ~/.local/bin or /snap/bin fail with command not found, and relative paths point somewhere else. Use absolute paths, set PATH= at the top of the crontab, and test with env -i PATH=/usr/bin:/bin HOME=$HOME /bin/sh -c 'your command'.
Why does % break my cron job?
In a crontab line, an unescaped % ends the command and turns the rest into standard input, so tar czf backup-$(date +%F).tgz runs as tar czf backup-$(date + with the remainder fed to stdin. Escape every % as \% inside the crontab, or move the command into a script file, where % is an ordinary character.
Where does cron output go?
cron mails anything a job prints to the crontab's owner, or to MAILTO if set. If no mail transfer agent such as postfix or exim is installed, the mail cannot be sent and cron throws the output away, logging (CRON) info (No MTA installed, discarding output). Redirect every job explicitly with >> /var/log/job.log 2>&1, so errors land in a file you can read, or route them to an alert script.
Do I need to restart cron after editing crontab?
No. crontab -e installs the new crontab and cron checks for changes every minute, so the edit takes effect within a minute. A restart is only needed if you edited files under /etc/cron.d with a tool that did not update the file's modification time. If a new job does not run, check that the minute you scheduled has actually passed and look for its CMD line in the log.
Part of the bash snippets collection
Related Scripts
- Prevent Overlapping Cron Runs With flock — the overlap fix
- The timeout Command — the hang fix
- Fix "bash: command not found" — the PATH cause, reproduced
- Bash Environment Variables — what cron's environment lacks