Articles

Kubernetes Jobs vs CronJobs: What's the Difference

A Kubernetes Job runs a pod to completion once; a CronJob runs Jobs on a recurring schedule. How each controller works and when to use them.

Chisato Chisato · · 4 min read
Abstract illustration representing Kubernetes

A Job in Kubernetes creates one or more pods and tracks them until a specified number complete successfully, then stops — it’s built for run-once and batch work, not long-lived services. A CronJob is a thin wrapper around Job that creates a new Job on a recurring schedule, the same way cron runs a script on a timer. If you’ve ever needed a Kubernetes-native replacement for a scheduled shell script, a CronJob is it.

How a Job works

A Deployment or StatefulSet (see pods, Deployments, and Services) keeps a set of pods running indefinitely, restarting them if they exit. A Job does the opposite: it expects its pods to finish, and success means the container exits with code zero.

Key fields on a Job spec:

  • completions — how many successful pod completions are required before the Job is considered done. Omit it and the default is 1.
  • parallelism — how many pods can run at once while working toward completions. Useful for batch work split across multiple pods.
  • backoffLimit — how many times a failing pod is retried before the Job itself is marked failed.
  • restartPolicy — must be Never or OnFailure for a Job’s pod template; Always (the Deployment default) isn’t valid here, since a Job’s pods are expected to terminate.

A Job that finishes leaves its pod objects around by default (subject to your cluster’s history limits) so you can inspect logs, which is one reason to reach for a Job instead of a Deployment for anything that’s meant to run once and stop — a migration script, a one-off data backfill, a batch render job.

How a CronJob works

A CronJob doesn’t run pods directly — it creates a Job object on each scheduled tick, and that Job creates the pods, exactly as if you’d applied it by hand. The CronJob spec wraps a standard cron schedule string around a Job template:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-cleanup
spec:
  schedule: "0 2 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: cleanup
              image: my-registry/cleanup:latest
          restartPolicy: OnFailure

The schedule field uses the same five-field cron syntax as traditional Unix cron — see what a cron job is for how that syntax works, or paste a schedule into our free cron parser to see it explained in plain English. Kubernetes evaluates the schedule in the timezone configured on the CronJob (UTC by default on most clusters).

CronJob-specific fields worth knowing:

  • concurrencyPolicy — what to do if a previous run’s Job is still active when the next tick fires. Allow (default) lets them overlap, Forbid skips the new run, Replace cancels the old one and starts fresh.
  • startingDeadlineSeconds — how late a missed tick (e.g., the controller was down) is still allowed to start; beyond that window, the run is skipped rather than run late.
  • successfulJobsHistoryLimit / failedJobsHistoryLimit — how many completed Job objects to retain for inspection before Kubernetes garbage-collects them.

Job vs CronJob at a glance

JobCronJob
TriggersCreated manually or by another controllerCreated automatically on a schedule
RunsOnce (or a fixed number of completions)Repeatedly, one Job per scheduled tick
Use caseOne-off batch task, migration, backfillRecurring maintenance, reports, periodic sync
Underlying objectPod(s)A Job per run, which then creates pods

In practice, a CronJob’s per-tick behavior is just a Job’s behavior — completions, parallelism, backoffLimit, and restartPolicy all still apply inside jobTemplate. Understanding Job semantics is a prerequisite for using CronJobs correctly.

Common pitfalls

Overlapping runs. With the default concurrencyPolicy: Allow, a slow job that runs longer than its schedule interval will pile up concurrent executions. If a job isn’t safe to run twice at once — because it mutates shared state, for instance — set concurrencyPolicy: Forbid.

Silent failures. A Job that exhausts its backoffLimit is marked failed but doesn’t retry further on its own, and a CronJob simply creates a fresh Job on the next tick regardless of whether the last one succeeded. Alerting on failed Job objects, rather than assuming “no output” means success, catches this.

Confusing a CronJob with a long-running process. A CronJob is for work that starts, does something bounded, and exits — not for something that should run continuously. A continuously running workload belongs in a Deployment, possibly paired with the HPA or VPA for scaling, not a CronJob restarted on a tight schedule.

Timezone confusion. Because the schedule is evaluated server-side against the CronJob’s configured timezone, a schedule written assuming local time can fire at an unexpected hour if the cluster or CronJob config uses UTC. Set the timezone explicitly rather than assuming a default.

For workloads that need more operational logic than a Job or CronJob’s built-in fields provide — custom retry logic, coordinating with external systems — a Kubernetes operator is the next step up in complexity.

The takeaway

A Job runs pods to completion and stops; a CronJob is a scheduler that creates a new Job on each tick of a cron expression. Reach for a Job when you have one-off or batch work that needs Kubernetes-native retry and parallelism semantics, and layer a CronJob on top the moment that work needs to repeat on a schedule. Get concurrencyPolicy and backoffLimit right up front — they’re the two settings most likely to turn a missed edge case into a production incident.

Chisato Chisato · · 4 min read

Kubernetes Init Containers Explained

Init containers run to completion before a pod's main containers start, making them the standard way to handle setup steps and startup ordering.

#Kubernetes #DevOps #Cloud
Chisato Chisato · · 4 min read

Helm vs Kustomize for Kubernetes Config

Helm templates and packages Kubernetes manifests with a templating language; Kustomize patches plain YAML declaratively, with no templates at all.

#Kubernetes #DevOps #Cloud