Skip to main content

Control retries and clean up finished jobs

Inspect 1.36

A control plane rarely fails outright because of finished Jobs. It becomes slow. A CronJob firing every minute leaves a Job and a pod object behind on every run, and nothing deletes them by default; over a week that is thousands of dead objects in etcd, the control-plane datastore. The API server reads them, backups grow, and list calls become heavier, until the cluster carries a lag no one can trace to a single cause. Leftover finished Jobs are among the most common reasons a control plane slowly degrades.

The fix is simple and permanent: give every Job a lifespan, and prevent failing Jobs from running longer than they should.

Diagnose a bloated control plane from leftover Jobs

If a cluster feels sluggish and you suspect leftover Jobs, work through the causes from most to least likely.

  1. Count finished Jobs across all namespaces. A handful is normal; hundreds or thousands is the symptom.
  2. Find the source. It is almost always a CronJob on a frequent schedule with no cleanup.
  3. Check whether those Jobs carry a TTL. Inspect a leftover Job's spec for ttlSecondsAfterFinished; if it is absent, nothing will ever remove it.
  4. Set the TTL (below) on the offending jobTemplate, then delete the existing backlog so etcd sheds the accumulated objects.

Clean up with ttlSecondsAfterFinished

Nothing removes a finished Job on its own. Set ttlSecondsAfterFinished and Kubernetes deletes the Job, pods included, that many seconds after it finishes:

yaml
		spec:
  ttlSecondsAfterFinished: 3600 # delete one hour after finishing
	

Treat this as mandatory on every Job, not as an optimization. One hour is enough to read logs after a failure; a routine success can use far less.

Bound retries with backoffLimit

backoffLimit caps how many times a Job's pod may fail before the whole Job is marked failed:

yaml
		spec:
  backoffLimit: 4
	

Between attempts Kubernetes waits with exponential backoff (10s, 20s, 40s, and so on, capped at six minutes), so a fast-failing Job does not spin in a tight loop and overload the API server. Keep the limit low for work that cannot succeed on retry, such as a bad input or a missing dependency; a high limit only delays the inevitable.

Skip retries that cannot succeed with podFailurePolicy

Some failures warrant a retry (a transient network error); some do not (the program exited with a "bad config" code). podFailurePolicy branches on the exit code so the Job fails fast instead of exhausting backoffLimit:

yaml
		spec:
  backoffLimit: 4
  podFailurePolicy:
    rules:
      - action: FailJob # do not retry
        onExitCodes:
          operator: In
          values: [42] # the app's "unrecoverable" code
	

An onExitCodes rule requires the pod template's restartPolicy: Never (the Job examples here use it); Kubernetes rejects podFailurePolicy on an OnFailure Job.

Draining a stuck Job

A Job stuck retrying, or one whose pod refuses to terminate, can hold up a node drain and stall a cluster upgrade. Delete it directly with kubectl delete job <name> to release the pod, then fix the underlying failure before rerunning. A backoffLimit paired with activeDeadlineSeconds keeps most Jobs out of that state; set both before you need them, not while an upgrade is blocked.