Troubleshooting 'cron not working': Fixing Permission Denied, Crashes, and 504 Errors
Master Linux cron troubleshooting. Fix 'cron not working', permission denied errors, and daemon crashes by mastering environment variables, paths, and logs.
- The most common cause of 'cron not working' is the restricted environment trap; cron does not load your user's full PATH or environment profile.
- 'Permission denied' errors typically stem from missing executable flags (+x) on scripts, incorrect file ownership, or restrictive SELinux/AppArmor policies.
- A 'cron 504' error occurs when triggering webhooks via curl/wget that take too long; switch to direct CLI execution to bypass web server gateway timeouts.
- Crond crashes are rare but usually indicate Out-Of-Memory (OOM) killer interventions or severely malformed system crontab files.
- Always capture both stdout and stderr in your crontab definitions (e.g., `>> /var/log/myjob.log 2>&1`) to expose silent script failures.
| Method | When to Use | Time | Risk |
|---|---|---|---|
| Check syslog / cron logs | Initial diagnosis to see if crond actually attempted to run the schedule | 2 mins | None |
| Add Output Redirection (2>&1) | When the job runs but silently fails without leaving traces in system logs | 5 mins | Low |
| Env Dump (env > /tmp/env.log) | When a script works perfectly in your terminal but fails in the crontab | 5 mins | None |
| Bypass Webserver (CLI execution) | When experiencing 'cron 504' gateway timeouts via wget or curl web cron | 15 mins | Medium |
Understanding the 'Cron Not Working' Phenomenon
When a scheduled job fails to execute on a Linux system, the frustration is compounded by cron's inherently silent nature. Unlike terminal commands that immediately output errors to standard output (stdout), cron runs as a background daemon (crond). If a job fails, the error is either emailed to the local user's mail spool (which is often unconfigured or ignored) or lost entirely if output redirection isn't strictly enforced.
The overarching complaint of "cron not working" usually breaks down into four specific failure domains: the restricted environment trap, permission denied errors, HTTP gateway timeouts (cron 504), and actual daemon crashes. Understanding which domain your failure falls into is the first step to resolution.
Symptom 1: The Restricted Environment Trap
The absolute most common reason a script works flawlessly when you run it as ./backup.sh but fails in cron is the environment. When you log in via SSH, your shell reads .bashrc, .bash_profile, or .profile, setting up a robust $PATH that knows exactly where node, python3, aws, or mysql live.
Cron does not do this. Cron executes with a highly minimal environment. By default, the cron $PATH is usually just /usr/bin:/bin. If your script relies on a binary located in /usr/local/bin or ~/.nvm/versions/..., cron will silently fail with a command not found error.
Symptom 2: Cron Permission Denied
When investigating logs, you might encounter (CRON) error (can't fork) or direct script output reading /bin/sh: 1: /path/to/script.sh: Permission denied. This occurs under a few specific conditions:
- Missing Executable Bit: The script lacks the
+xchmod flag. - Wrong User: A user is trying to run a crontab that touches files owned by
root, or vice versa. - Security Modules: SELinux (on RHEL/CentOS) or AppArmor (on Debian/Ubuntu) is blocking the cron daemon from accessing specific directories, even if standard Linux file permissions technically allow it.
- Cron Allow/Deny: The user is not listed in
/etc/cron.allowor is explicitly blocked in/etc/cron.deny.
Symptom 3: Cron 504 Gateway Timeouts
A "cron 504" error is technically a web server error, not a cron daemon error. It happens when developers use cron to trigger a web-based script via HTTP, commonly structured like this:
* * * * * curl -s https://example.com/api/run-heavy-background-job
If the job takes longer than the web server's configured timeout (typically 30 to 60 seconds for Nginx or Apache), the web server drops the connection and returns a 504 Gateway Timeout. Cron dutifully records the 504 response, but the underlying script execution may have been killed by PHP-FPM or Gunicorn.
Symptom 4: Cron Crash or Unresponsive Daemon
While rare, the crond process can crash or hang. This is almost always caused by the Linux Out-Of-Memory (OOM) killer terminating the daemon to save the system, or a massive buildup of zombie processes from previously hung cron jobs that exhausted the system's Process ID (PID) table.
Step 1: Diagnose the Exact Failure Mode
Stop guessing and start measuring. Follow these diagnostic steps sequentially to force cron to reveal its secrets.
1. Check the Cron Service Status
Ensure the daemon is actually running and hasn't crashed.
systemctl status cron # On Ubuntu/Debian
systemctl status crond # On RHEL/CentOS/Amazon Linux
Look for Active: active (running). If it says failed, check the journal for OOM killer messages (dmesg -T | grep -i oom).
2. Inspect the System Logs
Cron logs its scheduled invocations. It won't tell you if the script succeeded, but it will tell you if it attempted to run it.
grep CRON /var/log/syslog # Ubuntu/Debian
grep CRON /var/log/cron.log # Alternative Ubuntu/Debian
sudo tail -f /var/log/cron # RHEL/CentOS
If you see (user) CMD (/path/to/script.sh), cron is working. The failure is in your script or permissions.
3. Implement Strict Output Redirection
Modify your crontab (crontab -e) to capture everything. Append this to the end of your failing job:
>> /tmp/cron_debug.log 2>&1
Wait for the next scheduled run, then cat /tmp/cron_debug.log. You will likely see command not found or Permission denied.
Step 2: Implement the Fixes
Fix 1: Resolving Environment & PATH Issues
Never rely on the system PATH in a cron job. You have two robust solutions:
Option A: Use Absolute Paths for Everything
Inside your script, do not write python3 script.py. Write /usr/bin/python3 /absolute/path/to/script.py. Replace curl with /usr/bin/curl.
Option B: Define PATH in the Crontab
Open your crontab (crontab -e) and define the environment variables explicitly at the top of the file, before any cron schedules:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
* * * * * /path/to/script.sh >> /var/log/script.log 2>&1
Fix 2: Curing "Permission Denied"
First, make the script executable:
chmod +x /path/to/your/script.sh
Second, ensure the script starts with a valid shebang (e.g., #!/bin/bash or #!/usr/bin/env python3) on the very first line. Without a shebang, cron doesn't know which interpreter to invoke, often resulting in execution failures.
Third, verify the user context. If your script needs to restart Nginx, it must run as root. Do not put it in your user crontab (crontab -e). Instead, place it in the system crontab (sudo crontab -e) or drop a file in /etc/cron.d/.
Fix 3: Eliminating "Cron 504" Gateway Timeouts
Stop using HTTP requests (curl or wget) to trigger heavy internal background jobs. Web servers are designed for fast request/response cycles, not long-running data processing.
Instead of: * * * * * curl https://mysite.com/artisan/schedule/run
Change it to execute directly via the command line interface (CLI):
* * * * * /usr/bin/php /var/www/mysite/artisan schedule:run >> /dev/null 2>&1
By executing via CLI, you bypass Nginx/Apache entirely. There are no web timeouts, and the script can run for hours if necessary.
Fix 4: Recovering from a Cron Crash
If systemctl status cron shows the daemon is dead, find out why. Run:
journalctl -u cron -n 50 --no-pager
If you see crond: can't open or parse /etc/crontab, you have a syntax error in your system crontab file. Remember that /etc/crontab requires a user field that user crontabs do not.
Incorrect User Crontab:
* * * * * root /script.sh (Fails: 'root' is treated as a command)
Correct System Crontab (/etc/crontab):
* * * * * root /script.sh (Works: requires the user field)
If the crash was due to OOM, you need to implement locking in your script to prevent overlapping executions. Use flock to ensure only one instance of a heavy cron job runs at a time:
* * * * * /usr/bin/flock -n /tmp/myjob.lock /path/to/script.sh
Frequently Asked Questions
#!/bin/bash
# Cron Diagnostic Script - Run this to find common cron configuration issues
CRON_SCRIPT="/path/to/your/script.sh"
echo "--- Checking Cron Service Status ---"
if command -v systemctl >/dev/null 2>&1; then
systemctl status cron 2>/dev/null || systemctl status crond 2>/dev/null | grep "Active:"
else
service cron status 2>/dev/null || service crond status 2>/dev/null
fi
echo -e "\n--- Checking Script Permissions ---"
if [ -f "$CRON_SCRIPT" ]; then
ls -la "$CRON_SCRIPT"
if [ -x "$CRON_SCRIPT" ]; then
echo "[OK] Script is executable."
else
echo "[ERROR] Script is NOT executable. Run: chmod +x $CRON_SCRIPT"
fi
else
echo "[ERROR] Script file does not exist at $CRON_SCRIPT"
fi
echo -e "\n--- Checking Recent Cron Logs ---"
if [ -f /var/log/syslog ]; then
grep CRON /var/log/syslog | tail -n 5
elif [ -f /var/log/cron ]; then
tail -n 5 /var/log/cron
else
echo "Cron logs not found in standard locations."
fi
echo -e "\n--- Recommended Crontab Format for Debugging ---"
echo "* * * * * $CRON_SCRIPT >> /tmp/cron_debug.log 2>&1"Error Medic Editorial
Error Medic Editorial is managed by a team of Senior Site Reliability Engineers (SREs) and DevOps architects dedicated to demystifying complex Linux server issues, container orchestration, and system administration.