Job Completions, Parallelism and backoffLimit
Three fields decide how a Job behaves: `completions` (how many successes are needed), `parallelism` (how many at once) and `backoffLimit` (how many failures before giving up). The third is the one that surprises people, because a backoffLimit of two produces three Pods. Both a successful batch and a failing one are run here, and the failure is read out of the Job's conditions.
Application Design and Build Guide 10 of 44 Beginner
- Kubernetes1.36.4
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 14 min
- Reviewed23 August 2026
Written against the versions above. With `restartPolicy: Never` each attempt is a NEW Pod, and the failed Pods are kept so you can read their logs. With `restartPolicy: OnFailure` the same Pod's container is restarted in place and you get one Pod with a restart count instead - the same retry budget, counted differently.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster and kubectl.
- The session creates namespace
ckad-job, a Jobbatchwith four completions two at a time, and a Jobdoomedwhose container always exits 1.
-
Four completions, two at a time
completions: 4 parallelism: 2Four successful runs are required and two may run at once, so the controller keeps two Pods alive until four have succeeded:
NAME STATUS COMPLETIONS DURATION AGE batch Complete 4/4 ...The Pods spread across the workers, and each logged its own hostname - four distinct Pod names, which is what
completions: 4means. Setcompletionswithoutparallelismand they run one at a time; setparallelismwithoutcompletionsand any single success finishes the Job.bash Example session kubectl create namespace ckad-jobnamespace/ckad-job createdkubectl -n ckad-job wait --for=condition=Complete job/batch --timeout=180sjob.batch/batch condition metkubectl -n ckad-job get job batchNAME STATUS COMPLETIONS DURATION AGEbatch Complete 4/4 12s 12skubectl -n ckad-job get pods -l batch.kubernetes.io/job-name=batch -o 'custom-columns=POD:.metadata.name,PHASE:.status.phase,NODE:.spec.nodeName'POD PHASE NODEbatch-dphlg Succeeded cka1001-node01batch-qszkv Succeeded cka1001-node01batch-tvbj7 Succeeded cka1001-node01batch-wgwjz Succeeded cka1001-node01kubectl -n ckad-job logs -l batch.kubernetes.io/job-name=batch --tail=1done on batch-dphlgdone on batch-qszkvdone on batch-tvbj7done on batch-wgwjzExpected result
Complete 4/4and four Pods across the workers.Success conditionYou can express "run this N times, M at a time" correctly.
-
One that gives up
backoffLimit: 2with a container that always exits 1. After forty-five seconds:NAME STATUS COMPLETIONS DURATION AGE doomed Failed 0/1 45s 45sand three Pods, not two:
POD PHASE EXIT doomed-ddzh4 Failed 1 doomed-jh556 Failed 1 doomed-r5n2s Failed 1This is the detail worth carrying:
backoffLimitcounts retries after the first attempt. One initial run plus two retries is three Pods. Set it to 0 and you get exactly one attempt.The retries are also spaced out - 10s, 20s, 40s, doubling to a six-minute cap - which is why a Job with a high backoffLimit can take a very long time to declare failure.
bash Example session sleep 45; kubectl -n ckad-job get job doomedNAME STATUS COMPLETIONS DURATION AGEdoomed Failed 0/1 45s 45skubectl -n ckad-job get pods -l batch.kubernetes.io/job-name=doomed -o 'custom-columns=POD:.metadata.name,PHASE:.status.phase,EXIT:.status.containerStatuses[0].state.terminated.exitCode'POD PHASE EXITdoomed-ddzh4 Failed 1doomed-jh556 Failed 1doomed-r5n2s Failed 1Expected result
Failed, and three Pods each with exit code 1.Success conditionYou can predict how many attempts a backoffLimit produces.
-
Reading why it stopped
The Job's own conditions say it in one line:
FAILED CONDITION REASON 3 FailureTarget,Failed BackoffLimitExceeded,BackoffLimitExceededBackoffLimitExceeded- the Job did not fail because of what the container did, it failed because it ran out of retries. That distinction matters when you are deciding whether to raise the limit or fix the code.And because
restartPolicy: Neverkeeps every failed Pod, the evidence is still there:attempt on doomed-ddzh4 attempt on doomed-jh556 attempt on doomed-r5n2sThree Pods, three sets of logs, one label selector. On a Job that failed hours ago this is usually the fastest way in.
bash Example session kubectl -n ckad-job get job doomed -o 'custom-columns=FAILED:.status.failed,CONDITION:.status.conditions[*].type,REASON:.status.conditions[*].reason'FAILED CONDITION REASON3 FailureTarget,Failed BackoffLimitExceeded,BackoffLimitExceededkubectl -n ckad-job logs -l batch.kubernetes.io/job-name=doomed --tail=1attempt on doomed-ddzh4attempt on doomed-jh556attempt on doomed-r5n2sExpected result
BackoffLimitExceededand the logs of all three attempts.Success conditionYou can tell "the code is broken" from "the retry budget ran out".
-
restartPolicy decides what you are counting
Never3With
Never, each attempt is a fresh Pod and they accumulate - three attempts, three Pods. WithOnFailure, the kubelet restarts the container inside the same Pod and you see one Pod withRESTARTS 3instead.Which to choose:
Neverwhen you want the logs of every attempt separately, which is most of the time during development.OnFailurewhen the retries are expected and you do not want dozens of dead Pods.
A Job's template may only use those two.
restartPolicy: Alwaysis rejected outright - it would mean the Pod can never finish, which is the same problem guide 22 runs into from the other direction.Finally, nothing cleans up finished Jobs by default.
ttlSecondsAfterFinishedon the Job deletes it and its Pods a set time after it finishes, and on a cluster that runs batch work it is worth setting.bash Example session kubectl -n ckad-job get job doomed -o jsonpath='{.spec.template.spec.restartPolicy}'Neverkubectl -n ckad-job get pods -l batch.kubernetes.io/job-name=doomed --no-headers | wc -l3kubectl delete namespace ckad-job --wait=falsenamespace "ckad-job" deletedExpected result
Never, and a count of three Pods.Success conditionYou can choose a restartPolicy for the debugging you expect to do.
Troubleshooting
A Job made more Pods than
backoffLimit.Why: The limit counts retries, not total attempts.
Fix:Expected: attempts = backoffLimit + 1. Use
backoffLimit: 0for exactly one try.A failing Job takes a very long time to be marked Failed.
Why: Retries back off exponentially, up to six minutes apart.
Fix:Lower
backoffLimit, or setactiveDeadlineSecondsto cap the whole Job.The failed Pods are gone and you cannot read the logs.
Why:
restartPolicy: OnFailurerestarts in place; or a TTL deleted them.Fix:Use
restartPolicy: Neverwhile debugging. CheckttlSecondsAfterFinished.restartPolicy: Alwaysis rejected on a Job.Why: A Pod that always restarts can never complete.
Fix:Use
NeverorOnFailure.