Skip to main content

Lesson: One-Shot and Scheduled Work

What you'll learn

  • The difference between work that runs forever and work that runs once and finishes.
  • How a Job runs a task to completion and retries on failure.
  • How completions and parallelism let a Job run many times.
  • How a CronJob schedules Jobs on a timetable.

Skill gained: you can run batch and scheduled tasks on the cluster instead of long-running services.

The lesson

Deployments and StatefulSets assume your container should run forever — if it exits, that is a failure to fix. But lots of real work is the opposite: a database backup, a data import, a report generation. It runs, finishes, and should stop. For that, Kubernetes gives you Jobs and CronJobs.

1. Run-forever vs run-to-completion

This is the key mental split.

Deployment / StatefulSet        Job / CronJob
-------------------------       -------------------------
container exits = problem       container exits 0 = success
restart it forever              don't restart on success
"a service"                     "a task"

If you ran a backup script as a Deployment, Kubernetes would "helpfully" restart it the instant it finished, looping forever. A Job understands that finishing is the goal.

2. The Job

A Job runs one or more Pods until a set number of them complete successfully, then stops. If a Pod fails (exits non-zero or crashes), the Job retries it up to backoffLimit times.

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  backoffLimit: 4          # retry a failing Pod up to 4 times
  template:
    spec:
      restartPolicy: Never # required for Jobs: Never or OnFailure
      containers:
        - name: pi
          image: perl:5.40
          command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(200)"]

Note restartPolicy: Never — a Job's Pods may not use the default Always, because "always restart" contradicts "run once."

kubectl apply -f pi.yaml
kubectl get jobs               # COMPLETIONS goes 0/1 -> 1/1
kubectl logs job/pi            # see the result
kubectl get pods               # the Pod stays in Completed state
kubectl delete job pi          # cleanup (also removes its Pods)

A completed Pod sticks around (in Completed status) so you can read its logs. It is not running and uses no CPU.

3. completions and parallelism

By default a Job runs one Pod to success. Two fields let you scale that:

  • completions — how many successful Pods you need in total.
  • parallelism — how many run at the same time.
apiVersion: batch/v1
kind: Job
metadata:
  name: import
spec:
  completions: 6      # need 6 successful runs total
  parallelism: 2      # but only 2 at a time
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: worker
          image: busybox:1.36
          command: ["sh", "-c", "echo processing chunk; sleep 5"]
completions: 6, parallelism: 2

time -->
[ pod1 ][ pod3 ][ pod5 ]
[ pod2 ][ pod4 ][ pod6 ]
                         ^ Job complete (6/6)

This is the classic "work queue / batch" pattern: chop a big job into N pieces and process P at a time.

4. The CronJob

A CronJob creates a Job on a repeating schedule, using the same cron syntax you may know from Linux. Use it for backups, nightly reports, periodic cleanup.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-backup
spec:
  schedule: "0 2 * * *"        # every day at 02:00
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: busybox:1.36
              command: ["sh", "-c", "echo backing up at $(date)"]

The five schedule fields are: minute, hour, day-of-month, month, day-of-week. A few you will use often:

"*/5 * * * *"   every 5 minutes
"0 * * * *"     top of every hour
"0 2 * * *"     daily at 02:00
"0 3 * * 0"     Sundays at 03:00
kubectl apply -f backup.yaml
kubectl get cronjob                 # see SCHEDULE and LAST SCHEDULE
kubectl get jobs                    # each run creates a Job named backup-<timestamp>
kubectl create job --from=cronjob/nightly-backup test-now   # trigger one immediately to test

Two useful CronJob fields:

  • successfulJobsHistoryLimit / failedJobsHistoryLimit — how many old Jobs to keep (so they do not pile up).
  • concurrencyPolicy: Forbid — skip a new run if the previous one is still going (good for backups that must not overlap).

5. When to use which

One-off task, run now            -> Job
Same task on a schedule          -> CronJob (which creates Jobs)
Process N items, P at a time     -> Job with completions + parallelism
Long-running service / API       -> Deployment (NOT a Job)

A simple test: ask "should this container ever exit on its own?" If yes, it is a Job/CronJob. If no, it should run until I stop it, it is a Deployment.

6. Watching and cleaning up

kubectl get jobs -w               # -w watches live as COMPLETIONS changes
kubectl describe job import       # events, completion counts, failures
kubectl logs job/import           # logs (picks one of the Pods)
kubectl delete cronjob nightly-backup   # stops scheduling and removes its Jobs

CronJobs accumulate completed Jobs over time. Set the history limits, or you will see dozens of old Completed Pods cluttering kubectl get pods. That is harmless but messy — keeping the history small is good hygiene.

Dig deeper

Search terms

  • kubernetes job vs deployment
  • kubernetes job completions parallelism
  • kubernetes cronjob schedule syntax
  • kubectl create job from cronjob
  • kubernetes cronjob concurrencyPolicy forbid

Check yourself

  1. Why would running a backup script as a Deployment cause an endless loop?
  2. What restartPolicy values are allowed for a Job's Pods, and why not Always?
  3. If completions: 6 and parallelism: 2, how many Pods run at once and how many must succeed total?
  4. Write a schedule string that runs every day at 2 a.m.
  5. How do you trigger a CronJob immediately to test it without waiting for the schedule?