Run a CronJob
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 scale to zero nodes , 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 cluster autoscaler 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
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.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 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.
Run a Job
Run a task that runs to completion on Syself Autopilot, and design it to survive node drains, run in the right runtime, land on the right node type, and scale on demand.
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.