# Cron Job Not Running? Check These 9 Causes

By Dan Ochoa · Published 2026-07-09 · Updated 2026-10-24
Canonical: https://cronradar.com/blog/cron-job-not-running

> Debug a cron job that works manually but not on schedule: the cron service, user crontabs, syntax, % signs, PATH, permissions, SELinux, and logs.

**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:

```bash
systemctl status cron    # Debian/Ubuntu
systemctl status crond   # RHEL/CentOS/Fedora
```

If 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:

```bash
whoami
crontab -l
sudo crontab -l

# Replace appuser with the account that should run this job.
sudo crontab -u appuser -l
```

A 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](https://manpages.debian.org/trixie/cron/crontab.1.en.html); the [crontab guide](https://cronradar.com/blog/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:

```text
# 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.sh
```

Putting `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](https://cronradar.com/blog/crontab-errors) lists what each cron prints or logs when a line or the final newline is wrong. See [crontab(5)](https://man7.org/linux/man-pages/man5/crontab.5.html). For the five time fields themselves, see [cron expression syntax](https://cronradar.com/blog/cron-expression). Cron matches them against the daemon's time zone, which may not be the one in your shell; [cron timezone](https://cronradar.com/blog/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)](https://man7.org/linux/man-pages/man5/crontab.5.html)). Cron does this before the shell sees the line, so shell quotes don't protect a `%`. A `date` format is the usual casualty:

```text
# 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>&1
```

The 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](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/do_command.c#L165-L224), [Debian do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L186-L256)).

BusyBox crond, the cron in Alpine, passes `%` to the shell unchanged ([crond.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/miscutils/crond.c#L578-L579)). An unquoted `\%` works there too, because the shell drops the backslash ([POSIX shell](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_02_01)). 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](https://manpages.debian.org/trixie/cron/cron.8.en.html). [Cron environment variables](https://cronradar.com/blog/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.

```bash
# 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/app
```

Each 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)](https://man7.org/linux/man-pages/man8/mount.8.html)). Running a script there by path fails with `EACCES` ([execve(2)](https://man7.org/linux/man-pages/man2/execve.2.html)): the shell reports `Permission denied` and exits with 126, the status for a command that was found but couldn't be executed ([POSIX](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_08_02)). `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)](https://man7.org/linux/man-pages/man8/findmnt.8.html)):

```bash
findmnt -T /opt/app/backup.sh -o TARGET,OPTIONS
```

If 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](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/crontab.c#L170-L177), [Debian crontab.c](https://sources.debian.org/src/cron/3.0pl1-197/crontab.c/#L120-L134)), 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](https://cronradar.com/blog/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](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/security.c#L483-L588), [user.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/user.c#L111-L171)). 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](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/using_selinux/troubleshooting-problems-related-to-selinux)). Look for the evidence, then restore the default label:

```bash
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-backup
```

`ausearch` 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](https://ubuntu.com/server/docs/how-to/security/apparmor/)). 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)](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html)):

```bash
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](https://cronradar.com/blog/macos-cron-launchd) 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"](https://cronradar.com/blog/cron-no-mta-installed)). Redirecting output to a writable application log is another way to keep diagnostic evidence:

```bash
0 2 * * * /opt/app/backup.sh >> /var/log/app/backup.log 2>&1
```

## A 60-Second Diagnostic Checklist

1. `systemctl status cron` — is the daemon up?
2. `crontab -l` (and `sudo crontab -l`, `/etc/crontab`, `/etc/cron.d/`) — is the entry there, under the right user?
3. Five time fields, plus a username only in system crontabs?
4. Every `%` in the command escaped as `\%`, or the command moved into a script?
5. Absolute paths for interpreter, script, and files, and `PATH=` set or absolute binaries used?
6. Correct owner and access to interpreter, inputs, and outputs, on a mount without `noexec`?
7. No SELinux or AppArmor denials at the time of the run?
8. Output redirected to a log file?
9. 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](https://cronradar.com/blog/how-to-know-if-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](https://cronradar.com/blog/cron-dead-mans-switch).

[How to monitor cron jobs](https://cronradar.com/automation-monitoring) shows a shell wrapper that preserves the job's exit status when reporting fails. Follow the [setup guide](https://docs.cronradar.com/getting-started) to connect one real job and verify its next scheduled run.

Working with a coding assistant? Use the [agent quickstart](https://docs.cronradar.com/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](https://cronradar.com/blog/how-to-know-if-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.
