Skip to content

CronJobs

A CronJob creates Jobs on a schedule. It is the same run-to-completion model as a Job, with a timer in front of it — and almost all the difficulty is in what happens when the timer and reality disagree.

Reference

Terminology

  • schedule — Standard five-field cron: minute, hour, day of month, month, day of week. "*/5 * * * *" is every five minutes; "0 3 * * *" is 03:00 daily.
  • concurrencyPolicy — What to do if the previous run is still going when the next is due: Allow (default, they overlap), Forbid (skip this one), or Replace (kill the old one and start the new one).
  • startingDeadlineSeconds — How late a missed run may still start. If the controller was down or the cluster was busy, runs older than this are skipped rather than fired in a burst.
  • successfulJobsHistoryLimit / failedJobsHistoryLimit — How many finished Jobs to keep. Defaults are 3 and 1, and they are the reason your logs sometimes disappear.
  • suspend — Set true to stop the schedule without deleting it. The right move during maintenance.
  • timeZone — An IANA name such as Europe/Stockholm. Without it, schedules are interpreted in UTC, which is a classic source of “why did it run at the wrong time?”.

The Trap Worth Understanding

The CronJob controller does not guarantee exactly one run per interval. It guarantees at least the runs you asked for, and only one at a time if you say Forbid.

If the cluster is unavailable for two hours and startingDeadlineSeconds is not set, the controller may start a burst of catch-up runs when it returns — all at once. That is rarely what anyone wants, and it is the reason the field exists.

spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 60
  timeZone: "Europe/Stockholm"

That combination — do not overlap, do not catch up more than a minute late, and run on local time — is what most production schedules should look like.

Exercises

  1. A CronJob every minute.

    kubectl create cronjob tick --image=docker.io/library/busybox --schedule="* * * * *" -- date
    kubectl get cronjob tick
    kubectl get jobs -w

    Wait two minutes and watch the Jobs appear. Then read the logs of one of their Pods.

  2. Compare the concurrency policies. Change the container to sleep 90 so a run takes longer than the interval, then try each policy and watch what happens:

    kubectl patch cronjob tick -p '{"spec":{"concurrencyPolicy":"Allow"}}'
    kubectl patch cronjob tick -p '{"spec":{"concurrencyPolicy":"Forbid"}}'

    With Allow, overlapping Pods accumulate. With Forbid, runs are skipped and the CronJob looks quiet — which is correct, not broken.

  3. Find a run that never happened. Set startingDeadlineSeconds: 1, suspend the CronJob for a few minutes, then unsuspend. Notice that the missed runs are skipped rather than fired. kubectl describe cronjob reports the last schedule time and whether it succeeded.

  4. Look at a real one. Your cluster has CronJobs that back up certificates and renew tokens:

    kubectl get cronjobs -A
    kubectl get cronjob -n kube-system -o custom-columns=NAME:.metadata.name,SCHEDULE:.spec.schedule,SUSPEND:.spec.suspend

Gotchas Worth Knowing

  • Silence is not success. A CronJob that has stopped firing looks exactly like one with nothing to do. Alert on kube_cronjob_status_last_successful_time getting old, rather than assuming.
  • The history limits hide your logs. Only the last few Jobs are kept by default. If you need the output, ship it somewhere durable.
  • Schedules are UTC unless you say otherwise. Set timeZone explicitly.
  • A suspended CronJob is a common cause of a “missing” nightly job, and suspend: true is easy to leave behind.

What to Take Away

  • A CronJob is a schedule that creates Jobs; the interesting behaviour is in the overlaps and the misses.
  • concurrencyPolicy and startingDeadlineSeconds are the two fields that decide what happens when reality interferes with the timetable.
  • Never assume a quiet CronJob is a healthy one — check the last successful run.