Cron broke and you need to fix it. This page is the decision tree by symptom. Skim the section that matches what you are seeing, then click through to the satellite page with the actual fix.
The 8 most common cron failure modes:
- Cron did not run at all.
- Cron logs are empty.
- Cron ran but the task did nothing.
- Cron runs at the wrong time (timezone or DST).
- Manual run works, crontab does not.
- Container or serverless cron is unreliable.
- Multiple instances of the same job fired at once.
- The cron platform itself flaked.
If none of these match, jump to when to stop troubleshooting and start monitoring.
Cron did not run at all
The symptom: a scheduled job did not execute at the expected time. Nothing in the log file for that minute, no side effects in your app, no email from cron.
The order that fixes 95% of cases:
- Verify the cron daemon is up:
systemctl status cron(orcrondon RHEL). - Verify the crontab is loaded:
crontab -lfor the user that owns the job, plussudo crontab -lfor root. - Validate the crontab syntax in the free cron expression debugger. Five-field vs six-field mismatches are the most common silent failure.
- Check that the cron daemon was running at the scheduled minute by comparing
last rebootandsystemctl status cron's "Active: active (running) since" timestamp.
Full eight-step walkthrough with code: cron job not running: the 8-step debug checklist.
Cron logs are empty
The symptom: you ran grep CRON /var/log/syslog and there is nothing for the expected minute, or the log file does not exist.
The right path depends on the distro:
- Debian / Ubuntu:
/var/log/syslog(filtered bygrep CRON), orjournalctl -u cron. - RHEL / CentOS / Fedora: dedicated
/var/log/cron. - Alpine:
/var/log/messages, or stdout if you run busyboxcrondin the foreground. - macOS:
log show --predicate 'process == "cron"' --last 1h. - Any systemd distro:
journalctl -u cronorjournalctl -u crond.
If the log file shows nothing, cron did not attempt the job, which sends you back to cron did not run at all.
Full reference with sample output per distro: where are cron logs stored on Linux.
Cron ran but the task did nothing
The symptom: the cron log shows CMD (...) at the expected minute, but the side effects of the job never happened. The user table was not updated, the email was not sent, the backup file is not on disk.
The usual causes:
- Wrong environment. Cron runs with a minimal environment: typically no
PATHbeyond/usr/bin:/bin, noHOMEset the way your shell sets it, no~/.bashrcloaded. The command exits 0 because a missing env var made it skip the work it would otherwise do (think feature flags or "if API_KEY is empty, no-op").- Fix: at the top of the script, dump
env > /tmp/cron-env.txt. Run the same command manually withenv -i bash -c '/your/command'to reproduce the minimal env.
- Fix: at the top of the script, dump
- Wrong working directory. Cron starts in
$HOMEof the crontab owner. Relative paths break.- Fix:
cd /path/to/expected/dir && /your/command, or use absolute paths everywhere.
- Fix:
- Output went to
/dev/null. Most crontabs end with>/dev/null 2>&1to suppress mail. That also hides the error message you need.- Fix during debugging: remove the redirect and let cron mail you, or redirect to a file you can read later (
>> /var/log/myjob.log 2>&1).
- Fix during debugging: remove the redirect and let cron mail you, or redirect to a file you can read later (
- The script exits 0 even when it failed. Some scripts catch errors and return success anyway. Add
set -euo pipefailat the top of bash scripts; check exit codes in Python with explicitsys.exit(1)on the error path.
Reference: cron job not running, step 6: check that the script actually exits.
Timezone and DST drift
The symptom: a job that runs at "08:00" fires at 09:00 (or 07:00) for half the year, or fires at a different time than the dashboard shows.
The rules:
crontabuses the system timezone unless you setCRON_TZ=at the top of the file (where supported).- Platform cron is almost always UTC. Vercel Cron, GitHub Actions cron, Cloudflare Workers Cron Triggers, AWS EventBridge — all of them schedule in UTC. There is no "use my timezone" toggle.
- DST is the trap. A job written as
0 8 * * *in a UTC-locked scheduler will be 09:00 your local time in summer and 08:00 your local time in winter (in Europe), or 03:00 vs 04:00 ET (in the US).
How to handle it:
- System cron: set
TZ=Europe/Berlinat the top of the crontab file (or per-job:0 8 * * * TZ=Europe/Berlin /your/command). Verify withtimedatectl. - Platform cron in UTC: do the math twice a year. Or move the schedule layer to a per-IANA-timezone-aware scheduler. Crontap stores timezone per schedule;
Europe/BerlinmeansEurope/Berlinalways, DST handled automatically.
Reference: GitHub Actions cron drift problem for the platform-cron timezone story.
Manual run works, crontab does not
The symptom: you run the exact command at the shell prompt and it works. cron runs it and the job fails.
Almost always one of:
- PATH. Your shell has
/usr/local/binin PATH; cron does not. Use absolute paths everywhere:/usr/bin/python3 /opt/sync.pyinstead ofpython3 /opt/sync.py. Or setPATH=/usr/local/bin:/usr/bin:/binat the top of the crontab. - User context. You ran the command as your user; cron ran it as the crontab owner (often
root, sometimeswww-data). File permissions or~/.configfiles may not exist for the cron user. - Shell. Your interactive shell is
bash; cron uses/bin/sh(which isdashon Debian/Ubuntu and breaks bashisms). SetSHELL=/bin/bashat the top of the crontab or use full bash invocation:/bin/bash -c '/your/command'. - Working directory. Your shell starts in the project dir; cron starts in
$HOMEof the crontab owner. Use absolute paths orcdfirst.
Quickest reproduction: env -i HOME=/root /bin/sh -c '/your/command'. If that fails the same way cron does, you have isolated the issue.
Container or serverless cron is unreliable
The symptom: the cron works on your laptop but the production container or serverless function skips runs, or fires on the wrong cadence, or never fires at all.
Container-specific failures:
- No cron daemon in the image. Most slim base images do not include cron. A
CMD ["python", "app.py"]container will never runcrontabentries because there is no cron process. Either install cron explicitly (apt-get install -y cron) and start it as PID 1, or use external HTTP cron to call the container. - Container exited. Cron only runs while the container is up. Scale-to-zero (Cloud Run with min-instances 0, Lambda, scale-to-zero Fly Machines) means cron only runs when traffic woke the container. The schedule is at the mercy of traffic.
- PID 1 is not cron. If your container's main process is Python or Node, cron is a side process and dies when the main process exits. Run cron in foreground as PID 1 if it is the container's purpose.
The structural fix: move the schedule layer outside the container. External HTTP cron calls the container on the cadence you set; the container does the work and exits cleanly. Detailed pattern: Docker cron jobs: four ways and when to pick each.
Serverless cron limits:
- Vercel Cron on Hobby is hourly minimum, capped at 5 jobs; cadence changes require a redeploy. See Vercel Cron alternative.
- Netlify Scheduled Functions cap at 30-second runtime and do not retry on failure.
- GitHub Actions cron is best-effort and routinely drifts 5-30 minutes under load. See GitHub Actions cron drift problem.
- Cloudflare Workers Cron Triggers are UTC only and live per-
wrangler.toml; cadence changes ride along with deploys. See Crontap vs Cloudflare Workers Cron. - Firebase Scheduled Functions require the Blaze billing plan. See Firebase scheduled functions without Blaze.
Multiple instances of the same job fired
The symptom: the job ran more than once at the expected minute. The database has duplicate rows; two emails went out instead of one.
The common causes:
-
Two cron daemons running. A long-running container that you also have a host crontab on. Run
ps aux | grep cronand verify there is one cron process owning the job. -
The previous run is still going. Cron does not lock by default. A job scheduled every 5 minutes that takes 7 minutes to complete will have two copies running at the 5-minute mark. Use
flockto serialize:* * * * * /usr/bin/flock -n /tmp/myjob.lock /path/to/job.sh -
External cron + native cron both configured. During a migration, both schedulers fire the same job. Disable one explicitly before testing the other.
-
Kubernetes CronJob retry policy. If your
CronJobhasconcurrencyPolicy: Allow(the default), Kubernetes will not block overlapping runs. SetconcurrencyPolicy: Forbidto prevent the overlap.
The cron platform itself flaked
The symptom: nothing changed on your side, but the schedule stopped firing. The platform's status page mentions a recent incident.
This is the case for external dependencies. Quick checks for the platforms we cover:
- Is Vercel down? — Vercel Cron runs on the deployment plane.
- Is GitHub down? — GitHub Actions cron is best-effort even on healthy days.
- Is Cloudflare down? — Workers Cron Triggers ride the edge plane.
- Is Firebase down? — Scheduled Functions depend on Cloud Scheduler + Pub/Sub.
- Is AWS down? — EventBridge schedules are at-most-once and do not catch up.
- Is Render down? — Render Cron Jobs share platform compute.
If the platform was down at your expected minute, do not redeploy or retry your code. Re-fire the missed run manually once the platform recovers and wait one full cycle to confirm normal cadence resumed.
When to stop troubleshooting and start monitoring
You are reading this hub because cron broke and you noticed only when something downstream noticed it. That is normal once. If it happens more than once a month, the structural answer is to add monitoring that pages you the moment a run fails, instead of relying on downstream symptoms.
Two complementary layers:
- Loud failures. A run that returned 5xx, timed out, or threw an exception. Crontap fires HTTP cron and emails / Slacks you when a run fails, with the response body and status code in the alert. Reference: cron job monitoring.
- Silent failures. A run that never fired at all. Cron is the worst at telling you about this because the symptom is silence. The pattern is a dead-man check: the job pings a watcher on success, the watcher pages you when the ping stops. Reference: dead man's switch, explained for developers and pair Crontap with Healthchecks.io.
Stop debugging cron by hand. External HTTP cron with retries on 5xx, stored run logs, and failure alerts in one dashboard. Free forever tier with one schedule. Try Crontap →
Related on Crontap
- Cron job not running: the 8-step debug checklist. The deep-dive for failure mode 1.
- Where are cron logs stored on Linux. The deep-dive for failure mode 2.
- What is a cron job in Linux?. The pillar reference for syntax, daemon, and crontab basics.
- Cron syntax cheat sheet. When the problem turns out to be the expression.
- Cron expression recipes. Copy-paste expressions for common cadences when you need to rewrite from scratch.
- Cron job monitoring. The service page for failure alerts and run history.
- Dead man's switch, explained for developers. For the silent-failure case.
Language-specific cron guides
When the symptom looks language-flavored, jump straight to the right guide:
- Python cron jobs -- crontab + manage.py, APScheduler, platform, external HTTP cron.
- Django cron jobs -- django-q2, Celery Beat, management commands.
- Laravel cron jobs -- Scheduler + schedule:run, Forge, Vapor.
- PHP cron jobs -- crontab + php-cli pitfalls, the cli-vs-fpm env trap.
- Node.js cron jobs -- node-cron, BullMQ, the clustering trap.
- Postgres cron jobs -- pg_cron when your host allows it, advisory locks.
- Supabase cron jobs -- pg_cron, Supabase Cron, external HTTP cron.
- cPanel cron jobs -- shared-hosting context.
- Docker cron jobs -- four container patterns.
Stop debugging cron by hand. External HTTP cron with retries on 5xx, stored run logs, and failure alerts in one dashboard. Try Crontap →
