Run a CronJob
Most recurring work is idle most of the time. A nightly report runs for four minutes, and then a machine sits idle for the remaining twenty-three hours and fifty-six minutes, doing nothing but incurring cost. A CronJob paired with a burst pool closes that gap: the pool holds no nodes until the schedule fires, the autoscaler brings one up to run the Job, and removes it again once the Job finishes. You pay only for the four minutes.
Idle-node fit#
Point the CronJob at a worker pool that is allowed to scale to zero nodes , and the lifecycle takes care of itself.
flowchart LR A[Schedule fires] --> B[Job created, pod Pending] B --> C[Autoscaler provisions a node] C --> D[Job runs to completion] D --> E[Pool scales back to zero]
Between runs there is nothing to pay for and nothing to patch. This is the autoscaler managing capacity, not Knative's request-driven scale-to-zero: here the trigger is the clock, not incoming traffic. 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#
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." fires 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.Forbidskips 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.Replaceterminates 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 cleanup 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 2 a.m. 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.
Run a Job
Run a task that runs to completion once and reports success or failure, and design it to survive being evicted mid-run.
Control retries and clean up finished jobs
Finished Jobs quietly bloat etcd and slow the API server. Set a TTL so they clean themselves up, and bound retries so a failing Job cannot loop forever.