Back to guides

Guides · April 3, 2026

Cron troubleshooting: the hub for every way cron breaks

The decision tree for every common cron failure. Skim the section that matches your symptom; each one links to the satellite page with the actual fix.
crontap.com / guides
Cron broke and you need to fix it. The decision tree by symptom: not running at all, running but logs are empty, runs but nothing happens, timezone drift, container weirdness, permissions. Each section links to the satellite page with the full fix.

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:

  1. Cron did not run at all.
  2. Cron logs are empty.
  3. Cron ran but the task did nothing.
  4. Cron runs at the wrong time (timezone or DST).
  5. Manual run works, crontab does not.
  6. Container or serverless cron is unreliable.
  7. Multiple instances of the same job fired at once.
  8. 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:

  1. Verify the cron daemon is up: systemctl status cron (or crond on RHEL).
  2. Verify the crontab is loaded: crontab -l for the user that owns the job, plus sudo crontab -l for root.
  3. Validate the crontab syntax in the free cron expression debugger. Five-field vs six-field mismatches are the most common silent failure.
  4. Check that the cron daemon was running at the scheduled minute by comparing last reboot and systemctl 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 by grep CRON), or journalctl -u cron.
  • RHEL / CentOS / Fedora: dedicated /var/log/cron.
  • Alpine: /var/log/messages, or stdout if you run busybox crond in the foreground.
  • macOS: log show --predicate 'process == "cron"' --last 1h.
  • Any systemd distro: journalctl -u cron or journalctl -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:

  1. Wrong environment. Cron runs with a minimal environment: typically no PATH beyond /usr/bin:/bin, no HOME set the way your shell sets it, no ~/.bashrc loaded. 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 with env -i bash -c '/your/command' to reproduce the minimal env.
  2. Wrong working directory. Cron starts in $HOME of the crontab owner. Relative paths break.
    • Fix: cd /path/to/expected/dir && /your/command, or use absolute paths everywhere.
  3. Output went to /dev/null. Most crontabs end with >/dev/null 2>&1 to 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).
  4. The script exits 0 even when it failed. Some scripts catch errors and return success anyway. Add set -euo pipefail at the top of bash scripts; check exit codes in Python with explicit sys.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:

  1. crontab uses the system timezone unless you set CRON_TZ= at the top of the file (where supported).
  2. 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.
  3. 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/Berlin at the top of the crontab file (or per-job: 0 8 * * * TZ=Europe/Berlin /your/command). Verify with timedatectl.
  • 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/Berlin means Europe/Berlin always, 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:

  1. PATH. Your shell has /usr/local/bin in PATH; cron does not. Use absolute paths everywhere: /usr/bin/python3 /opt/sync.py instead of python3 /opt/sync.py. Or set PATH=/usr/local/bin:/usr/bin:/bin at the top of the crontab.
  2. User context. You ran the command as your user; cron ran it as the crontab owner (often root, sometimes www-data). File permissions or ~/.config files may not exist for the cron user.
  3. Shell. Your interactive shell is bash; cron uses /bin/sh (which is dash on Debian/Ubuntu and breaks bashisms). Set SHELL=/bin/bash at the top of the crontab or use full bash invocation: /bin/bash -c '/your/command'.
  4. Working directory. Your shell starts in the project dir; cron starts in $HOME of the crontab owner. Use absolute paths or cd first.

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:

  1. No cron daemon in the image. Most slim base images do not include cron. A CMD ["python", "app.py"] container will never run crontab entries 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.
  2. 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.
  3. 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:

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:

  1. Two cron daemons running. A long-running container that you also have a host crontab on. Run ps aux | grep cron and verify there is one cron process owning the job.

  2. 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 flock to serialize:

    * * * * * /usr/bin/flock -n /tmp/myjob.lock /path/to/job.sh
  3. External cron + native cron both configured. During a migration, both schedulers fire the same job. Disable one explicitly before testing the other.

  4. Kubernetes CronJob retry policy. If your CronJob has concurrencyPolicy: Allow (the default), Kubernetes will not block overlapping runs. Set concurrencyPolicy: Forbid to 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:

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:

  1. 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.
  2. 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

Language-specific cron guides

When the symptom looks language-flavored, jump straight to the right guide:

Stop debugging cron by hand. External HTTP cron with retries on 5xx, stored run logs, and failure alerts in one dashboard. Try Crontap →

From the blog

Read the blog

Guides, patterns and product updates.

Tutorials on scheduling API calls, webhooks and automations, plus deep dives into cron syntax, timezones and reliability.

Alternatives

Vercel Cron every minute: beating the Hobby hourly limit

Vercel Cron caps Hobby at hourly cadence and 5 jobs, and ties every change to a redeploy. Here is the external cron pattern teams use to ship per-minute schedules, per-IANA timezones, and one dashboard across projects without paying $20/mo per user for Pro.

Alternatives

Cloud Run cron without Cloud Scheduler

Cloud Scheduler costs $0.10 per job per month after the first 3 and asks for OIDC plus IAM bindings on every target. Here is the IAM-free pattern Cloud Run teams use to fire their .run.app URLs on a clock with one bearer token and one dashboard across every GCP project.