Home › Cron Job Not Running

Cron Job Not Running

A step-by-step guide to debugging a cron job that silently does nothing. Covers the most common causes: PATH, permissions, environment variables, output logging, timezone and missing trailing newline.

Step 1: Is Cron Actually Running?

The first thing to check - before anything else. If the cron daemon is not running, nothing fires.

Debian / Ubuntu

systemctl status cron

If inactive: sudo systemctl start cron && sudo systemctl enable cron

CentOS / RHEL / AlmaLinux

systemctl status crond

If inactive: sudo systemctl start crond && sudo systemctl enable crond

Step 2: Did Cron Actually Fire the Job?

There is a difference between cron not running the job and the job running but failing. Check the cron log first.

Debian / Ubuntu

grep CRON /var/log/syslog | tail -30
# live:
tail -f /var/log/syslog | grep CRON

Look for a line with CMD - that means cron fired it. If no CMD line appears, cron is not seeing the job at all.

CentOS / RHEL

tail -30 /var/log/cron
# live:
tail -f /var/log/cron

Same logic - CMD in the log means cron fired it. No CMD means cron cannot see the entry.

With journald (any distro)

journalctl -u cron --since "1 hour ago"    # Debian/Ubuntu
journalctl -u crond --since "1 hour ago"   # CentOS/RHEL

Step 3: Enable Output Logging

By default, cron discards all output from your script. If the job fires but nothing happens, you have no way to know what went wrong - unless you redirect output to a file.

0 2 * * * /usr/bin/php /var/www/script.php >> /var/log/myjob.log 2>&1

The >> appends stdout to the log file. The 2>&1 also redirects stderr (error messages) to the same file. Without 2>&1 you only see normal output - exceptions, fatal errors and warnings are silently discarded. Once you have a log file, tail -f /var/log/myjob.log shows you exactly what the script is doing.

Step 4: Test the Command Manually

Copy the exact command from the crontab and run it in the terminal, as the same user cron will use. If it fails here, it fails in cron too.

# Run as yourself:
/usr/bin/php /var/www/script.php

# Run as the cron user (e.g. www-data):
sudo -u www-data /usr/bin/php /var/www/script.php

Running as the correct user is important - file permissions and home directory differ per user. A script that works as root may fail as www-data.

Step 5: The PATH Problem

This is the single most common cause of cron jobs that work manually but fail silently. Cron runs with a minimal environment: PATH=/usr/bin:/bin. Your shell has ~/.bashrc loaded with /usr/local/bin, ~/bin, rbenv, nvm and everything else. Cron has none of that.

Solution 1: Use full paths

# Find the full path:
which php
which python3
which node

# Use it in the crontab:
0 2 * * * /usr/bin/php /var/www/script.php

Solution 2: Set PATH in the crontab

PATH=/usr/local/bin:/usr/bin:/bin
HOME=/home/username

0 2 * * * php /var/www/script.php

Variables defined at the top of the crontab apply to all jobs in that file.

Step 6: Environment Variables

Cron does not load ~/.bashrc, ~/.profile or any shell initialization file. Database credentials, API keys, DJANGO_SETTINGS_MODULE, NODE_ENV - none of them are available unless you set them explicitly.

In the crontab

APP_ENV=production
DB_PASS=secret
0 2 * * * /usr/bin/php script.php

Via a wrapper script

#!/bin/bash
source /home/user/.env
/usr/bin/php script.php

Call the wrapper from cron instead of the script directly.

Inside the script

Load the .env file explicitly at the top of the script using an absolute path - do not rely on the working directory.

# Python
load_dotenv('/home/user/project/.env')
# PHP
$dotenv->load('/var/www/html');

Step 7: Permissions

Script must be executable

ls -la /path/to/script.sh
chmod +x /path/to/script.sh

Shell scripts need the execute bit set. PHP and Python scripts called via their interpreter (/usr/bin/php script.php) do not need it - but the interpreter binary itself must be executable by the cron user.

File ownership

ls -la /path/to/script.php
# should be readable by the cron user:
chown www-data:www-data /path/to/script.php

The cron user must be able to read the script and write to any output files or directories the script touches.

Step 8: Missing Trailing Newline

This is a surprisingly common cause on Debian and Ubuntu. Vixie-cron silently ignores the last line of a crontab if it does not end with a newline character. When editing with crontab -e, always press Enter after your last entry before saving. You can verify by running crontab -l | cat -A - the last line should end with $, not just the command text.

Step 9: Cron Expression Is Wrong

A valid-looking expression can still never fire. Common mistakes:

  • 0 9 * * 1-5 - correct (weekdays at 9 AM)
  • 9 0 * * 1-5 - wrong (minute 9 of midnight, not 9 AM)
  • * 9 * * 1-5 - every minute between 9:00 and 9:59, not once at 9:00
  • Quartz expression (6 fields) pasted into Linux crontab (5 fields)
  • Day-of-month + day-of-week both set: Linux uses OR, not AND
  • February 31st - logically valid syntax, never fires

Step 10: Timezone Mismatch

Cron runs in the server's system timezone. If you expect a job at 9 AM but the server is in UTC and you are in UTC+2, the job fires at 11 AM your time.

timedatectl          # see current timezone
cat /etc/timezone    # on older systems

To schedule in a specific timezone on Linux, set the CRON_TZ variable at the top of the crontab:

CRON_TZ=Europe/Madrid
0 9 * * 1-5 /usr/bin/php /var/www/script.php

Platform-Specific Causes

Python / virtualenv

Using python3 instead of the virtualenv Python. The system Python does not have your project's packages. Always use /path/to/venv/bin/python.

Python cron job guide

Laravel scheduler

Missing the cd /project && prefix, wrong PHP path, or .env not readable by the cron user. Run php artisan schedule:list to verify schedules are registered.

Laravel scheduling guide

Node.js / node-cron

In-process schedulers die when the Node process stops. If your app crashed or was redeployed, the cron jobs stop too. Use PM2 to keep the process alive.

Node.js cron job guide

CentOS / RHEL: SELinux

SELinux can silently block cron jobs that access files or network resources outside their expected context. Check: sudo ausearch -m avc -ts recent | grep cron

CentOS cron job guide

Plesk / cPanel

In Plesk, "Run a command" tasks run in a chrooted environment - many system paths are inaccessible. Use "Run a PHP script" or "Fetch a URL" instead.

Plesk cron job guide

GitHub Actions cron

GitHub schedules only run on the default branch, can be delayed 15+ minutes during peak load, and are automatically disabled after 60 days of repo inactivity. Forks do not run scheduled workflows by default.

GitHub Actions cron guide

Vercel cron jobs

Cron jobs only run on production deployments - not on preview. Changes to vercel.json only take effect after a new deployment. On the Hobby plan, jobs are limited to once per day maximum.

Vercel cron jobs guide

Cloudflare Workers cron

The scheduled() handler must use ctx.waitUntil() for async work - without it, async operations may be cut off. Triggers are registered on deploy; changes to wrangler.toml require a new wrangler deploy.

Cloudflare cron triggers guide

Windows / Task Scheduler

The task is set to "Run only when user is logged on" - change to "Run whether user is logged on or not". Also check that the "Start in" directory is set correctly.

Windows cron job guide

How to Check if a Cron Job Is Running

Before debugging why a cron job is not running, confirm whether it fired at all. These commands tell you definitively whether cron executed the job.

Check the cron log (Debian / Ubuntu)

grep CRON /var/log/syslog | grep "$(date +%b\ %e)"

Look for a line with CMD and your command. If it appears, cron ran the job. If it does not appear, cron never fired it - the expression is wrong or cron is not running.

Check the cron log (CentOS / RHEL)

grep "$(date +%b\ %e)" /var/log/cron

Same logic. CMD in the log means the job fired. No CMD means cron did not run it.

Check with journald (any distro)

journalctl -u cron --since "1 hour ago" | grep CMD
journalctl -u crond --since "1 hour ago" | grep CMD

Use cron on Debian/Ubuntu, crond on CentOS/RHEL.

Check via output log

tail -f /var/log/myjob.log

If you added >> /var/log/myjob.log 2>&1 to your cron entry, this file shows exactly what the script did and any errors it produced. No file means you have not enabled logging yet - see Step 3 above.

Check with a test entry

* * * * * echo "cron fired at $(date)" >> /tmp/cron-test.log

Add this to your crontab and wait 1-2 minutes. If /tmp/cron-test.log gets entries, cron is working. If not, the daemon is not running or the crontab is not being read.

Check via a timestamp file

# Add to the end of your script:
date >> /tmp/myscript-lastrun.txt

# Then check:
cat /tmp/myscript-lastrun.txt

A simple way to verify the script itself completed - not just that cron fired it. If the cron log shows CMD but this file is not updated, the script is crashing before it finishes.

Quick Diagnosis Checklist

  • Is the cron daemon running? (systemctl status cron)
  • Does the job appear in crontab -l?
  • Does the crontab file end with a newline?
  • Does the job appear in /var/log/syslog or /var/log/cron?
  • Does the command work when run manually?
  • Are all paths absolute (not relative)?
  • Is the script executable? (chmod +x)
  • Is output redirected to a log? (>> log 2>&1)
  • Are environment variables set explicitly?
  • Is the cron expression valid? (use the checker below)

Validate your cron expression

A wrong expression is the easiest problem to rule out. Paste it in the checker - it takes two seconds.