DeveloperJuly 20, 2025·9 min read

Cron Job Not Running? How to Debug Cron Schedules and Timing Issues

Cron job silently failing, running at the wrong time, or never firing at all? This guide diagnoses which problem you have and gives the exact fix — including the timezone issue that catches almost everyone.

A cron job not running at the expected time is one of the most frustrating server-side debugging experiences — the backup did not run, the report was never sent, the cleanup job silently skipped. Before touching anything, you need to diagnose which of two completely different problems you have: the job is not running at all, or it is running but at the wrong time. The causes and fixes are completely different.

Step 1 — Diagnose: not running at all vs running at wrong time

SymptomMost likely causeSection
Job never fires at any timeCron expression syntax error, file not executable, wrong user's crontabFix 1 and Fix 3
Job fires but at the wrong timeTimezone mismatch — server runs UTC, you wrote local timeFix 2
Job fires, nothing happens, no errorScript fails silently — no output logging, wrong PATHFix 4
Job worked before, stopped after deploycrontab overwritten, env variable missing, permissions changedFix 3 and Fix 4
Job runs on dev but not productionDifferent timezone on servers, different user permissionsFix 2 and Fix 3

Fix 1 — Validate the cron expression syntax

A single misplaced character in a cron expression causes the job to never fire — silently, with no error message. The cron daemon simply skips expressions it cannot parse.

The five-field format is: minute hour day-of-month month day-of-week

# ✅ Valid expressions
0 9 * * *          # 9:00 AM every day
*/15 * * * *       # Every 15 minutes
0 0 1 * *          # Midnight on the 1st of every month
0 9 * * 1-5        # 9:00 AM Monday through Friday
30 8 * * 1         # 8:30 AM every Monday

# ❌ Common syntax errors
0 9 * * * *        # 6 fields instead of 5 (extra asterisk)
9:00 * * * *       # Using colon instead of space-separated fields
0 9 * * Mon-Fri    # Some cron implementations don't support day names — use 1-5
* 9 * * *          # Fires every minute from 9:00 to 9:59 — missing the "0" for minute

Example: A developer wrote 0 9 * * Mon-Fri intending to run a job on weekdays. On their Linux server, this worked fine. After deploying to a minimal Alpine Linux container, the job never fired — the cron implementation in BusyBox does not support day names. The fix: 0 9 * * 1-5.

Use the Cron Job Generator to build expressions visually and confirm the next 5 run times in plain English before deploying. The tool also shows common preset schedules you can adapt.

Fix 2 — The timezone problem (the #1 cause of "wrong time")

This is by far the most common cause of cron jobs running at the wrong time. Cron runs in the server's timezone — not your local timezone. Cloud servers (AWS EC2, Google Cloud, Render, Railway, Heroku, DigitalOcean) run in UTC by default.

Cron job timezone comparison — same expression runs at different local timesDiagram showing that a cron expression set to "0 9 * * *" (9 AM) on a UTC server runs at 4 AM EST, 1 AM PST, and 2:30 PM IST. The UTC time is fixed; local times vary by offset.0 9 * * * → "Run at 9 AM"But 9 AM in which timezone? The server's timezone — almost always UTC in the cloud.12am3am6am9am12pm3pm6pm9pm12pmCron fires: 9:00 UTCUTC: 9:00 AMCloud servers (AWS, GCP, Render)EST (UTC−5): 4:00 AMUS East Coast — New YorkPST (UTC−8): 1:00 AMUS West Coast — Los AngelesIST (UTC+5.5): 2:30 PMIndia Standard Time — MumbaiFix: set TZ=America/New_York in crontab, or calculate the UTC equivalent before writing the expression
The same cron expression fires at 9:00 AM UTC — which is 4:00 AM EST, 1:00 AM PST, and 2:30 PM IST

Example: A team in London set up a cron job to run their daily report at 0 9 * * *, meaning "9 AM." Their AWS EC2 instance ran UTC. The report ran at 9 AM UTC — which was 10 AM BST in summer (one hour ahead) but 9 AM in winter (same as UTC). The report arrived at different times depending on the season, with no changes to the crontab. The fix: change to TZ=Europe/London in the crontab.

How to check your server's timezone:

# Check current timezone
date
timedatectl   # Linux with systemd
cat /etc/timezone

# Check what timezone cron is using
date +%Z  # in a cron job to log timezone at runtime

Fix option 1 — Set TZ in your crontab:

# Add TZ at the top of crontab -e
TZ=America/New_York
0 9 * * * /path/to/script.sh   # Now runs at 9 AM Eastern

Fix option 2 — Convert to UTC in the expression:

# If you want 9 AM EST (UTC-5), write 2 PM UTC
0 14 * * * /path/to/script.sh

# 9 AM PST (UTC-8) = 5 PM UTC
0 17 * * * /path/to/script.sh

GitHub Actions: Scheduled workflows always run in UTC and the TZ variable has no effect on the schedule. Calculate the UTC equivalent of your desired local time and use that in the on.schedule.cron expression. Also note: GitHub Actions scheduled jobs can run up to 15 minutes late under heavy load — they are not millisecond-precise.

Fix 3 — crontab not saved or installed under the wrong user

This is the most common cause of "it worked in testing but not in production." There are two separate crontab systems: user crontabs and system crontabs.

# Check if your job is actually in the crontab
crontab -l               # List your jobs — if not here, it was never saved
crontab -l -u www-data   # Check a specific user's crontab (as root)

# Edit crontab (always saves automatically on exit)
crontab -e

# System-wide crontabs live here — formatted differently
ls /etc/cron.d/
ls /etc/cron.daily/
cat /etc/cron.d/myjob

Example: A developer edited the crontab as the deployuser during setup. The script needed to access a database as the www-data user. The job appeared in crontab -l for deploy but the script silently failed because it lacked permission to the database socket. The fix: move the crontab entry to www-data using crontab -e -u www-data.

File permissions: The script must be executable. This is the second-most common cause of a job that appears in the crontab but never does anything:

# Make script executable
chmod +x /path/to/script.sh

# Verify
ls -la /path/to/script.sh
# Should show: -rwxr-xr-x

Fix 4 — The job runs but silently fails

Standard cron captures no output by default. If your script fails, the error message goes nowhere — cron's only output mechanism is the system mail, which most servers never check.

Add output logging to every cron job:

# Redirect stdout and stderr to a log file
0 2 * * * /path/to/script.sh >> /var/log/myjob.log 2>&1

# Timestamped logging — much easier to debug
0 2 * * * echo "--- $(date) ---" >> /var/log/myjob.log 2>&1 && /path/to/script.sh >> /var/log/myjob.log 2>&1

The PATH problem: Cron runs with a stripped-down PATH — typically just /usr/bin:/bin. Commands that work in your shell may not be found by cron. The fix: use full paths or set PATH explicitly.

# ❌ Fails in cron — python3 not in cron's PATH
0 2 * * * python3 /path/to/script.py

# ✅ Fix option 1 — use full path
0 2 * * * /usr/bin/python3 /path/to/script.py

# ✅ Fix option 2 — set PATH in crontab
PATH=/usr/local/bin:/usr/bin:/bin:/home/user/.local/bin
0 2 * * * python3 /path/to/script.py

Environment variables: Cron does not load your shell profile (.bashrc, .profile). Variables like DATABASE_URL or API_KEY that work in your terminal are not available in cron unless you set them explicitly:

# Set env vars in crontab directly
DATABASE_URL=postgresql://localhost:5432/mydb
API_KEY=your_key_here
0 2 * * * /path/to/script.sh

# Or source an env file in the script
0 2 * * * source /etc/myapp/env && /path/to/script.sh

How to test a cron expression before deploying

Never guess at a cron expression for a production job. Test it first:

  1. Use the Cron Job Generator: Enter your desired schedule visually and verify the plain-English description matches what you expect. The tool shows the next 5 scheduled run times. Try it here →
  2. crontab.guru: Paste any expression and see the next run times with calendar visualisation.
  3. Test at high frequency first: Change the expression to */1 * * * * (every minute), confirm the job fires and produces the correct output, then change to your actual schedule.
  4. Check the log immediately after the scheduled time: If you expect the job at 2:00 AM, check the log at 2:01 AM — if nothing is there, the job did not run at all (expression or permission problem), not a script failure.

Key takeaways

  • Diagnose first: "not running at all" and "running at wrong time" have different causes and different fixes.
  • The #1 cause of wrong timing: cloud servers run UTC. Calculate the UTC equivalent of your desired local time, or set TZ= at the top of your crontab.
  • Always run crontab -l to confirm the job is actually saved before debugging anything else.
  • Use full paths for commands in cron scripts — cron's PATH is stripped down and does not include your shell's PATH.
  • Add output logging to every cron entry — without logging, failures are completely invisible.
  • For the complete cron expression syntax reference including wildcards, step values, and platform differences, see the complete cron expression syntax guide.
  • For a reference of 20 common cron schedules ready to copy, see cron job examples for common schedules.

Frequently asked questions

How do I check if a cron job ran?

Check the system log: grep CRON /var/log/syslog (Ubuntu/Debian) or grep CRON /var/log/cron (RHEL/CentOS). The syslog shows when cron started the job, even if the job itself failed. If the job is not in the syslog, it was either not scheduled or cron is not running.

How do I run a cron job in a Docker container?

Cron in Docker requires: installing cron, copying the crontab into the container, and running crond as the container's main process. A common pattern:

# Dockerfile
FROM alpine:latest
RUN apk add --no-cache dcron

COPY crontab /etc/crontabs/root
RUN chmod 0644 /etc/crontabs/root

CMD ["crond", "-f", "-l", "2"]

For production, prefer a dedicated scheduler (Kubernetes CronJob, AWS EventBridge, GitHub Actions) over running cron inside a Docker container.

Why does my GitHub Actions cron job not run at the exact time?

GitHub Actions scheduled workflows can run up to 15 minutes late during peak load periods. For time-sensitive jobs (sending reports at exactly 9 AM), GitHub Actions is not reliable. Use a dedicated scheduler like AWS EventBridge or a cron-capable service that guarantees execution time.

What does */5 mean in a cron expression?

The */n syntax means "every n units." In the minute field, */5 means "every 5 minutes" (0, 5, 10, 15...55). In the hour field, */2 means "every 2 hours" (0, 2, 4...22). The Cron Job Generator converts these to plain English so you can verify the interval before deploying.

Can I run a cron job more often than once a minute?

Standard cron has a 1-minute minimum resolution. For sub-minute scheduling, use application-level timers: setInterval in Node.js, APScheduler in Python with second-level intervals, or a dedicated job queue (Celery, BullMQ, Sidekiq) that supports sub-minute scheduling.

Free tool

Try the Cron Job Generator

Use our free cron job generator to calculate results instantly — no signup required.

Open Cron Job Generator
Tags:croncron jobschedulingtimezonelinuxdevopsgithub actions