Recuro.

Cron Job Not Running? Check These 8 Things First

·
Updated March 22, 2026
· Recuro Team
crondebuggingops

Quick Summary — TL;DR

  • First check the basics: is the cron daemon running, and does your cron expression actually match when you think it does?
  • Most cron failures come from environment differences — cron runs with a minimal PATH, no shell profile, and possibly a different timezone than your interactive session.
  • Watch for silent killers: the % character is interpreted as a newline in crontab (escape it as \%), and a missing newline at the end of the crontab file can cause the last entry to be silently ignored.
  • Always redirect stdout and stderr to a log file so failures leave a trace instead of vanishing silently.
  • Use flock to prevent overlapping executions, and check /var/log/syslog or journalctl to confirm cron is even attempting to run your job.
Cron Job Not Running Troubleshooting

You wrote a script. It works perfectly when you run it by hand. You add it to cron, wait for the scheduled time, and nothing happens. No output, no error, no indication that anything went wrong. This is the most common cron complaint, and it almost always comes down to one of nine causes.

1. The cron daemon is not running

Before debugging your job, verify the scheduler itself is alive. The cron daemon is a system service that can be stopped by a reboot, a package update, or a misconfigured init system. For a full guide to managing crontab — editing, listing, and common pitfalls — start there if you’re new to the tooling.

On systemd-based systems (Ubuntu, Debian, CentOS 7+, RHEL):

Terminal window
systemctl status cron
# or on CentOS/RHEL:
systemctl status crond

You should see Active: active (running). If not:

Terminal window
sudo systemctl start cron
sudo systemctl enable cron

On older init systems:

Terminal window
service cron status
# If stopped:
sudo service cron start

On macOS, the equivalent is launchd, but if you’re using crontab -e on macOS, the cron daemon is typically managed automatically. Verify with:

Terminal window
sudo launchctl list | grep cron

If the daemon isn’t running, none of your jobs will fire, no matter how correct the cron expression is.

2. Syntax errors in the cron expression

A malformed expression silently does nothing. If you need a refresher on the five fields, operators, and special strings, see the cron syntax cheat sheet. Common mistakes:

  • Six fields instead of five. Some systems (like Spring or Quartz) use a seconds field. Standard crontab uses five fields only: minute, hour, day-of-month, month, day-of-week.
  • Swapped minute and hour. 0 9 * * * runs at 9:00 AM. 9 0 * * * runs at 12:09 AM. These look similar but fire 9 hours apart.
  • Using names incorrectly. 0 9 * * MON works on some systems but not all. Use numeric values (0 9 * * 1) for portability.
  • Day-of-week numbering. Sunday is 0 on most systems but 7 on others. Both usually work, but check your platform.
  • Missing newline at end of file. Some cron implementations silently ignore the last line of a crontab if it does not end with a newline character. Always make sure there is a blank line after your last cron entry. This is easy to miss with crontab -e and is invisible when you review the file.

Validate your expression before deploying. Paste it into the cron expression explainer to see the exact next run times, or build one from scratch with the cron expression generator. See 25 cron expression examples for tested, copy-paste schedules.

3. Unescaped % characters in the command

The % character has special meaning in crontab: it is interpreted as a newline. Everything after the first unescaped % is fed to the command as stdin, not passed as part of the command line. This breaks commands silently with no error in the cron log.

The most common victim is date formatting:

Terminal window
# Broken — cron sees "date +" and pipes "Y-" as stdin
0 2 * * * /usr/bin/tar czf /backups/db-$(date +%Y-%m-%d).tar.gz /var/lib/mysql
# Fixed — escape every % with a backslash
0 2 * * * /usr/bin/tar czf /backups/db-$(date +\%Y-\%m-\%d).tar.gz /var/lib/mysql

Any % in your cron command must be escaped as \%. This applies everywhere in the command string, not just in datecurl payloads, awk format strings, and anything else that contains a literal percent sign.

If your command has many % characters, put the logic in a shell script and call that from cron instead. Scripts referenced by cron are not subject to crontab’s % parsing:

Terminal window
# The script itself can use % freely — only the crontab line parses %
0 2 * * * /home/app/scripts/backup.sh

This issue does not appear when you test commands in an interactive shell, which makes it especially hard to catch. If your cron log shows the job ran but the output or files are wrong, check for unescaped % characters first.

4. PATH and environment variable issues

This is the single most common reason a script works manually but fails in cron. When you open a terminal, your shell loads ~/.bashrc, ~/.bash_profile, or ~/.zshrc, which set up your PATH, language settings, and other environment variables. Cron does none of this. It runs with a minimal environment, typically just:

SHELL=/bin/sh
PATH=/usr/bin:/bin
HOME=/home/youruser

If your script calls python, node, php, or any command that lives outside /usr/bin:/bin, cron will not find it. The fix is to use absolute paths everywhere:

Terminal window
# Wrong — cron can't find python3
* * * * * python3 /home/app/scripts/sync.py
# Right — absolute path to the interpreter
* * * * * /usr/bin/python3 /home/app/scripts/sync.py

Find the absolute path of any command with which:

/usr/bin/python3
which python3
which node
# /usr/local/bin/node

Alternatively, set PATH at the top of your crontab:

Terminal window
PATH=/usr/local/bin:/usr/bin:/bin
0 * * * * python3 /home/app/scripts/sync.py

If your script depends on other environment variables (API keys, database URLs), define them in the crontab or source them explicitly:

Terminal window
0 * * * * . /home/app/.env && /usr/bin/python3 /home/app/scripts/sync.py

5. Permission problems

Cron runs jobs as the user who owns the crontab. If the script file is not executable or is owned by a different user, it will fail.

Terminal window
# Make the script executable
chmod +x /home/app/scripts/sync.sh
# Verify ownership
ls -la /home/app/scripts/sync.sh

Other permission issues to check:

  • The script writes to a directory the cron user cannot access. A job running as www-data cannot write to /root/logs/.
  • SELinux or AppArmor is blocking execution. Check audit.log or dmesg for denial messages.
  • The crontab was edited by root but should run as a service user. Use crontab -u www-data -e to edit another user’s crontab.

If you put your cron entry in /etc/cron.d/, the file format requires a username field between the schedule and the command:

Terminal window
# /etc/cron.d/myapp — note the username field
0 * * * * www-data /usr/bin/php /var/www/app/artisan schedule:run

Files in /etc/cron.d/ must be owned by root and must not be group-writable or world-writable, or cron will refuse to load them.

6. The script works manually but not in cron

If your script runs fine from an interactive terminal but fails silently in cron, the difference is almost always the environment. Beyond PATH (covered above), watch for:

  • Working directory. Cron does not cd into your project directory. Use absolute paths for all file references, or add an explicit cd at the start of the command:
Terminal window
0 * * * * cd /var/www/app && /usr/bin/php artisan schedule:run
  • Shell differences. Cron uses /bin/sh by default, not bash. If your script uses bash-specific syntax (arrays, [[ ]] tests, process substitution), either set SHELL=/bin/bash in the crontab or add a shebang line:
#!/bin/bash
# Now bash-specific syntax works
  • Missing display server. Scripts that use GUI tools or need a DISPLAY variable will fail in cron. This includes anything that spawns a browser or uses graphical libraries.

  • Virtual environments. Python scripts that depend on a virtualenv need to activate it or use the virtualenv’s Python binary directly:

Terminal window
0 * * * * /home/app/venv/bin/python /home/app/scripts/sync.py

7. Output and error swallowing

By default, cron sends a job’s stdout and stderr to the local mail system via MAILTO. If no mail transfer agent is configured (common on modern servers), that output is silently discarded. Your job could be failing with a clear error message, but you never see it.

Redirect output to a log file:

Terminal window
0 * * * * /usr/bin/python3 /home/app/scripts/sync.py >> /var/log/myapp/sync.log 2>&1

Breaking this down:

  • >> appends stdout to the log file
  • 2>&1 redirects stderr to the same destination

To capture stdout and stderr separately:

Terminal window
0 * * * * /usr/bin/python3 /home/app/scripts/sync.py >> /var/log/myapp/sync.log 2>> /var/log/myapp/sync-error.log

If you want email notifications, set MAILTO at the top of the crontab and make sure a mail transfer agent (like postfix or msmtp) is installed:

Terminal window
0 * * * * /usr/bin/python3 /home/app/scripts/sync.py

To suppress output for a job you know is noisy but working:

Terminal window
0 * * * * /usr/bin/python3 /home/app/scripts/sync.py > /dev/null 2>&1

Be careful with that last one. Sending output to /dev/null is the reason most cron debugging is hard in the first place. Use it sparingly.

8. Timezone mismatches

Cron evaluates expressions against the system clock. If your server is set to UTC but you expected the job to run at 9 AM in your local timezone, it will fire at the wrong time.

Check the system timezone:

Terminal window
timedatectl
# or
cat /etc/timezone
# or
date +%Z

If the server is in UTC and you need a job to run at 9 AM US Eastern (UTC-5 / UTC-4 during DST):

Terminal window
# UTC equivalent of 9 AM ET (standard time)
0 14 * * * /usr/bin/python3 /home/app/scripts/report.py

The problem with this approach is daylight saving time. The UTC offset changes twice a year, so your “9 AM” job drifts by an hour.

Better approaches:

  • Set the crontab timezone (supported on some systems):
Terminal window
CRON_TZ=America/New_York
0 9 * * * /usr/bin/python3 /home/app/scripts/report.py
  • Use UTC consistently and handle display-time conversion in your application code. This is the safest approach and the one every managed scheduler uses.

9. Overlapping jobs

If a job takes longer than the interval between runs, you get concurrent executions. A backup script that takes 90 minutes, scheduled every hour, will have two instances running simultaneously by the second hour. This can cause data corruption, duplicate processing, or resource exhaustion.

Use flock to enforce mutual exclusion:

Terminal window
# Only one instance runs at a time — subsequent attempts are skipped
0 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/bin/python3 /home/app/scripts/sync.py

The -n flag makes flock exit immediately (non-blocking) if the lock is already held. Without -n, it waits — which is rarely what you want for cron jobs.

For more complex concurrency control, use a timeout:

Terminal window
# Wait up to 60 seconds for the lock, then give up
0 * * * * /usr/bin/flock -w 60 /tmp/sync.lock /usr/bin/python3 /home/app/scripts/sync.py

If your jobs are regularly overlapping, the real fix is to either increase the interval or optimize the job’s execution time.

Checking the cron logs

When none of the nine causes above explain the problem, check whether cron even attempted to run your command. Cron logs every attempt, and these logs are the fastest way to narrow down the issue.

Debian/Ubuntu:

Terminal window
grep CRON /var/log/syslog

CentOS/RHEL:

Terminal window
cat /var/log/cron

systemd-based systems (any distro):

Terminal window
journalctl -u cron --since "1 hour ago"
# or for crond:
journalctl -u crond --since "1 hour ago"

A normal cron log entry looks like:

Mar 19 09:00:01 server CRON[12345]: (www-data) CMD (/usr/bin/php /var/www/app/artisan schedule:run)

If you see the entry, cron ran the command. The problem is in your script. If there is no entry at the expected time, the issue is with the cron expression, the daemon, or the crontab itself.

To verify your crontab is loaded correctly:

Terminal window
crontab -l

If the output is empty or missing your entry, the crontab was not saved properly. Re-edit with crontab -e.


Tired of debugging cron infrastructure? If you’re spending more time on PATH issues, silent failures, and log wrangling than on the actual jobs, a managed job scheduling service eliminates the entire category. Recuro runs your scheduled HTTP requests with built-in execution logs, automatic retries, configurable failure alerts, and a dashboard that shows every job’s status and history. You define the cron expression and the endpoint URL — everything else is handled.

If you’re already running cron jobs and want to add monitoring without migrating, see how to monitor cron jobs and get alerts.

Frequently asked questions

Why is my cron job not running?

The most common causes are: the cron daemon is stopped, the cron expression has a syntax error, PATH is not set correctly (cron uses a minimal environment), the script is not executable, or output is being discarded so you cannot see the error. Start by checking whether the daemon is running (systemctl status cron) and whether cron logged an attempt (grep CRON /var/log/syslog).

How do I debug a cron job that works manually but fails in cron?

The difference is almost always the environment. Cron runs with a minimal PATH (/usr/bin:/bin), uses /bin/sh instead of bash, does not load your shell profile, and does not set a working directory. Use absolute paths for all commands and file references, set SHELL=/bin/bash if you need bash features, and redirect stderr to a log file (2>&1) so you can see the actual error message.

Where are cron logs located?

On Debian and Ubuntu, cron logs are in /var/log/syslog (filter with grep CRON /var/log/syslog). On CentOS and RHEL, they are in /var/log/cron. On any systemd-based system, use journalctl -u cron to view them. These logs show whether cron attempted to run your command, which helps you determine if the problem is the schedule or the script.

How do I fix PATH issues in cron?

Cron does not load your shell profile, so commands like python3, node, or php may not be found. Either use absolute paths in your cron command (e.g., /usr/bin/python3 instead of python3), or set PATH at the top of your crontab file (e.g., PATH=/usr/local/bin:/usr/bin:/bin). Find the absolute path of any command by running 'which command-name' in your terminal.

How do I prevent cron jobs from overlapping?

Use flock to create a lock file that prevents concurrent executions. Add it before your command in the crontab: /usr/bin/flock -n /tmp/myjob.lock /path/to/script.sh. The -n flag makes flock exit immediately if another instance already holds the lock, so the second run is skipped instead of queued.

Why does my cron job run at the wrong time?

Cron evaluates expressions against the system clock. If your server timezone is UTC and you scheduled a job for '0 9 * * *', it runs at 9 AM UTC, not your local time. Check your system timezone with 'timedatectl' or 'date +%Z'. Either convert your desired time to UTC, or set CRON_TZ in your crontab if your system supports it.

How do I see the error output from a cron job?

By default, cron sends output to the local mail system, which is often not configured on modern servers. Redirect both stdout and stderr to a log file by appending '>> /var/log/myjob.log 2>&1' to your cron command. You can also set [email protected] at the top of the crontab, but this requires a working mail transfer agent on the server.

Why does the % character break my cron job?

In crontab, the % character is interpreted as a newline. Everything after the first unescaped % is sent to the command as stdin instead of being part of the command line. This silently breaks commands like date +%Y-%m-%d. Escape every % as \% in your crontab entry, or move the command into a shell script where % is treated normally.


Stop managing infrastructure. Start scheduling jobs.

Recuro handles cron scheduling, retries, alerts, and execution logs — so you can focus on building your product.

No credit card required