| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; any Kubernetes cluster |
| Cert prep | CKA · CKAD |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | Job YAML, completions, parallelism, backoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished, status inspection, logs, deletion, comparison with bare Pod and CronJob, and common Job problems. Does not cover CronJob schedule syntax, indexed Jobs, Pod failure policies, init or sidecar patterns, or distributed batch frameworks. |
A Deployment keeps replicas running until you scale it down. A Job is built for the opposite case: run a command, count successful completions, and stop. That makes Jobs the right controller for migrations, reports, backups, and other work that must finish rather than stay online.
In the job-lab namespace you will build that pattern step by step — start with a minimal busybox Job, read its Pod logs, then change one control at a time to see how completions, parallelism, retries, deadlines, and automatic cleanup behave before you wire the same fields into production batch manifests.
What Is a Kubernetes Job?
A Job creates one or more Pods to perform a finite task. The Job completes after the required number of Pods succeed. Failed Pods can be retried according to Job settings.
Jobs fit one-time batch and maintenance work:
- Database migrations
- Data processing
- One-time backups
- Report generation
- Administrative scripts
A Deployment keeps replicas running. A Job tracks successful completions and then stops. For long-running services, see choose a Kubernetes workload resource. For Pod phases and restartPolicy on bare Pods, see Kubernetes Pods and Pod Lifecycle.
Job vs bare Pod
| Bare Pod | Job |
|---|---|
| Creates one Pod object directly | Manages Pods for a finite task |
| Does not track required completions | Tracks successful completions |
| No Job-level retry limit | Supports backoffLimit |
| No automatic finished-resource TTL | Supports ttlSecondsAfterFinished |
| Suitable for simple testing | Suitable for managed batch work |
Job vs CronJob
A Job runs once; a CronJob creates Jobs on a schedule. See Job versus CronJob for the complete comparison.
Create and Inspect a Kubernetes Job
Minimal Job YAML
Save this manifest as report-gen.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: report-gen
namespace: job-lab
spec:
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command:
- sh
- -c
- |
echo "Report generated on $(date)"
sleep 5Create the namespace:
kubectl create namespace job-labSample output:
namespace/job-lab createdApply the Job:
kubectl apply -f report-gen.yamlSample output:
job.batch/report-gen createdWatch until the Job reports Complete:
kubectl get jobs -n job-lab --watchSample output:
NAME STATUS COMPLETIONS DURATION AGE
report-gen Running 0/1 2s 2s
report-gen Running 0/1 4s 4s
report-gen Complete 1/1 9s 9sPress Ctrl+C after report-gen reaches Complete.
Alternatively, wait deterministically:
kubectl wait --for=condition=complete job/report-gen -n job-lab --timeout=60sSample output:
job.batch/report-gen condition metCOMPLETIONS shows successful Pods versus the required count (1/1). DURATION is how long the Job was active. STATUS becomes Complete when all required Pods succeed.
Job status, Pods, and logs
List the Pod the Job created:
kubectl get pods -n job-lab -l batch.kubernetes.io/job-name=report-genSample output:
NAME READY STATUS RESTARTS AGE
report-gen-nn9kp 0/1 Completed 0 9sRead the container output through the Job shortcut:
kubectl logs job/report-gen -n job-labSample output:
Report generated on Sun Jul 26 09:22:02 UTC 2026The log line confirms the one-shot command ran and exited successfully.
| Task | Command |
|---|---|
| List Jobs | kubectl get jobs |
| Watch completion | kubectl get jobs --watch |
| Inspect Job details | kubectl describe job <name> |
| List Pods created by a Job | kubectl get pods -l batch.kubernetes.io/job-name=<name> |
| View logs | kubectl logs job/<name> |
| View Job YAML | kubectl get job <name> -o yaml |
When parallelism or completions create multiple Pods, kubectl logs job/<name> may only show one Pod. List Pods by label and read logs per Pod when you need every worker transcript.
Important Job fields
spec.template
spec.template is a Pod template. Kubernetes automatically adds batch.kubernetes.io/job-name and batch.kubernetes.io/controller-uid labels to Job Pods. Set container command, args, and environment here the same way you would on a Pod — see container command, args, and environment when you need to override image defaults.
restartPolicy
Job Pod templates allow only:
Never— failed Pods stay failed; the Job controller creates replacement Pods when retries are allowedOnFailure— the kubelet restarts the container inside the same Pod on failure
Always is not valid on a Job Pod template because a Job Pod must exit when the task finishes.
Selectors and reruns
Normally, omit spec.selector. Kubernetes generates a unique selector and matching Pod labels for each Job. Manually reusing selectors can cause Jobs to count or delete unrelated Pods. The Job selector is optional and normally generated by Kubernetes.
A completed Job does not run again when you reapply the same unchanged object. To execute it again, delete and recreate the Job or create another Job with a new name. Use a CronJob when execution must recur on a schedule.
At-least-once execution
Design Job commands to be idempotent. A Job uses the requested number of successful completions as its success criterion, but it does not provide exactly-once execution. Retries, deadlines, or failure limits can prevent the Job from succeeding, and the same program can sometimes start more than once. Even a Job with one completion and parallelism of one can occasionally start the program more than once because of node, kubelet, eviction, or controller recovery behavior.
Job status
kubectl get jobs summarizes controller state:
| Column or field | Meaning |
|---|---|
STATUS |
Complete, Failed, or active (Running) while work remains |
COMPLETIONS |
Successful Pods versus spec.completions (default 1) |
| Active Pods | Pods still running |
| Successful Pods | Pods that completed without failure |
| Failed Pods | Pods that terminated with failure |
When a Job finishes, Events on kubectl describe job record the reason, such as BackoffLimitExceeded or DeadlineExceeded.
Run Multiple Job Completions
Set completions
spec.completions sets how many successful Pod completions the Job needs. Default is 1.
Save this manifest as multi-complete.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: multi-complete
namespace: job-lab
spec:
completions: 3
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo task $HOSTNAME && sleep 2"]Delete the first Job, then apply the new one:
kubectl delete job report-gen -n job-labSample output:
job.batch "report-gen" deletedDeleting the Job also deletes the Pods it owns through cascading deletion.
kubectl apply -f multi-complete.yamlEarly in the run:
kubectl get jobs multi-complete -n job-labSample output:
NAME STATUS COMPLETIONS DURATION AGE
multi-complete Running 0/3 3s 3sWait for completion:
kubectl wait --for=condition=complete job/multi-complete -n job-lab --timeout=120skubectl get jobs multi-complete -n job-labSample output:
NAME STATUS COMPLETIONS DURATION AGE
multi-complete Complete 3/3 18s 18sCOMPLETIONS moved from 0/3 to 3/3 as three separate Pods each exited successfully.
Limit parallelism
spec.parallelism limits how many Job Pods run at the same time. Default is 1.
Save this manifest as parallel-batch.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: parallel-batch
namespace: job-lab
spec:
completions: 6
parallelism: 2
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo worker $HOSTNAME && sleep 3"]kubectl delete job multi-complete -n job-lab --ignore-not-found=truekubectl apply -f parallel-batch.yamlWhile the Job is active, count running Pods:
kubectl get pods -n job-lab -l batch.kubernetes.io/job-name=parallel-batchSample output:
NAME READY STATUS RESTARTS AGE
parallel-batch-99mdf 1/1 Running 0 4s
parallel-batch-l4nkt 1/1 Running 0 4sTwo Pods run at once because parallelism is 2. When one finishes, the controller starts another until six Pods succeed.
| Field | Purpose |
|---|---|
completions |
Number of successful completions required |
parallelism |
Maximum Pods that should run concurrently |
Actual parallelism can be lower when remaining completions are fewer than parallelism, nodes lack resources, or scheduling blocks new Pods.
Wait for all six completions:
kubectl wait --for=condition=complete job/parallel-batch -n job-lab --timeout=120sSample output:
job.batch/parallel-batch condition metkubectl get jobs parallel-batch -n job-labSample output:
NAME STATUS COMPLETIONS DURATION AGE
parallel-batch Complete 6/6 23s 23sNonIndexed work distribution
These examples use the default NonIndexed completion mode. All worker Pods receive the same command. The Job counts successes but does not assign a unique task number to each Pod.
Non-indexed completions are interchangeable; Indexed Jobs provide explicit completion indexes but are correctly outside this article's scope.
Control Job Failure and Runtime
Retry failed work with backoffLimit
backoffLimit caps how many times Kubernetes retries a failed Pod before marking the Job failed. Default is 6. Retries use increasing delay between attempts.
Save this manifest as fail-batch.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: fail-batch
namespace: job-lab
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "echo failing && exit 1"]kubectl delete job parallel-batch -n job-lab --ignore-not-found=truekubectl apply -f fail-batch.yamlWith restartPolicy: Never, backoffLimit: 3 allows the initial attempt followed by up to three replacement attempts. This example can therefore leave four failed Pods before the Job reaches BackoffLimitExceeded. Kubernetes 1.36 evaluates failed Pods against backoffLimit and recreates failed Pods with increasing delay.
Wait until the Job fails:
kubectl wait --for=condition=failed job/fail-batch -n job-lab --timeout=180skubectl get jobs fail-batch -n job-labSample output:
NAME STATUS COMPLETIONS DURATION AGE
fail-batch Failed 0/1 77s 77sList failed Pods:
kubectl get pods -n job-lab -l batch.kubernetes.io/job-name=fail-batchSample output:
NAME READY STATUS RESTARTS AGE
fail-batch-6sq6n 0/1 Error 0 45s
fail-batch-mcfzk 0/1 Error 0 77s
fail-batch-rlpm4 0/1 Error 0 66s
fail-batch-wq467 0/1 Error 0 4sInspect Events for the failure reason:
kubectl describe job fail-batch -n job-labSample output (trimmed Events section):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 78s job-controller Created pod: fail-batch-mcfzk
Normal SuccessfulCreate 67s job-controller Created pod: fail-batch-rlpm4
Warning BackoffLimitExceeded 1s job-controller Job has reached the specified backoff limitRead logs from one failed Pod:
FAILED_POD=$(kubectl get pod -n job-lab -l batch.kubernetes.io/job-name=fail-batch -o jsonpath='{.items[0].metadata.name}')kubectl logs "$FAILED_POD" -n job-labSample output:
failingIf you need every attempt, prefix logs from all failed Pods:
kubectl logs -n job-lab -l batch.kubernetes.io/job-name=fail-batch --prefix --max-log-requests=10Stop long-running work with activeDeadlineSeconds
spec.activeDeadlineSeconds limits total active time for the Job. When the deadline passes, running Pods are terminated and the Job fails with reason DeadlineExceeded. The active deadline takes precedence over backoffLimit.
Set the field on the Job spec, not inside the Pod template.
Save this manifest as slow-batch.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: slow-batch
namespace: job-lab
spec:
activeDeadlineSeconds: 10
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: busybox:1.36
command: ["sh", "-c", "sleep 120"]kubectl delete job fail-batch -n job-lab --ignore-not-found=truekubectl apply -f slow-batch.yamlWait for failure:
kubectl wait --for=condition=failed job/slow-batch -n job-lab --timeout=120skubectl get jobs slow-batch -n job-labSample output:
NAME STATUS COMPLETIONS DURATION AGE
slow-batch Failed 0/1 41s 41sEvents show the deadline:
kubectl describe job slow-batch -n job-labSample output (Events section):
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 41s job-controller Created pod: slow-batch-l87b5
Normal SuccessfulDelete 31s job-controller Deleted pod: slow-batch-l87b5
Warning DeadlineExceeded 0s job-controller Job was active longer than specified deadlineThe Job controller starts terminating active Pods when the ten-second deadline is reached. Kubernetes 1.31 and later does not add the terminal Failed condition until all Job Pods have terminated. The Pod's termination grace period can therefore make the displayed Job duration longer than ten seconds; a duration near 40 seconds is possible with the default 30-second Pod termination grace period. Kubernetes now delays Complete or Failed until all Job Pods terminate, and terminationGracePeriodSeconds can extend that interval. The deadline still takes precedence over backoffLimit.
Troubleshoot Common Kubernetes Job Problems
| Symptom | Likely cause | Fix |
|---|---|---|
Job stays Running indefinitely |
Container never exits, wrong completions, or command waits on input |
Confirm the process exits on success; set activeDeadlineSeconds to cap run time; fix hanging commands |
BackoffLimitExceeded in Events |
Command, image, or dependency failure after retries | Read failed Pod logs and exit codes; fix command/args, env vars, and mounted files |
Fewer parallel Pods than parallelism |
Low remaining completions, node resources, scheduling, or ResourceQuota |
Check kubectl describe pod Events and cluster capacity; reduce parallelism or add nodes |
| Completed Job still listed | No TTL or CronJob history cleanup configured | Delete manually, set ttlSecondsAfterFinished, or tune CronJob history limits |
| Logs missing after completion | TTL or manual deletion removed Pods | Collect logs before cleanup or raise TTL; use centralized logging for production batch work |
What's Next
- Kubernetes CronJobs with Examples
- Kubernetes Liveness, Readiness and Startup Probes
- Kubernetes ConfigMap with Examples
References
Summary
You created a minimal batch/v1 Job with restartPolicy: Never, watched COMPLETIONS reach 1/1, and read logs with kubectl logs job/<name>. That baseline is the pattern for database migrations, reports, and other tasks that must run once and finish.
completions and parallelism split total work from concurrent workers in the default NonIndexed mode — every Pod runs the same command, and your application must coordinate distinct work externally. backoffLimit and activeDeadlineSeconds answer different failure questions: how many times to retry versus how long the whole Job may stay active. When both apply, the deadline wins. Design commands to be idempotent because Jobs track successful completion counts but do not provide exactly-once execution. Kubernetes explicitly warns that the same program can sometimes start twice even with one completion, one parallel worker, and restartPolicy: Never.
For one-off work, apply a Job directly. For recurring batch work, use a CronJob that creates Jobs on a schedule. Next, compare Job versus CronJob or open the CronJob scheduler guide when you need recurring schedules.

