CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Associate

Pod Restart Policies and Job Completion

Every other workload object exists to keep something running. A Job exists to run something until it succeeds and then stop - four completions two at a time, Pods that end up Completed rather than Running, and a CronJob that creates a fresh Job on every tick.

Kubernetes Fundamentals Guide 6 of 46 Beginner

Written against the versions above. Job and CronJob are both in the `batch/v1` API group and have been stable for years. The `batch.kubernetes.io/job-name` label used here to select a Job's Pods replaced the older unprefixed `job-name` label in 1.27.

Node count only matters here for parallelism - two Pods run at once, and they can land anywhere.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. Four completions, two at a time

    A Job runs Pods until a set number of them succeed. This one asks for completions: 4 with parallelism: 2 - four successful runs, no more than two at once:

    NAME   STATUS     COMPLETIONS   DURATION   AGE
    pi     Complete   4/4        27s        27s
    4/2 completions/parallelism, succeeded=4

    Those two numbers do different jobs. completions is how much work there is; parallelism is how fast you are willing to do it. Raising parallelism does not change how much runs, only how long it takes - and backoffLimit, set to 3 here, is how many failures the Job tolerates before it gives up entirely.

    This is the object to reach for when work has an end: a migration, a batch import, a report. A Deployment would restart it forever, because a Deployment's definition of healthy is *running*.

    bash Example session
    kubectl wait --for=condition=Complete job/pi --timeout=300sjob.batch/pi condition metkubectl get job piNAME   STATUS     COMPLETIONS   DURATION   AGEpi     Complete   4/4           27s        27skubectl get job pi -o jsonpath="{.spec.completions}/{.spec.parallelism} completions/parallelism, succeeded={.status.succeeded}{\"\n\"}"4/2 completions/parallelism, succeeded=4

    Expected resultA Job reporting Complete with 4 of 4 completions.

    Success conditionYou can say what parallelism changes and what it does not.

  2. The Pods stay, and that is deliberate

    Look at what the Job left behind:

    NAME       READY   STATUS      RESTARTS   AGE
    pi-5td9f   0/1     Completed   0          27s
    pi-bn9sg   0/1     Completed   0          3s
    pi-shckt   0/1     Completed   0          6s

    Completed, not gone. 0/1 ready, because nothing is running in them any more - and they are still there. A Completed Pod is not a failure or a leak; it is the record. Its logs are still readable, which is the only reason you can ask a finished Job what it produced:

    3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803...

    This is where the disk goes on clusters that run a lot of batch work, so it is worth knowing the controls: ttlSecondsAfterFinished on the Job deletes it and its Pods a set time after finishing, and deleting the Job deletes its Pods with it. Nothing cleans up on its own by default.

    bash Example session
    kubectl get pods -l batch.kubernetes.io/job-name=piNAME       READY   STATUS      RESTARTS   AGEpi-5td9f   0/1     Completed   0          27spi-bn9sg   0/1     Completed   0          3spi-shckt   0/1     Completed   0          6skubectl logs -l batch.kubernetes.io/job-name=pi --tail=1 | head -23.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038203.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303820

    Expected resultCompleted Pods still listed, and their logs still readable.

    Success conditionYou read output from a Pod that had already finished.

  3. A CronJob makes a new Job every tick

    A CronJob is a factory for Jobs. schedule: "*/1 * * * *" - every minute:

    NAME        SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
    heartbeat   */1 * * * *   <none>     False     0        <none>          0s

    A minute later there is a Job, with a name derived from the schedule tick:

    NAME                 STATUS    COMPLETIONS   DURATION   AGE
    heartbeat-29788229   Running   0/1           3s         3s

    and the CronJob now reports ACTIVE 1 and a LAST SCHEDULE.

    Three fields on a CronJob are worth setting on purpose, all present here. concurrencyPolicy: Forbid skips a tick if the previous run is still going - without it a job that takes longer than its interval piles up. successfulJobsHistoryLimit and failedJobsHistoryLimit cap how many finished Jobs are kept, which is the CronJob equivalent of the disk problem from the last step. And TIMEZONE means the schedule runs in the cluster's time zone unless you set timeZone explicitly - a common source of jobs firing at an unexpected hour.

    bash Example session
    kubectl get cronjob heartbeatNAME        SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGEheartbeat   */1 * * * *   <none>     False     0        <none>          0skubectl get jobsNAME                 STATUS    COMPLETIONS   DURATION   AGEheartbeat-29788229   Running   0/1           3s         3s

    Expected resultA CronJob, then a Job created by it on the next minute boundary.

    Success conditionYou can name what concurrencyPolicy prevents.

  4. Run one now, without waiting for the clock

    Waiting a minute to find out whether a CronJob works is a poor way to spend a minute. --from copies the CronJob's template into a Job you trigger immediately:

    kubectl create job --from=cronjob/heartbeat heartbeat-manual
    2026-08-21T06:29:14Z

    The same container, the same command, on demand. This is how you test a schedule's payload without touching the schedule, and it is the single most useful thing to know about CronJobs in practice.

    The manual Job is an ordinary Job and is not counted against the CronJob's history limits, so remember to delete it. And suspend: true on the CronJob - the SUSPEND column in the previous step - stops the schedule without deleting anything, which is what you want while debugging rather than removing the object.

    bash Example session
    kubectl create job --from=cronjob/heartbeat heartbeat-manualjob.batch/heartbeat-manual createdkubectl wait --for=condition=Complete job/heartbeat-manual --timeout=120sjob.batch/heartbeat-manual condition metkubectl logs job/heartbeat-manual2026-08-21T06:29:14Z

    Expected resultA Job created from the CronJob's template, completing immediately.

    Success conditionYou ran the schedule's work without waiting for the schedule.

Troubleshooting

Official sources