Error Medic

Cron Not Working? Fix Permission Denied, Crashes, and 504 Errors on Linux

Cron not working on Linux? Step-by-step fixes for cron permission denied, cron crashes, and cron 504 timeout errors. Includes real diagnostic commands and root

Last updated:
Last verified:
2,374 words
Key Takeaways
  • Cron jobs fail silently by default — always redirect output with >> /var/log/job.log 2>&1 to capture both stdout and stderr from every job.
  • PATH mismatch is the #1 root cause: cron only provides /usr/bin:/bin, so tools in /usr/local/bin or ~/.local/bin are invisible. Fix by adding PATH= at the top of your crontab or using absolute command paths.
  • Permission denied errors stem from a missing execute bit (chmod +x), wrong file ownership for the cron user, or SELinux/AppArmor policies blocking execution.
  • Cron 504 errors occur when a cron job triggers an HTTP endpoint that exceeds the reverse proxy timeout — replace curl with a direct CLI command to eliminate the HTTP layer entirely.
  • Quick diagnostic sequence: verify the daemon is running with systemctl status cron, then read the last 30 cron log entries with grep CRON /var/log/syslog | tail -30.
Fix Approaches Compared
MethodWhen to UseTime to ApplyRisk
Set PATH= at top of crontabCommands work in shell but not found in cron2 minLow
chmod +x on script fileScript missing execute bit, permission denied on file30 secLow
Redirect output to log fileJob runs silently with no visible error output2 minLow
Replace curl with CLI commandCron 504 gateway timeout on HTTP-triggered jobs10 minMedium
Increase Nginx proxy_read_timeoutHTTP-triggered cron jobs timing out at proxy layer5 minLow
restorecon or chcon for SELinuxAVC denial blocking cron execution on SELinux systems5 minMedium
Restart crond via systemctlCron daemon crashed or in a failed/inactive state1 minLow

Understanding Why Cron Jobs Fail

Cron is deceptively simple — define a schedule and a command, and it runs. Yet "cron not working" is one of the most searched Linux administration problems on the internet. Root causes fall into five buckets: environment mismatches, permission problems, missing output capture, cron daemon failures, and crontab syntax errors.

The single most important concept every Linux admin must internalize: cron does not inherit your interactive shell environment. This one fact explains the overwhelming majority of failures.

The Cron Environment vs. Your Shell

When you SSH into a server, your shell sources .bashrc, .bash_profile, and /etc/profile, building a rich PATH like:

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/user/.local/bin

Cron's default PATH is:

/usr/bin:/bin

So python3, node, aws, docker, kubectl, and anything installed under /usr/local/bin or ~/.local/bin silently fails in cron unless you define PATH explicitly in the crontab.


Step 1: Verify the Cron Daemon Is Running

Before chasing script or permission issues, confirm the cron daemon is alive:

# systemd-based systems (Ubuntu 16.04+, Debian 8+, RHEL 7+, CentOS 7+)
systemctl status cron        # Debian/Ubuntu
systemctl status crond       # RHEL/CentOS/Fedora

# Verify a cron process is running
pgrep -a cron

A crashed cron daemon outputs:

● cron.service - Regular background program processing daemon
   Active: failed (Result: exit-code) since Mon 2026-02-23 10:14:22 UTC; 3min ago

Restart and investigate the crash cause:

sudo systemctl restart cron          # Debian/Ubuntu
sudo systemctl restart crond         # RHEL/CentOS
journalctl -u cron --since "1 hour ago" -n 50

Step 2: Read the Cron Logs

Cron logs every execution attempt to syslog. Examine these before anything else:

# Ubuntu/Debian
grep CRON /var/log/syslog | tail -30

# RHEL/CentOS
grep CRON /var/log/cron | tail -30

# Any systemd distribution
journalctl -u cron --since "today"

What the log entries mean:

  • (user) CMD (/path/to/script) — Job was triggered. No entry at the expected time means wrong schedule or daemon is down.
  • (CRON) error (can't fork) — System resource exhaustion or process limit reached.
  • (user) FAILED to open PAM security session (Permission denied) — PAM or NSS failure, common on LDAP/Active Directory systems.
  • Nothing at all at the expected time — Wrong crontab syntax, job is in the wrong user's crontab, or the daemon is not running.

Step 3: Fix Permission Denied Errors

There are three distinct scenarios that produce a permission denied error in cron:

Scenario A: Script missing the execute bit

# Diagnose
ls -la /home/user/backup.sh
# -rw-r--r-- 1 user user 1024 Feb 23 09:00 backup.sh  <- no execute bit

# Fix
chmod +x /home/user/backup.sh
# Or more restrictively:
chmod 750 /home/user/backup.sh

Scenario B: Script owned by the wrong user

Cron executes jobs as the owner of the crontab. If root's crontab calls a script owned and readable only by www-data, it will be denied:

# Check ownership
stat /var/www/html/cron.sh

# Fix: align ownership with the cron user
sudo chown root:root /var/www/html/cron.sh
chmod 755 /var/www/html/cron.sh

Scenario C: SELinux or AppArmor blocking execution

# Check for SELinux AVC denials involving cron
sudo ausearch -m avc -ts recent 2>/dev/null | grep cron

# Restore default security context
sudo restorecon -v /home/user/backup.sh

# Or apply a specific type context manually
sudo chcon -t bin_t /home/user/backup.sh

# AppArmor on Ubuntu
sudo dmesg | grep -i apparmor | tail -10

Step 4: Resolve PATH and Environment Issues

Method 1 — Set PATH globally at the top of crontab:

# Open with: crontab -e
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1

Method 2 — Use absolute paths inside the script:

#!/bin/bash
# Find absolute paths in your shell first:
#   which python3  =>  /usr/bin/python3
#   which aws      =>  /usr/local/bin/aws

/usr/bin/python3 /var/www/html/manage.py collectstatic
/usr/local/bin/aws s3 sync /backup s3://my-bucket/ --delete

Method 3 — Source the user profile before executing:

# In crontab
0 2 * * * . /home/user/.bash_profile && /home/user/backup.sh >> /var/log/backup.log 2>&1

Step 5: Always Capture Output

Without output redirection, cron silently discards all stdout and stderr (unless MAILTO is configured and a mail server is available). Add logging to every job:

# Append stdout and stderr to a log
0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1

# Disable mail delivery and log locally
MAILTO=""
0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1

# Add timestamps to each run in the log
0 2 * * * date >> /var/log/backup.log && /home/user/backup.sh >> /var/log/backup.log 2>&1

Step 6: Diagnose and Fix Cron 504 Errors

Cron 504 errors appear when a cron job triggers an HTTP endpoint — common patterns include Laravel scheduler URLs, WordPress WP-Cron HTTP requests, and curl-based webhook triggers — and the request duration exceeds the reverse proxy's read timeout.

Typical error in the cron log file:

curl: (28) Operation timed out after 30000 milliseconds with 0 bytes received

Or in the web server access log:

GET /wp-cron.php?doing_wp_cron HTTP/1.1 504 0 - 30.002

Fix A — Replace HTTP trigger with a direct CLI call (recommended):

# BEFORE (fragile — subject to proxy timeouts, HTTP stack issues)
* * * * * /usr/bin/curl -s https://example.com/cron/run > /dev/null 2>&1

# AFTER — Laravel: run artisan scheduler directly
* * * * * cd /var/www/html && /usr/bin/php artisan schedule:run >> /var/log/laravel-cron.log 2>&1

# AFTER — WordPress: disable HTTP cron, use system cron
# Step 1: add to wp-config.php:  define('DISABLE_WP_CRON', true);
# Step 2: add to crontab:
*/15 * * * * /usr/bin/php /var/www/html/wp-cron.php >> /var/log/wp-cron.log 2>&1

Fix B — Increase Nginx proxy read timeout:

# /etc/nginx/sites-available/your-site
location / {
    proxy_pass         http://127.0.0.1:8080;
    proxy_read_timeout 300s;
    proxy_connect_timeout 75s;
    proxy_send_timeout 300s;
}

Fix C — Fire and forget (cron does not block waiting for response):

* * * * * /usr/bin/curl -s https://example.com/cron/run > /dev/null 2>&1 &

Step 7: Validate Crontab Syntax

Silent syntax errors permanently prevent a job from running:

# WRONG: @reboot combined with time fields
@reboot 0 2 * * * /home/user/script.sh   # Error: extra fields after @reboot

# WRONG: out-of-range hour (valid: 0-23)
0 25 * * * /home/user/script.sh

# WRONG: out-of-range month (valid: 1-12)
0 2 * 13 * /home/user/script.sh

# CORRECT
0 2 * * * /home/user/script.sh
@reboot /home/user/startup.sh

Use https://crontab.guru to validate cron time expressions interactively before deploying.


Step 8: Reproduce the Failure as the Cron User

The definitive debugging technique is to run the job with exactly the same minimal environment cron uses:

# Replace www-data with your actual cron user
CRON_HOME=$(eval echo ~www-data)
sudo -u www-data env -i \
  HOME="$CRON_HOME" \
  PATH=/usr/bin:/bin \
  SHELL=/bin/bash \
  bash -c '/var/www/html/cron.sh >> /tmp/cron-test.log 2>&1'

cat /tmp/cron-test.log

If the job fails here, you have reproduced the exact failure without waiting for the cron schedule. Iterate — fix PATH, permissions, or missing dependencies — until this command exits cleanly, then update your crontab or script accordingly.

Frequently Asked Questions

bash
#!/bin/bash
# cron-diagnose.sh — Comprehensive cron troubleshooting script
# Usage: bash cron-diagnose.sh [/path/to/script.sh] [cron-user]

SCRIPT_PATH="${1:-}"
CRON_USER="${2:-$(whoami)}"
SEP="================================================"

echo "$SEP"
echo "STEP 1: Cron Daemon Status"
echo "$SEP"
if command -v systemctl >/dev/null 2>&1; then
    systemctl status cron 2>/dev/null || systemctl status crond 2>/dev/null
else
    service cron status 2>/dev/null || service crond status 2>/dev/null
fi

echo ""
echo "$SEP"
echo "STEP 2: Recent Cron Log Entries (last 20)"
echo "$SEP"
if [ -f /var/log/syslog ]; then
    grep CRON /var/log/syslog | tail -20
elif [ -f /var/log/cron ]; then
    tail -20 /var/log/cron
else
    journalctl -u cron --since "2 hours ago" 2>/dev/null | tail -20 || \
    journalctl -u crond --since "2 hours ago" 2>/dev/null | tail -20
fi

echo ""
echo "$SEP"
echo "STEP 3: Current User Crontab"
echo "$SEP"
crontab -l 2>/dev/null || echo "No crontab for $(whoami)"

echo ""
echo "$SEP"
echo "STEP 4: System-wide Cron Jobs (/etc/crontab and /etc/cron.d/)"
echo "$SEP"
ls -la /etc/cron.d/ 2>/dev/null
head -30 /etc/crontab 2>/dev/null

echo ""
echo "$SEP"
echo "STEP 5: PATH Tool Availability Check"
echo "$SEP"
echo "Interactive PATH : $PATH"
echo "Cron default PATH: /usr/bin:/bin"
echo ""
echo "Tools NOT in cron default PATH that are in your shell PATH:"
for cmd in python3 python node npm ruby aws docker kubectl terraform ansible; do
    cmd_path="$(command -v "$cmd" 2>/dev/null)"
    if [ -n "$cmd_path" ] && [[ "$cmd_path" != /usr/bin/* ]] && [[ "$cmd_path" != /bin/* ]]; then
        echo "  [WARN] $cmd -> $cmd_path (must set PATH in crontab or use absolute path)"
    fi
done

if [ -n "$SCRIPT_PATH" ] && [ -f "$SCRIPT_PATH" ]; then
    echo ""
    echo "$SEP"
    echo "STEP 6: Script Permissions: $SCRIPT_PATH"
    echo "$SEP"
    stat "$SCRIPT_PATH"
    echo ""
    sudo -u "$CRON_USER" test -x "$SCRIPT_PATH" 2>/dev/null && \
        echo "[OK]   $CRON_USER can execute $SCRIPT_PATH" || \
        echo "[FAIL] $CRON_USER cannot execute $SCRIPT_PATH — fix: chmod +x $SCRIPT_PATH"

    echo ""
    echo "$SEP"
    echo "STEP 7: Run Script as Cron User with Minimal Environment"
    echo "$SEP"
    LOG=/tmp/cron-diagnose-output.log
    CRON_HOME=$(eval echo ~"$CRON_USER")
    sudo -u "$CRON_USER" env -i \
        HOME="$CRON_HOME" \
        PATH=/usr/bin:/bin \
        SHELL=/bin/bash \
        bash "$SCRIPT_PATH" > "$LOG" 2>&1
    EXIT_CODE=$?
    echo "Exit code: $EXIT_CODE"
    echo "Output (first 50 lines):"
    head -50 "$LOG"
    echo "Full output saved to: $LOG"
fi

echo ""
echo "$SEP"
echo "STEP 8: SELinux / AppArmor Status"
echo "$SEP"
if command -v getenforce >/dev/null 2>&1; then
    echo "SELinux mode: $(getenforce)"
    echo "Recent cron AVC denials:"
    ausearch -m avc -ts recent 2>/dev/null | grep -i cron | tail -5 || \
        echo "  None found (ausearch not available or no recent denials)"
elif command -v aa-status >/dev/null 2>&1; then
    echo "AppArmor status:"
    sudo aa-status 2>/dev/null | head -10
else
    echo "Neither SELinux nor AppArmor tools detected on this system"
fi

echo ""
echo "Diagnostic complete. Review any [FAIL] or [WARN] lines above."
echo "If a script path was provided, review $LOG for the cron-environment output."
E

Error Medic Editorial

Error Medic Editorial is the technical writing team at Error Medic, composed of senior SRE engineers, Linux system administrators, and DevOps practitioners with over a decade of production operations experience across cloud and on-premise Linux environments. The team specializes in translating complex system debugging scenarios into actionable, reproducible solutions.

Sources

Related Articles in Cron

Explore More Linux Sysadmin Guides