Cron Job Not Running? Check These 9 Causes
Updated
Contents
If a cron job is not running, first check whether cron itself is running, which user's crontab holds the job, and where the command writes its output. A command that works in your terminal can fail under cron because it runs with a different user, working directory, shell, or environment.
Start with the read-only checks below, in order. Examples use common Linux cron implementations; service names and logging differ by distribution.
The 9 Most Common Causes#
1. Cron Isn't Running#
Before debugging your job, confirm the daemon is alive:
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS/FedoraIf it is inactive, inspect its recent logs before starting or restarting it. If the service is absent, check whether the deployment uses a different scheduler, such as a systemd timer or Kubernetes CronJob. A container containing a crontab file does not by itself prove cron is running.
2. The Job Is in a Different User's Crontab#
crontab -l shows the current user's jobs, and crontab -e edits them. sudo crontab -l shows root's. Neither combines every schedule on the server:
whoami
crontab -l
sudo crontab -l
# Replace appuser with the account that should run this job.
sudo crontab -u appuser -lA message saying one user has no crontab does not mean the server has no scheduled jobs. System schedules can also live in /etc/crontab and /etc/cron.d/. See the crontab command reference; the crontab guide has a read-only script that lists every schedule on a host.
If the same script is in root's and an admin user's crontab, those are independent schedules. Both can run, with different permissions and environments. One may succeed while the other fails; they may also overlap or compete for an output file. Compare both entries and their logs. Choose the account that should own the task, verify its access, and remove an unintended duplicate only after confirming it is redundant. Running everything as root is not a general fix for a permissions problem.
3. Crontab Syntax#
User crontabs have five time fields followed by the command. System crontabs add a username after the time fields:
# User crontab, installed with crontab -e:
0 2 * * * /opt/app/backup.sh
# System entry in /etc/crontab or /etc/cron.d/app-backup:
0 2 * * * appuser /opt/app/backup.shPutting appuser into a user crontab makes it part of the command. Omitting it from a system entry shifts the fields into the wrong positions. Check the final newline too; crontab error messages lists what each cron prints or logs when a line or the final newline is wrong. See crontab(5). For the five time fields themselves, see cron expression syntax. Cron matches them against the daemon's time zone, which may not be the one in your shell; cron timezone shows how to check it and covers each implementation's daylight saving behavior.
4. An Unescaped % in the Command#
cronie and Debian's cron end the command at the first unescaped %. Everything after it goes to the command's standard input, with any further % turned into a newline (crontab(5)). Cron does this before the shell sees the line, so shell quotes don't protect a %. A date format is the usual casualty:
# Broken: the shell receives only "/opt/app/backup.sh > /var/log/app/backup-$(date +"
0 2 * * * /opt/app/backup.sh > /var/log/app/backup-$(date +%F).log 2>&1
# Fixed: cron turns \% into a literal % before the shell runs the command
0 2 * * * /opt/app/backup.sh > /var/log/app/backup-$(date +\%F).log 2>&1The cut-off command is a shell syntax error, so backup.sh never starts, and the error message goes to cron's mail. Cron's log gives it away: the CMD line records the command after the cut, ending in $(date + (cronie do_command.c, Debian do_command.c).
BusyBox crond, the cron in Alpine, passes % to the shell unchanged (crond.c). An unquoted \% works there too, because the shell drops the backslash (POSIX shell). The fix that works everywhere is to move the command into a script, where cron never sees the % signs.
5. Relative Paths#
Do not assume cron starts in the directory you used in your terminal. Use the deployed interpreter and script paths, and set a working directory when the application needs one: cd /opt/app && /opt/app/.venv/bin/python /opt/app/backup.py.
6. Environment and PATH#
Cron does not reproduce your interactive terminal session. Shell profiles, aliases, activated virtual environments, and credentials may be absent. Set the required PATH or use absolute binary paths, and load secrets through the application's existing protected configuration. Check whether a required variable is present without logging its value or the entire environment.
On Debian, /etc/cron.d/ entries do not inherit environment settings from /etc/crontab. See Debian cron behavior. Cron environment variables lists what each implementation puts in a job's environment and shows how to run a command with exactly that environment.
7. Permissions, noexec Mounts, and cron.allow#
A directly executed script needs execute permission and a valid interpreter line. A script passed to an interpreter, such as /bin/bash /opt/app/backup.sh, needs to be readable; the interpreter must be executable. The account also needs access to parent directories, configuration, inputs, and outputs. Failure to open the redirected log file can prevent the command from starting.
# Inspect access without executing the backup. Replace account and paths.
sudo -u appuser test -r /opt/app/backup.sh
sudo -u appuser test -x /opt/app/backup.sh
sudo -u appuser test -w /var/log/appEach check returns zero when the condition is true. Fix the specific ownership or access problem rather than applying broad recursive permissions.
The execute bit isn't enough on a filesystem mounted noexec, which doesn't "permit direct execution of any binaries" (mount(8)). Running a script there by path fails with EACCES (execve(2)): the shell reports Permission denied and exits with 126, the status for a command that was found but couldn't be executed (POSIX). sh backup.sh still works, because the interpreter only reads the file, so a script you tested that way can fail under cron. Find the mount the script is on (findmnt(8)):
findmnt -T /opt/app/backup.sh -o TARGET,OPTIONSIf the options include noexec, move the script to a filesystem mounted without it.
/etc/cron.allow and /etc/cron.deny decide who may run the crontab command. A user they exclude gets You (alice) are not allowed to use this program (crontab), and nothing is installed (cronie crontab.c, Debian crontab.c), which is easy to miss when a deploy script installs the crontab. On Debian and Ubuntu, root can't install one for that user either: crontab -u alice prints The user alice cannot use this program (crontab). cronie checks only the account running the command. Neither daemon consults the files when it runs jobs, so they don't stop a crontab that is already installed. The crontab guide covers the rules and each distribution's defaults.
8. SELinux or AppArmor Denies It#
SELinux and AppArmor can refuse access that file permissions allow, and they record the refusal in the audit or kernel log, not in the job's output.
On Fedora and RHEL with SELinux enforcing, cronie checks each crontab file's SELinux label before loading it. If the label isn't allowed, crond logs Unauthorized SELinux context and FAILED (loading cron table) and skips the whole file (security.c, user.c). A file written in a home directory and moved into /etc/cron.d can carry the wrong label: Red Hat warns that mv from a home directory can leave files labeled user_home_t, a type that "should not exist outside of home directories" (Using SELinux). Look for the evidence, then restore the default label:
journalctl -u crond --since today | grep -i selinux
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR,USER_SELINUX_ERR -ts today
ls -Z /etc/cron.d/app-backup
sudo restorecon -v /etc/cron.d/app-backupausearch reads the audit log, /var/log/audit/audit.log, which is also where denials hit by the job's own commands go. If it finds nothing, the same Red Hat guide covers journalctl -t setroubleshoot and the dontaudit rules that hide some denials.
Ubuntu installs and loads AppArmor by default (Ubuntu Server docs). It confines programs that have a profile, which sudo aa-status lists, so a job that runs one of them can be refused a file its account can read. Denials appear in the kernel log with apparmor="DENIED" and the profile's name. journalctl -k shows kernel messages from the current boot (journalctl(1)):
sudo journalctl -k --since today | grep 'apparmor="DENIED"'On a Mac, privacy protections can refuse a scheduled job access to folders such as Documents with Operation not permitted; cron on macOS covers the fixes and launchd.
9. It Ran — and Failed#
Cron can send command output by mail when a mail transport and recipient are configured, but verify that delivery works: without a mail transfer agent, Debian's cron deletes the output and cronie writes it to syslog (cron "No MTA installed"). Redirecting output to a writable application log is another way to keep diagnostic evidence:
0 2 * * * /opt/app/backup.sh >> /var/log/app/backup.log 2>&1A 60-Second Diagnostic Checklist#
systemctl status cron— is the daemon up?crontab -l(andsudo crontab -l,/etc/crontab,/etc/cron.d/) — is the entry there, under the right user?- Five time fields, plus a username only in system crontabs?
- Every
%in the command escaped as\%, or the command moved into a script? - Absolute paths for interpreter, script, and files, and
PATH=set or absolute binaries used? - Correct owner and access to interpreter, inputs, and outputs, on a mount without
noexec? - No SELinux or AppArmor denials at the time of the run?
- Output redirected to a log file?
- Something watching that the job runs at all?
The Blind Spot: A Job That Stops Triggers No Error#
A scheduler invocation proves that cron started the command, not that the backup or report completed. Check the application's exit result and expected output; how to know if a cron job ran compares the ways to do that. For a run that never starts, compare the installed schedule with scheduler logs and recent deployments.
Logs help explain what happened, but something still needs to compare the expected schedule with observed completions. With heartbeat monitoring, the job reports success and the monitor alerts when the next report is overdue. Lifecycle reporting also records starts and explicit failures. This is a dead man's switch.
How to monitor cron jobs shows a shell wrapper that preserves the job's exit status when reporting fails. Follow the setup guide to connect one real job and verify its next scheduled run.
Working with a coding assistant? Use the agent quickstart to grant temporary workspace access and let it prepare monitoring. The deployed job uses its own application key.
Frequently Asked Questions#
How do I check if a cron job ran?#
Look for the invocation in cron's log, redirect the job's output to a log file, and record its exit code. To catch runs that were missed entirely — where no log is written — use external heartbeat monitoring. How to know if a cron job ran compares the four methods.
Why does my cron job work manually but not in crontab?#
Compare the execution user, interpreter, working directory, environment, and file access. Reproduce the command as the same account only when running it again is safe; avoid duplicating a payment or import during diagnosis.
Does cron log anywhere by default?#
Cron logs each command it starts to the system log. Inspect it with journalctl -u cron on Debian and Ubuntu or journalctl -u crond on RHEL and Fedora; hosts running rsyslog may also write it to a file such as /var/log/syslog. Invocation logs and application output are different evidence. Output may be mailed or redirected; check your host's configuration.
Can root and another user both have the same cron job?#
Yes. Their crontabs are independent, so the command can run twice with different permissions and environments. Inspect both schedules and logs before deciding which entry should remain.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.