Guides · Utiltown

Cron Job Not Running? 9 Causes and a Fix Path

"The cron job is not running" is a chain of assumptions, and the break can be in the expression, the timezone, the daemon, the environment, or the command itself. Debugging that chain top-to-bottom finds the break in minutes; poking at the script first usually wastes hours. The nine causes below are ordered by where they sit in the chain — work through them in order and the first failing step is your answer.

Step 0 — Prove the schedule is actually due

Before touching logs, verify the expression means what you think it means in the scheduler you are using. Paste it into the Cron Expression Parser, pick the right dialect, and read the next ten run times. Three mistakes here are near-universal:

Step 1 — Check the timezone the scheduler uses

System cron evaluates the expression in the machine's local timezone, which on cloud servers is usually UTC. Confirm what "now" means to the scheduler:

timedatectl            # system timezone
date                   # what "now" means to cron
TZ=Asia/Shanghai date  # what you assume "now" is

Fix options: set CRON=Asia/Shanghai (or CRON_TZ, depending on the cron implementation) at the top of the crontab, or schedule everything in UTC and convert at the presentation layer. GitHub Actions schedules in UTC unless you set the optional timezone: property on the schedule trigger.

Step 2 — Confirm the trigger fired

grep CRON /var/log/syslog | tail        # Debian / Ubuntu
journalctl -u cron --since "1 hour ago" # Debian-family systemd (crond on RHEL)
grep crond /var/log/cron | tail         # RHEL / CentOS

Step 3 — Check the schedule's edge cases

Some "missing" runs are real schedule semantics, not bugs:

Step 4 — Reproduce the cron environment

The classic failure: the script runs perfectly in your shell and never from cron. Cron starts sh with a minimal PATH (/usr/bin:/bin), no profile, no nvm/rbenv/pyenv, and no TTY:

# See exactly what cron would resolve
env -i /bin/sh -c 'echo $PATH; which node; which python3'

Step 5 — Capture output; never let it vanish

Without redirection, stdout/stderr go to the local mailbox — and with no MTA configured they are discarded. This is the single most common reason jobs "fail silently for months":

0 9 * * * /opt/scripts/myjob.sh >> /tmp/myjob.log 2>&1

(Use a path the job's user can actually write — a user crontab usually cannot create files under /var/log; point it at an app-specific directory you have chown'd.)

Then watch the next run: log created but empty means the wrapper started and the inner command hangs or exits silently — add set -x to trace.

Step 6 — Permissions, locking, and overlap

Step 7 — Platform-specific gotchas

Platform Symptom Cause / fix
GitHub Actions Runs late by 5–30 min, or skipped Best-effort scheduling under load. Never for deadlines; use workflow_dispatch from an external timer or a self-hosted runner with real cron.
GitHub Actions Nothing after merging to a branch schedule: only triggers from the default branch.
systemd timer Timer loaded but never fires The service unit is not enabled: systemctl enable --now myjob.timer; verify with systemctl list-timers and systemd-analyze calendar.
systemd timer Missed runs while the machine was off Add Persistent=true to catch up on missed elapses at boot.
Quartz (Java) Fires Sunday instead of Monday Day-of-week numbering: Quartz 1 = Sunday. Use MON-FRI.
Docker Cron "runs" but nothing happens Cron drops environment variables set in the image; the daemon is not the container's PID 1 supervisor. Run cron in the foreground and supervise it, or schedule from the host against docker exec.
Kubernetes CronJob Jobs pile up or vanish concurrencyPolicy defaults to Allow (piles up). Missed runs are re-run after a controller outage when startingDeadlineSeconds is unset (unless 100+ schedules were missed, which aborts with an error) — but explicitly skipped when a deadline is set and exceeded. Set both deliberately.
Windows Task Scheduler Never fires for a different user Task runs only when the trigger user is logged on — set "Run whether user is logged on or not" (requires stored credentials).

Step 8 — The 60-second sanity sweep

crontab -l | grep -v '^#' | grep -v '^$'   # what is scheduled
systemctl status cron --no-pager | head -3 # is the daemon alive
grep CRON /var/log/syslog | tail -5        # did triggers fire
ls -l /tmp/myjob.log                       # is output being captured

Put the parser in your loop

Most of the chain above reduces to one question: does this expression mean what I think, in this zone, in this dialect? The Utiltown Cron Expression Parser answers it in one paste — plain-English decode, dialect switch, timezone selector, and the next ten run times, all computed locally in the browser.

Frequently Asked Questions

My cron expression parses fine but the job never runs. Where do I start?

Separate the schedule from the execution. First prove the schedule is actually due: validate the expression in the Utiltown Cron Expression Parser, in the right dialect and the server timezone, and compare the computed next-run times with your expectation. Timezone and dialect mistakes look identical to scheduler failures. Only after the times check out should you move to logs, environment, and permissions.

Why does my job run at the wrong time?

Almost always a timezone mismatch. System cron evaluates the expression in the machine timezone (UTC on most cloud servers), GitHub Actions schedules in UTC unless you set the optional timezone property, and Quartz depends on the configured scheduler timezone. Convert your intended local time to the scheduler zone — the Utiltown Unix Timestamp Converter shows both side by side.

The script runs manually but not from cron. What is different?

The environment. Cron starts sh with a minimal PATH (often /usr/bin:/bin), sources no profile, and has no nvm/rbenv/pyenv. The same command line resolves to a different binary — or none. Use absolute paths for interpreters and files, cd inside a wrapper script, and redirect output to a log so the real error is visible.

How do I prove whether cron even fired?

Read the scheduler logs: grep CRON /var/log/syslog (Debian/Ubuntu), journalctl -u cron (Debian-family systemd; the unit is crond on RHEL-family), or /var/log/cron (RHEL family). Trigger lines present means the schedule works and the command is failing; no trigger lines points at the crontab, the daemon, or the expression syntax itself.

Can a cron job fail silently for months?

Yes. Without output redirection, cron mails stdout/stderr to the local mailbox — and with no mail transfer agent configured, that output is discarded. A job that logs nothing may have been failing since day one. End every production job with >> /tmp/myjob.log 2>&1 (a writable path) and alert on the log.