Skip to main content

Most recurring work is idle between runs. A nightly report might run for four minutes, leaving the node it needs paid for but unused the rest of the day. A CronJob paired with a burst pool avoids this: the pool holds no nodes until the schedule fires, the autoscaler adds one to run the Job, then removes it once the Job finishes. You pay only for the minutes the Job runs.

Idle-node fit 

Point the CronJob at a worker pool that is allowed to , and the lifecycle takes care of itself.

flowchart LR
  A["Schedule fires"]:::app --> B["Job created, pod Pending"]:::app
  B --> C["Autoscaler provisions a node"]:::platform
  C --> D["Job runs to completion"]:::app
  D --> E["Pool scales back to zero"]:::platform

Between runs there is no reason to have a node running. This is the managing capacity. The one delay to plan for is provisioning: a fresh node takes a couple of minutes to come up, so a job that must start exactly on the second needs a node already warm.

Schedule and timezone 

cronjob.yamlyaml
				apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *" # 02:00 every day
  timeZone: "Europe/Berlin" # without this, the schedule is UTC
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never # batch pods run once; never restart in place
          containers:
            - name: report
              image: your/report:tag
			

The schedule field is ordinary cron: minute, hour, day-of-month, month, day-of-week. Set timeZone explicitly. Leave it off and the schedule runs in UTC, so a job you wrote as "2 a.m." starts at the wrong local hour, and drifts by an hour again at every daylight-saving change.

Control overlap with concurrencyPolicy 

A schedule says nothing about what happens when a run is still in progress and the next one comes due. concurrencyPolicy decides:

  • Allow (default) starts the next run regardless, so two can overlap.
  • Forbid skips the new run until the previous one finishes. This is what most backups and syncs need: two copies writing at once is how state becomes corrupted.
  • Replace terminates the running one and starts a fresh run.

Missed runs and history 

If the cluster or the CronJob controller was down when a run was due, startingDeadlineSeconds governs the recovery. A run that is more than that many seconds late is skipped rather than fired, so a brief outage does not end with the controller launching a backlog of catch-up jobs all at once when it recovers.

Finished Jobs remain for inspection, capped by successfulJobsHistoryLimit and failedJobsHistoryLimit (default 3 and 1). Keep them low; see for why finished objects should not accumulate. When you need to stop a schedule without deleting it (a maintenance window, a paused pipeline), set suspend: true and re-enable it later.

Trigger a run without waiting 

You do not have to wait until the designated time to confirm it works. Trigger a Job directly from the CronJob spec:

				$ kubectl create job --from=cronjob/nightly-report test-run
			

That runs the exact jobTemplate on demand. Watch it land on a freshly provisioned node, complete, and let the pool drain back to zero: the whole idle-node lifecycle, on your schedule instead of the cron one.