Cron Job Troubleshooting Guide

Cron jobs fail in spectacular and quiet ways. The spectacular failures — a missing binary, a syntax error in the crontab, a Kubernetes pod that won't schedule — are easy to find because something obviously went wrong. The quiet failures are worse: the job "runs", the exit code is 0, but the actual work didn't happen. This guide walks through the categories of cron problems I have debugged most often, with the diagnostic steps that usually surface the root cause.

My Cron Job Doesn't Run At All

Start by confirming that cron itself is even attempting to fire your schedule. On most Linux distributions, cron logs every job invocation to syslog:

# Debian / Ubuntu
grep CRON /var/log/syslog | tail -50

# RHEL / Amazon Linux / CentOS
grep CROND /var/log/cron | tail -50

# systemd-based distros
journalctl -u cron --since "1 hour ago"

If you see lines like (USER) CMD (/path/to/script.sh) at the expected times, cron is firing your job and the problem lies inside the script. If those lines are missing, cron is not seeing the schedule at all. The most common reasons:

  • The cron daemon is stopped. Check with systemctl status cron (Debian) or systemctl status crond (RHEL). On a fresh container image, the daemon often isn't enabled by default.
  • The crontab has a syntax error. crontab -l will show what cron actually loaded. Any line cron rejects is logged with the parsing error.
  • The schedule is in the wrong field. A typo like 0 * 9 * 1-5 instead of 0 9 * * 1-5 moves the "9" into the day-of-month slot, which means the job runs at minute 0 of every hour, but only on the 9th of the month. Validate every new schedule against CronWizard's human-readable translation before deploying.
  • You edited the wrong crontab. crontab -e as root edits root's crontab; as a regular user, it edits that user's. /etc/crontab and /etc/cron.d/* require an extra user field. Confirm whoami matches the crontab you intended to edit.

It Works When I Run It Manually but Not From Cron

This is by far the most common failure mode in practice. Cron does not run your job in your interactive shell. It runs it with a minimal environment that contains almost nothing. Variables you take for granted — PATH, HOME, LANG, anything sourced from ~/.bashrc — are not set.

The fix is to make the script self-sufficient:

#!/bin/bash
set -euo pipefail

# Pin PATH explicitly. Cron's default is often just /usr/bin:/bin
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Source the env file your application uses
set -a
source /opt/myapp/.env
set +a

# Use absolute paths for every binary
/usr/bin/python3 /opt/myapp/jobs/daily_report.py

A useful debugging trick is to dump the cron environment to a file once and inspect it:

* * * * * env > /tmp/cron-env.txt 2>&1

Wait a minute, then cat /tmp/cron-env.txt. The difference between that file and env in your interactive shell is the gap your script has to fill in.

It Runs at the Wrong Time

This always — always — turns out to be a timezone problem. Cron does not have an opinion about timezones; it uses whatever the host's system timezone is. There are several layers where that can be set:

  1. The TZ environment variable in the crontab line itself, which takes precedence: TZ=Europe/Istanbul 0 9 * * 1-5 /opt/bin/job.sh.
  2. The crontab's CRON_TZ directive at the top of the file: CRON_TZ=UTC applies to all subsequent entries.
  3. The host's system timezone (/etc/timezone on Debian, the /etc/localtime symlink on most distros).
  4. For Kubernetes CronJobs (1.27+), the .spec.timeZone field. Without it, the cluster-wide controller-manager timezone is used, which is almost always UTC.

Diagnose by running date inside the same context as the cron job:

* * * * * date >> /tmp/cron-date.log

If date reports a different timezone than you expected, that is the source of the wrong-time bug. Once you know what cron thinks the time is, adjusting the schedule (or pinning a timezone) is mechanical.

It Runs but the Output Is Lost

By default, cron emails the output of every job to the user's local mailbox. Most modern systems don't have a working mail transfer agent, which means the output is silently dropped. To capture output explicitly, redirect both stdout and stderr to a log file:

0 2 * * * /opt/bin/backup.sh >> /var/log/myapp/backup.log 2>&1

The 2>&1 is the critical part. Without it, anything written to stderr (which is where most error messages go) vanishes. Pair the log file with logrotate so it doesn't grow forever.

On Kubernetes, container logs are captured automatically by the kubelet; just make sure your job writes to stdout/stderr instead of an in-container log file that gets discarded when the pod terminates.

It Runs Twice (or More)

Duplicate runs usually fall into one of these categories:

  • The same schedule installed twice. Check crontab -l for every user, plus /etc/crontab, /etc/cron.d/, and /etc/cron.{hourly,daily,weekly,monthly}/. Old deployments left behind in /etc/cron.d/ are a classic source of mystery duplicates.
  • Multiple replicas without locking. If you run cron inside a Kubernetes Deployment with replicas > 1, every replica fires the schedule independently. The right tool for this is a Kubernetes CronJob (which schedules one Job per fire) plus concurrencyPolicy: Forbid.
  • A long-running job overlapping itself. A 5-minute schedule that takes 7 minutes will overlap. See the best practices guide for locking patterns that prevent this.

Kubernetes CronJob-Specific Failures

Kubernetes CronJobs add their own layer of failure modes. The diagnostic flow is:

  1. kubectl get cronjob <name> — confirm LAST SCHEDULE matches what you expect.
  2. kubectl describe cronjob <name> — look at Events. Most scheduling failures (quota, missing service account, invalid pod spec) appear here.
  3. kubectl get jobs --selector=cronjob-name=<name> — list the Job objects the CronJob created. Each one corresponds to one schedule fire.
  4. kubectl logs job/<job-name> — read the actual container logs.

A common silent failure is hitting the successfulJobsHistoryLimit or failedJobsHistoryLimit defaults (3 and 1 respectively). After three successful runs, the oldest Job is garbage-collected, taking its logs with it. For jobs you actually need to debug after the fact, raise these limits or ship logs to a long-term store.

For a deeper dive into Kubernetes CronJob behavior, including theconcurrencyPolicy, startingDeadlineSeconds, and timezone handling, see the dedicated Kubernetes CronJob guide.

Frequently Asked Questions

Why is my cron job not running at all?

The most common reasons are: (1) the cron daemon is not running on the host, (2) the crontab file has a syntax error and was rejected, (3) the user the crontab belongs to does not have permission to run the command, or (4) the schedule is correct but the timezone is different from what you expect. Check the system log (/var/log/syslog or journalctl -u cron) first — cron almost always logs the reason it skipped a run.

Why does my cron job work when I run it manually but not when cron runs it?

Cron runs your script with a minimal environment — no PATH, no shell aliases, no profile sourcing. Use absolute paths to every binary (/usr/bin/python3, not python3), set PATH explicitly at the top of your script or crontab, and source any required environment files (e.g. source /etc/environment) before doing anything else.

Why is my cron job running at the wrong time?

Almost always a timezone mismatch. Cron uses the system timezone of the host process, which in containers is usually UTC. If you wrote the schedule for your local timezone, you need to either change the host timezone (TZ environment variable, /etc/timezone) or convert the schedule to UTC. On Kubernetes 1.27+, set spec.timeZone on the CronJob.

Why is my Kubernetes CronJob suspended automatically?

Kubernetes auto-suspends CronJobs that miss too many start times (default startingDeadlineSeconds combined with a slow scheduler). Check the .status.lastScheduleTime and conditions on the CronJob. Common causes are a controller-manager outage, a CronJob with a startingDeadlineSeconds shorter than the schedule, or a resource quota that prevented Job creation.

Why did my cron job run twice within seconds?

A few possibilities: (1) you have the same crontab installed for two different users, (2) the schedule is duplicated across /etc/crontab and /etc/cron.d/, (3) on Kubernetes, concurrencyPolicy is Allow and a previous run was still in progress, or (4) you ran a manual invocation that overlapped with the scheduled one. Greppping the host for the script name usually finds the duplicate quickly.