Skip to content

Jobs and CronJobs

A Job runs one or more Pods until a task finishes successfully rather than keeping them running forever, and a CronJob creates Jobs automatically on a cron-formatted schedule.

A Deployment’s Pods are meant to run indefinitely; a Job’s Pods are meant to finish. A Job tracks successful completions and retries failures up to a limit, instead of restarting a Pod forever the way a long-running controller would.

apiVersion: batch/v1
kind: Job
metadata:
name: report-backfill
spec:
completions: 5
parallelism: 2
backoffLimit: 3
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: OnFailure
containers:
- name: backfill
image: myregistry/report-backfill:1.0.0
args: ['--month=2026-06']

completions: 5 means the Job is not done until 5 Pods have exited successfully; parallelism: 2 caps how many of those Pods run at once; backoffLimit: 3 is how many failed attempts the Job tolerates before it gives up and marks itself Failed; activeDeadlineSeconds: 600 is an overall wall-clock timeout for the whole Job, after which it is terminated regardless of completions. Notice restartPolicy: OnFailure — a Job’s Pod template must use OnFailure or Never, never Always, because a Job’s entire point is to stop once the work is done.

Terminal window
# Watch completions accumulate, then check the Job's final status
kubectl get job report-backfill --watch
kubectl describe job report-backfill
# Logs from a finished Job's Pods
kubectl logs -l job-name=report-backfill --tail=100

A CronJob does not run Pods directly — it creates a Job on a schedule, and that Job creates the Pods, exactly as if you had submitted the Job by hand at that moment.

apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-report
spec:
schedule: '0 2 * * *'
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: report
image: myregistry/nightly-report:1.0.0

schedule: '0 2 * * *' runs at 02:00 every day, in standard cron syntax. concurrencyPolicy decides what happens if a previous run is still going when the next one is due: Allow (the default) runs them side by side, Forbid skips the new run entirely, and Replace cancels the still-running Job and starts the new one in its place. startingDeadlineSeconds bounds how late a missed run is still allowed to start. successfulJobsHistoryLimit and failedJobsHistoryLimit control how many old Job objects the CronJob keeps around for inspection before garbage-collecting them.

flowchart LR
  cj["CronJob nightly-report (schedule: 0 2 * * *)"] -->|creates| j["Job nightly-report-29102026"]
  j -->|creates| p1["Pod (runs to completion)"]
  j -->|creates| p2["Pod (runs to completion)"]
  p1 --> done["Succeeded, Pods stop"]
  p2 --> done
A CronJob creates a Job on its schedule; the Job creates Pods that run to completion
Terminal window
# List CronJobs and the Jobs they have created
kubectl get cronjob nightly-report
kubectl get jobs --selector=job-name
# Trigger one ad-hoc run right now, without waiting for the schedule
kubectl create job nightly-report-manual --from=cronjob/nightly-report

Because a CronJob’s schedule can fire many times, successfulJobsHistoryLimit/failedJobsHistoryLimit matter in practice: without them old Job and Pod objects would accumulate forever every night.

How does a Job's workload model differ from a Deployment's?
What does backoffLimit control on a Job?
Which restartPolicy values are valid for a Job's Pod template?
What does concurrencyPolicy: Forbid do on a CronJob?