CronJob Schedule and Concurrency

How do you run a Job on a repeating schedule instead of on demand? A CronJob holds a cron expression and, each time the clock matches, creates a Job from its template, which runs a Pod. It also decides what happens when a run is still going at the next tick, prunes finished Jobs, and starts the latest run missed while it was down if startingDeadlineSeconds allows, warning when more than 100 ticks were missed. A CronJob never creates a Pod itself, its Jobs do.

Play the animation 6 steps, animated, best on a wide screen

CronJob Schedule and Concurrency, Kubernetes diagram

CronJob Schedule and Concurrency, step by step

  1. At 12:00 the wall clock matches the schedule, and the controller creates one Job, backup-28394640, from spec.jobTemplate. That Job in turn starts its own Pod. The path is always CronJob then Job then Pod, never CronJob straight to Pod. The numeric suffix is the scheduled time in minutes since the Unix epoch, so a repeated create for one tick collides on the name instead of adding a second run.

    Play from step 1
  2. The 12:00 backup is slow and still Running when the 12:05 tick arrives. The spec.concurrencyPolicy field decides what happens to overlapping runs. With Forbid the controller skips this tick and records the Event JobAlreadyActive. The default Allow would let a second Job start alongside the first, and Replace would delete the still-running Job and start a fresh one in its place. Once the 12:00 run finishes, that skipped tick can still start if it is inside startingDeadlineSeconds.

    Play from step 2
  3. By 12:10 the 12:00 run has finished, so there is no overlap left to forbid and the schedule simply runs. The 12:10, 12:15 and 12:20 ticks each create a Job of their own, and each of those Jobs starts its own Pod. Runs are never reused and one tick never feeds another: each of these ticks gets a Job of its own, and the skipped 12:05 tick, by now past its deadline, gets none.

    Play from step 3
  4. Finished Jobs pile up, so the controller caps how many it keeps with successfulJobsHistoryLimit, which defaults to 3, and failedJobsHistoryLimit, which defaults to 1. Once a fourth successful Job exists it deletes the oldest, here backup-28394640, and that Job takes its Pod with it. Trimming history is why kubectl get jobs shows only the most recent runs of a schedule.

    Play from step 4
  5. The controller was down at 12:25, and on recovery finds a missed tick. The startingDeadlineSeconds field bounds how late a missed run may still start, and a tick older than that is skipped without an Event, not run late. Past 100 missed start times, counted inside the deadline if set, the controller records a TooManyMissedTimes warning and still starts the latest tick. A CronJob is not exactly-once and may rarely create two Jobs or none for a tick, so the Job should be idempotent.

    Play from step 5
  6. Setting spec.suspend=true pauses the CronJob. The clock keeps advancing and 12:30 still matches the schedule, but the controller creates no new Jobs while it is suspended, and a Job already running is left to finish on its own. Clearing the flag back to false resumes creation, and only the latest tick missed while suspended starts immediately, if startingDeadlineSeconds allows. This pauses a schedule without deleting the CronJob and losing its history.

    Play from step 6

Sources