Skip to content
LastPing
GUIDE · KUBERNETES · CRONJOB NOT RUNNING

Kubernetes CronJob not running: what the controller skipped, and why

A CronJob that creates no Job has nothing to fail: no pod, no failed Job, no alert. On every schedule the controller decides whether to create one, and suspend, concurrencyPolicy, startingDeadlineSeconds and the 100 missed schedules rule can each make it decide not to. Check each below, then add one ping to the job so a run that never happens opens an incident.

Your assistant can do this for you

Find out why my Kubernetes CronJob is not running, and monitor it with LastPing

Kubernetes' documented behaviour: "If there are more than 100 missed schedules, then it does not start the Job and logs the error." The error reads too many missed start times. Set or decrease .spec.startingDeadlineSeconds or check clock skew. (Kubernetes documentation, CronJob.)
On this page
  1. Check these first: the CronJob's own events
  2. Seven reasons a CronJob does not run
  3. Get alerted when a CronJob does not run
  4. What this catches
  5. Questions people ask

Check these first: the CronJob's own events

The controller records why it did not start a Job. Read that before anything else.

# SUSPEND, ACTIVE and LAST SCHEDULE at a glance
kubectl get cronjob nightly-backup

# The events at the bottom name the reason
kubectl describe cronjob nightly-backup

# Start one run now, outside the schedule
kubectl create job nightly-backup-manual --from=cronjob/nightly-backup

The event names worth knowing: JobAlreadyActive (Forbid is holding the run back), MissSchedule (the starting deadline passed), TooManyMissedTimes, UnknownTimeZone and FailedCreate. Events expire after a while, and by default only the last 3 successful and 1 failed Jobs are kept (successfulJobsHistoryLimit and failedJobsHistoryLimit), so the evidence of an old run may already be gone.

Seven reasons a CronJob does not run

In the order worth checking, quoting the Kubernetes documentation where it is exact.

The CronJob is suspended

With .spec.suspend: true the controller starts no Jobs; runs already started carry on. The SUSPEND column of kubectl get cronjob shows it. The Kubernetes documentation warns that executions suspended during their scheduled time count as missed, and that when suspend changes back to false on a CronJob without a starting deadline, "the missed Jobs are scheduled immediately."

kubectl patch cronjob nightly-backup -p '{"spec":{"suspend":false}}'

concurrencyPolicy: Forbid, behind a Job that never ends

With Forbid, "if it is time for a new Job run and the previous Job run hasn't finished yet, the CronJob skips the new Job run." The event reads Not starting job because prior execution is running and concurrency policy is Forbid. One hung Job blocks every run after it. Give the Job template an activeDeadlineSeconds: when a Job reaches it, its running pods are terminated and the Job fails with reason DeadlineExceeded, so the next run can start.

The run missed its startingDeadlineSeconds

startingDeadlineSeconds is how late a Job may still be started. Past it, the controller skips that run, records Missed scheduled time to start a job, and Kubernetes treats the run as a failed Job; later runs are still scheduled. Keep it at 10 seconds or more: the controller checks every 10 seconds, and a smaller value can mean the CronJob is never scheduled.

Too many missed start times

For each CronJob the controller counts the schedules it missed since the last scheduled time. The documentation says that with more than 100 it does not start the Job and logs too many missed start times. Set or decrease .spec.startingDeadlineSeconds or check clock skew, which also appears as a TooManyMissedTimes warning event. Its own example is a controller that was down for about two hours under a one-minute schedule. With startingDeadlineSeconds set, it counts only the misses inside that window, so the Job starts again.

The schedule is read in another timezone

Without .spec.timeZone, the kube-controller-manager interprets the schedule in its own local time zone. Set .spec.timeZone to an IANA name such as Etc/UTC (stable since Kubernetes v1.27); an unknown name produces an invalid timeZone event. TZ= or CRON_TZ= inside .spec.schedule is rejected with a validation error. The cron expression explainer has a Kubernetes mode that lists the next runs in any zone and marks the daylight saving changes.

The schedule means something else

Day of week runs from 0 to 6, and ? means the same as *. Shortcuts such as @daily and @hourly work. When both day of month and day of week are restricted, the job runs when either matches, and Kubernetes treats a stepped field such as */2 as restricted where Linux cron does not. Check an expression in the explainer before trusting it.

A Job was created, but its pod never ran

The CronJob did its part; the problem is one level down. An image that cannot be pulled, a pod stuck Pending for lack of resources or a node selector that matches nothing all leave a Job with no running pod. List the Job's pods and describe them:

kubectl get jobs
kubectl get pods --selector=batch.kubernetes.io/job-name=<job-name>
kubectl describe pod <pod-name>

Get alerted when a CronJob does not run

Nothing inside the cluster reports a Job that was never created. A ping from the job command, checked against the schedule outside the cluster, reports the silence. No operator, nothing installed.

Create a heartbeat monitor with the same schedule

In the console, create a monitor with the CronJob's spec.schedule, in the zone from spec.timeZone (or the controller manager's zone if it is unset), plus a grace window. LastPing reads the expression with Linux cron rules, so for a stepped day field check the expression in both modes of the explainer. Copy the ping URL.

Ping start, success and failure from the job command

Keep the URL in a Secret (it is a capability URL: anyone who has it can ping the monitor), then wrap the workload. A failure pings /fail and exits non-zero, so Kubernetes' own retries still apply:

spec:
  schedule: "0 3 * * *"
  timeZone: "Etc/UTC"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      activeDeadlineSeconds: 3600
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: backup
              image: your-registry/backup:latest
              env:
                - name: LASTPING_URL
                  valueFrom:
                    secretKeyRef:
                      name: lastping
                      key: ping-url
              command:
                - /bin/sh
                - -c
                - |
                  curl -fsS -m 10 --retry 3 -o /dev/null "$LASTPING_URL/start"
                  if /app/backup.sh; then
                    curl -fsS -m 10 --retry 3 -o /dev/null "$LASTPING_URL"
                  else
                    curl -fsS -m 10 --retry 3 -o /dev/null "$LASTPING_URL/fail"
                    exit 1
                  fi

The Kubernetes CronJob guide has the full manifest, a busybox wget variant for images without curl, and the Secret.

What this catches

The job reports what it can; the missing ping reports the rest.

  • No Job was created: suspended, held back by Forbid, past its starting deadline, over 100 missed schedules or scheduled in the wrong zone. No ping in the window opens an incident.
  • The pod never ran: an image that will not pull or a pod stuck Pending sends no ping either, so the same silence raises the alert.
  • The run hung: the start ping arrived and the finish did not, so overrun detection opens an incident while it is still running.
  • The workload failed: the /fail ping alerts at once, and the non-zero exit still lets Kubernetes retry.
  • Recovery: the next successful run closes the incident and sends a recovery notice.

Kubernetes says plainly that a CronJob creates a Job "approximately once per execution time of its schedule", and that in some circumstances two Jobs might be created, or none, so make the job idempotent. The monitor tells you about the "none".

Questions people ask

Front-loaded answers: the most important fact first.

  • Why is my Kubernetes CronJob not running?

    Run kubectl describe cronjob and read the events at the bottom. The usual reasons: suspend is true; concurrencyPolicy is Forbid and an earlier Job is still running; the run missed its startingDeadlineSeconds; the controller counted more than 100 missed schedules; or the schedule is read in another timezone than you expect. If a Job was created but nothing ran, look at the Job's pods instead.

  • What does too many missed start times mean?

    The CronJob controller counts the schedules it missed since the last scheduled time. The Kubernetes documentation says that with more than 100 it does not start the Job and logs too many missed start times. Set or decrease .spec.startingDeadlineSeconds or check clock skew. With startingDeadlineSeconds set, it counts only the misses inside that window, so setting it lets the Job start again.

  • What does startingDeadlineSeconds do?

    It is how many seconds after its scheduled time a Job may still be started. Past it, the controller skips that run and Kubernetes treats it as a failed Job; later runs are still scheduled. Without it, runs have no deadline. Keep it at 10 seconds or more, because the controller checks every 10 seconds.

  • Why does concurrencyPolicy Forbid stop my CronJob?

    With Forbid, a new run is skipped while the previous Job is still running, and the skipped run counts as missed. One hung Job therefore blocks every run after it. Set activeDeadlineSeconds on the Job so a stuck run is terminated, and monitor the runs so the first skip alerts you.

  • What happens when I unsuspend a CronJob?

    Runs whose scheduled time fell while it was suspended count as missed. If the CronJob has no startingDeadlineSeconds, the missed Jobs are scheduled immediately when suspend changes from true to false. Set a starting deadline first if catch-up runs would do harm.

  • What timezone does a Kubernetes CronJob use?

    Without .spec.timeZone, the kube-controller-manager reads the schedule in its own local time zone. Set .spec.timeZone to an IANA name such as Etc/UTC; the field is stable since Kubernetes v1.27, and TZ or CRON_TZ inside the schedule is rejected. The cron expression explainer lists the next runs in any zone.

  • How do I get alerted when a CronJob does not run?

    Nothing inside the cluster reports a Job that was never created. Add a ping to the job command and give a LastPing monitor the same schedule, timezone and a grace window: a run that never starts sends no ping and opens an incident, and a /fail ping alerts at once.

  • Is this monitoring free?

    Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

kubectl won't tell you about the Job that was never created. This will.

One ping in the manifest now beats reading expired events later. First alert wired up in minutes, and LastPing monitors itself the same way.