Most CKAD candidates learn Jobs as four fields — completions, parallelism, backoffLimit, and restartPolicy — and stop there. That covers the bulk of what the exam asks, and our CKAD Jobs and CronJobs guide walks through it in full. But the Job API has grown considerably in recent Kubernetes releases, and the newer fields are now stable, documented on kubernetes.io, and fair game for any task that says “use the workload resource that fits”.
This guide covers the part of the Job API the basic guide leaves out: Indexed Jobs, pod failure policy, per-index backoff, success policy, and suspended Jobs. These are the fields that decide which Pod does which piece of work, and what a failure actually means. We’ll keep it practical — YAML you can type, commands to verify it, and the validation rules that make a manifest get rejected.
A note on exam weighting, stated honestly: the CKAD curriculum lists “understand Jobs and CronJobs” under Application Design and Build without enumerating individual fields. The core four fields are the most likely to be tested. Treat this article as depth that makes you faster and more confident with the whole API — and as genuinely useful knowledge for running batch workloads in production — rather than a list of guaranteed questions.
Where the Basic Job Model Runs Out
A plain Job with completions: 5 runs Pods until five succeed. Every Pod is interchangeable. That creates three practical problems:
- No work assignment. If you have five shards of data to process, each Pod has no built-in way to know which shard is its own. You need an external queue, or you end up creating five separate Jobs.
- All failures are equal. A Pod that exits with a code meaning “the input is corrupt, retrying is pointless” is retried just like a Pod that was evicted during a node drain. The first wastes compute; the second unfairly burns
backoffLimit. - One bad shard sinks everything.
backoffLimitis a single budget for the whole Job. One index that fails repeatedly can exhaust it and fail the Job while the other shards were perfectly healthy.
The newer fields solve each problem in turn:
| Problem | Field | What it does |
|---|---|---|
| Pods need to know which piece of work is theirs | completionMode: Indexed | Gives each Pod a stable index from 0 to completions minus 1 |
| Some failures should not be retried, others should not be counted | podFailurePolicy | Matches exit codes or Pod conditions to an action |
| One failing index should not exhaust the shared retry budget | backoffLimitPerIndex | Gives each index its own retry budget |
| The Job is “done” before every index finishes | successPolicy | Declares the Job successful when chosen indexes succeed |
| The Job should exist but not run yet | suspend | Creates the Job without creating Pods |
Indexed Jobs: completionMode
A Job has a completionMode of either NonIndexed (the default) or Indexed. In Indexed mode, the Job controller assigns each Pod a completion index from 0 to completions - 1, and the Job is complete when there is one successful Pod for every index.
apiVersion: batch/v1
kind: Job
metadata:
name: shard-processor
spec:
completionMode: Indexed
completions: 4
parallelism: 2
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo processing shard $JOB_COMPLETION_INDEX; sleep 5"]
There is no imperative flag for this. The fast path on the exam is to generate a skeleton and edit it:
kubectl create job shard-processor --image=busybox:1.36 \
--dry-run=client -o yaml -- sh -c 'echo processing shard $JOB_COMPLETION_INDEX' > job.yaml
# add completionMode, completions, parallelism under spec, then:
kubectl apply -f job.yaml
Note the single quotes around the shell command — with double quotes your own shell would expand $JOB_COMPLETION_INDEX to an empty string before kubectl ever sees it.
How a Pod Learns Its Index
The index is exposed four ways, and knowing all of them lets you answer any “make the index available to the container” variant:
| Mechanism | Where it appears | Notes |
|---|---|---|
| Environment variable | JOB_COMPLETION_INDEX | Injected automatically into every container; no manifest change needed |
| Annotation | batch.kubernetes.io/job-completion-index | Set on the Pod by the Job controller |
| Label | batch.kubernetes.io/job-completion-index | Available in recent releases; lets you select Pods by index |
| Hostname | <job-name>-<index> | The Pod hostname follows this pattern |
Because the index is an annotation, you can also project it into a file with the Downward API — the same technique covered in our Downward API guide:
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: index
fieldRef:
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
Verifying an Indexed Job
kubectl get job shard-processor
kubectl get pods -l job-name=shard-processor \
-L batch.kubernetes.io/job-completion-index
kubectl get job shard-processor -o jsonpath='{.status.completedIndexes}{"\n"}'
status.completedIndexes is a compact string such as 0-3 or 0,2-3. Pod names look like shard-processor-0-abcde — the index, then a random suffix. If index 1 fails and is retried, you’ll see a second Pod whose name also starts with shard-processor-1-.
Rules to Remember
completionsis required in Indexed mode. Omitting it gets the manifest rejected by the API server.completionModeis immutable once the Job is created. To change it, delete and recreate the Job.parallelismstill caps concurrency. Withcompletions: 4andparallelism: 2, two indexes run at a time.- Combined with a headless Service whose selector matches the Job’s Pods and a
subdomainin the Pod template, the predictable hostnames give each index a stable DNS name — the pattern used when worker Pods must find each other.
Pod Failure Policy: Deciding What a Failure Means
By default, every Pod failure counts against backoffLimit, and the Job controller recreates Pods with an exponential back-off delay (10s, 20s, 40s, capped at six minutes). podFailurePolicy lets you override that behavior with an ordered list of rules.
apiVersion: batch/v1
kind: Job
metadata:
name: importer
spec:
completions: 6
parallelism: 2
backoffLimit: 4
podFailurePolicy:
rules:
- action: FailJob
onExitCodes:
containerName: main
operator: In
values: [42]
- action: Ignore
onPodConditions:
- type: DisruptionTarget
template:
spec:
restartPolicy: Never
containers:
- name: main
image: busybox:1.36
command: ["sh", "-c", "exit 42"]
Read it top to bottom:
- If the
maincontainer exits with code 42, the application is telling us the failure is not retriable. Fail the whole Job immediately and terminate any other running Pods. - If the Pod failed because of a disruption — preemption, an API-initiated eviction such as
kubectl drain, or a taint-based eviction — ignore it. Recreate the Pod without spending any ofbackoffLimit. - Anything else falls through to default handling and counts against
backoffLimit.
The Four Actions
| Action | Effect | Typical use |
|---|---|---|
FailJob | Marks the entire Job as Failed and terminates all running Pods | Non-retriable errors: bad config, corrupt input |
Ignore | Does not count toward backoffLimit; a replacement Pod is created | Infrastructure disruptions that are not the app’s fault |
Count | Counts toward backoffLimit (the default behavior, made explicit) | Documenting intent, or matching before a broader rule |
FailIndex | Fails only the affected index without further retries | Indexed Jobs using backoffLimitPerIndex |
The Two Match Types
Each rule uses exactly one of:
onExitCodes— matches container exit codes.operatorisInorNotIn,valuesis a list of integers, andcontainerNameis optional (omit it to match any container in the Pod, including init containers).onPodConditions— matches Pod conditions bytype.DisruptionTargetis the one you’ll use in practice;statusdefaults toTrue.
Validation Rules That Get Manifests Rejected
These are the details that cost time when a kubectl apply fails:
restartPolicymust beNever. WithOnFailure, the kubelet restarts the container inside the same Pod and the Job controller never sees a failed Pod to evaluate. The API server rejects the combination outright.0is not allowed invalueswhen the operator isIn. Exit code 0 is success, so a failure rule can never match it.- A rule has
onExitCodesoronPodConditions, not both. - Rules are evaluated in order, first match wins. Put the most specific rules first.
FailIndexrequiresbackoffLimitPerIndexto be set, which in turn requires Indexed mode.
Seeing It Work
kubectl apply -f importer.yaml
kubectl get job importer
kubectl describe job importer | grep -A6 Conditions
kubectl get job importer \
-o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.reason}{"\n"}{end}'
A Job stopped by a FailJob rule shows a Failed condition with reason PodFailurePolicy — distinct from BackoffLimitExceeded (retry budget exhausted) and DeadlineExceeded (activeDeadlineSeconds hit). Reading that reason is the quickest way to answer “why did this Job fail?” in a troubleshooting task; the same read-the-conditions habit applies across the scenarios in our CKAD application troubleshooting guide.
Per-Index Backoff: backoffLimitPerIndex
In an Indexed Job, a single shared backoffLimit is a blunt tool: one poisoned index can burn the entire budget. backoffLimitPerIndex gives each index its own retry budget, so the Job keeps working through the healthy indexes.
apiVersion: batch/v1
kind: Job
metadata:
name: per-index
spec:
completionMode: Indexed
completions: 10
parallelism: 3
backoffLimitPerIndex: 1
maxFailedIndexes: 5
podFailurePolicy:
rules:
- action: FailIndex
onExitCodes:
operator: In
values: [42]
template:
spec:
restartPolicy: Never
containers:
- name: main
image: python:3.12-alpine
command:
- python3
- -c
- |
import os, sys
if int(os.environ["JOB_COMPLETION_INDEX"]) % 2 == 0:
sys.exit(1)
How this behaves:
- Each index may fail once and be retried once. A second failure marks that index as failed.
- An exit code of 42 skips the retries for that index entirely (
FailIndex). - If more than 5 indexes fail, the Job stops early and is marked Failed (
maxFailedIndexes). - Without
maxFailedIndexes, the Job runs every index to its conclusion before reporting a result.
The Job as a whole still ends as Failed if any index failed — but you get the results of every index that could succeed, and the status tells you exactly which ones did not:
kubectl get job per-index -o jsonpath='{.status.completedIndexes}{"\n"}{.status.failedIndexes}{"\n"}'
# 1,3,5,7,9
# 0,2,4,6,8
Requirements: completionMode: Indexed and restartPolicy: Never. When backoffLimitPerIndex is set, you normally leave backoffLimit unset.
Success Policy: Done Before Every Index Finishes
Some workloads have a “leader” index whose result is all that matters, or need only a subset of indexes to succeed — a simulation where any three of ten runs converge, for example. successPolicy lets an Indexed Job declare success early.
spec:
completionMode: Indexed
completions: 10
parallelism: 10
successPolicy:
rules:
- succeededIndexes: "0,2-3"
succeededCount: 1
Each rule can set:
succeededIndexes— the set of indexes to consider, in the same range syntax as the status fields.succeededCount— how many of those must succeed.
With both set as above, the Job succeeds when any one of indexes 0, 2, or 3 succeeds. With only succeededIndexes, all listed indexes must succeed. With only succeededCount, any indexes count. When a rule is met, the Job gets a SuccessCriteriaMet condition, lingering Pods are terminated, and the Job then becomes Complete.
Success policy is the newest of these fields. It only works in Indexed mode, and whether it is available depends on the Kubernetes version of the cluster you are using — run kubectl explain job.spec.successPolicy to check.
Suspending a Job
Most people know suspend from CronJobs. Jobs have the same field, and it behaves a little differently:
# Create a Job that does not start
kubectl create job report --image=busybox:1.36 --dry-run=client -o yaml -- date \
| sed 's/^spec:/spec:\n suspend: true/' | kubectl apply -f -
# Resume it
kubectl patch job report --type=merge -p '{"spec":{"suspend":false}}'
# Suspend a running Job
kubectl patch job report --type=merge -p '{"spec":{"suspend":true}}'
What to know:
- A Job created with
suspend: trueexists but creates no Pods.kubectl get jobshows zero active Pods and aSuspendedcondition. - Suspending a running Job terminates its active Pods (they receive SIGTERM and get their graceful termination period — see our lifecycle hooks and graceful shutdown guide). Pods that already completed still count.
- Resuming resets the Job’s
startTime, so theactiveDeadlineSecondstimer starts fresh. - While a Job is suspended (and has not started), some Pod template scheduling fields such as node affinity and tolerations can be updated — the mechanism queueing systems use to decide where a Job lands before releasing it.
The contrast with CronJobs is worth holding clearly: suspending a CronJob only stops future Jobs from being created and leaves running Jobs alone; suspending a Job stops the work itself.
Putting It Together: An Exam-Style Task
Create a Job named
batch-splitin namespacedatathat processes 5 work items, 2 at a time. Each Pod must receive its item number. If the container exits with code 3, the Job must fail immediately without retries. Pod evictions must not count as failures. Other failures may be retried up to 2 times.
apiVersion: batch/v1
kind: Job
metadata:
name: batch-split
namespace: data
spec:
completionMode: Indexed
completions: 5
parallelism: 2
backoffLimit: 2
podFailurePolicy:
rules:
- action: FailJob
onExitCodes:
operator: In
values: [3]
- action: Ignore
onPodConditions:
- type: DisruptionTarget
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo item $JOB_COMPLETION_INDEX"]
Verify:
kubectl -n data get job batch-split
kubectl -n data get pods -l job-name=batch-split
kubectl -n data logs -l job-name=batch-split --prefix
kubectl -n data get job batch-split -o jsonpath='{.status.completedIndexes}{"\n"}'
Map each sentence of the task to a field: “5 work items” is completions, “2 at a time” is parallelism, “receive its item number” is completionMode: Indexed, “exit code 3 fails immediately” is a FailJob rule, “evictions must not count” is an Ignore rule on DisruptionTarget, and “retried up to 2 times” is backoffLimit. And because a failure policy is present, restartPolicy has to be Never.
Quick Reference
| Field | Requires | Default | Mutable after creation |
|---|---|---|---|
completionMode | completions set when Indexed | NonIndexed | No |
podFailurePolicy | restartPolicy: Never | none | No |
backoffLimitPerIndex | Indexed mode, restartPolicy: Never | none | No |
maxFailedIndexes | backoffLimitPerIndex | none | No |
successPolicy | Indexed mode | none | No |
suspend | — | false | Yes |
When you forget the shape of any of these on the exam, kubectl explain is faster than searching the docs:
kubectl explain job.spec.podFailurePolicy.rules
kubectl explain job.spec.podFailurePolicy.rules.onExitCodes
kubectl explain job.spec --recursive | grep -iE "index|suspend|policy"
More speed habits like this are collected in our CKAD kubectl cheat sheet.
Common Mistakes
- Using
restartPolicy: OnFailurewith a pod failure policy. The API rejects it. Failure policy and per-index backoff both needNever. - Forgetting
completionsin Indexed mode. A work-queue style Job (nocompletions) cannot be Indexed. - Double-quoting
$JOB_COMPLETION_INDEXon the command line. Your local shell expands it to nothing. Use single quotes, or write the YAML directly. - Putting a broad rule before a specific one. Rules are first-match. A
NotIn [0]-style catch-all placed first swallows everything below it. - Expecting
Ignoreto mean “do not retry”. It means the opposite: retry without counting. “Do not retry” isFailJoborFailIndex. - Trying to edit an immutable field.
kubectl editoncompletionModeorpodFailurePolicyfails; usekubectl replace --force -f job.yamlto delete and recreate. - Assuming a suspended Job keeps its Pods. Suspending a running Job terminates the active Pods.
How to Practice
Spin up any local cluster and work through these until you can do each from a blank file in a few minutes:
- Create an Indexed Job with 4 completions and print the index from each Pod. Confirm with
kubectl logs --prefix. - Mount the completion index as a file using a
downwardAPIvolume. - Write a Job whose container runs
exit 42, with aFailJobrule for code 42. Confirm theFailedcondition reason isPodFailurePolicyand only one Pod was created. - Change the rule to
Countand watchbackoffLimitget consumed instead. - Build the even-indexes-fail example with
backoffLimitPerIndex: 1and readcompletedIndexesandfailedIndexes. - Create a suspended Job, confirm no Pods exist, resume it, then suspend it mid-run and watch the Pods terminate.
Once the fields feel familiar, the remaining challenge is doing it under time pressure alongside fifteen other tasks. A full-length timed run is the best way to find out whether you reach for the right field quickly; the CKAD Mock Exam Bundle on Sailor.sh provides scored, exam-style practice across every CKAD domain, with explanations you can review afterwards. For where Jobs sit in the overall blueprint, see our CKAD exam domains breakdown.
Frequently Asked Questions
What is an Indexed Job in Kubernetes?
An Indexed Job is a Job with completionMode: Indexed. Each Pod is assigned a completion index from 0 to completions minus 1, exposed through the JOB_COMPLETION_INDEX environment variable, a Pod annotation and label, and the Pod hostname. The Job completes when one Pod has succeeded for every index.
How does a Pod know its index in an Indexed Job?
The simplest way is the JOB_COMPLETION_INDEX environment variable, which Kubernetes injects automatically. The index is also available as the batch.kubernetes.io/job-completion-index annotation, which you can project into a file with a Downward API volume, and as part of the Pod hostname.
What does podFailurePolicy do in a Kubernetes Job?
It lets you decide how specific failures are handled instead of counting every failure against backoffLimit. Rules match on container exit codes or Pod conditions and apply an action: FailJob to stop immediately, Ignore to retry without counting, Count for default behavior, or FailIndex to fail a single index of an Indexed Job.
Why is my Job rejected when I add a podFailurePolicy?
The most common cause is restartPolicy: OnFailure in the Pod template — a pod failure policy requires restartPolicy: Never. Other causes are listing exit code 0 with the In operator, setting both onExitCodes and onPodConditions in the same rule, or using FailIndex without backoffLimitPerIndex.
What is the difference between backoffLimit and backoffLimitPerIndex?
backoffLimit is one retry budget shared by the whole Job; when it is exhausted the Job fails. backoffLimitPerIndex applies only to Indexed Jobs and gives each index its own budget, so one repeatedly failing index does not prevent the others from running. Failed indexes are recorded in status.failedIndexes.
What happens when you suspend a running Job?
Setting spec.suspend: true on a running Job terminates its active Pods, while already-completed Pods still count toward completions. Setting it back to false creates new Pods and resets the Job’s start time. This differs from a CronJob, where suspend only prevents future runs.
Are Indexed Jobs and pod failure policy on the CKAD exam?
The curriculum requires you to understand Jobs and CronJobs without listing individual fields. The fundamentals — completions, parallelism, backoffLimit, restartPolicy, and CronJob scheduling — are the most likely to appear. The advanced fields here are stable parts of the same API, so knowing them rounds out your preparation and helps you read unfamiliar manifests quickly.
Conclusion
The basic Job model treats every Pod and every failure the same. The advanced fields add precision: completionMode: Indexed tells each Pod which piece of work is its own, podFailurePolicy separates “retrying is pointless” from “that wasn’t the app’s fault”, backoffLimitPerIndex stops one bad shard from sinking the rest, successPolicy lets a Job finish when enough work is done, and suspend separates creating a Job from running it.
For the exam, remember the constraints that cause rejected manifests — restartPolicy: Never for failure policy and per-index backoff, completions required for Indexed mode, and immutability of almost everything except suspend — and practice reading status.conditions, completedIndexes, and failedIndexes to explain what a Job did. Start from the fundamentals in the Jobs and CronJobs guide, layer these patterns on top, and then test yourself under realistic conditions with the CKAD Mock Exam Bundle.