Kubernetes
CronJob Management
Automation
Job Cleanup
DevOps

How to cleanup failed CronJob spawned Jobs once a more recent job passes

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Kubernetes CronJobs create a new Job object on every scheduled run, which means failures can pile up quickly. The built-in history settings help control how many successful or failed Jobs are retained, but they do not express a conditional rule such as "delete failed Jobs only after a later run succeeds."

If that is the behavior you want, treat it as a custom cleanup policy. In practice, the solution is either a separate cleanup task that inspects Jobs by owner and completion time, or a broader retention rule such as ttlSecondsAfterFinished if conditional cleanup is not required.

What Kubernetes Gives You Out of the Box

CronJobs support these history knobs:

yaml
1apiVersion: batch/v1
2kind: CronJob
3metadata:
4  name: report-job
5spec:
6  schedule: "*/15 * * * *"
7  successfulJobsHistoryLimit: 1
8  failedJobsHistoryLimit: 3
9  jobTemplate:
10    spec:
11      template:
12        spec:
13          restartPolicy: Never
14          containers:
15            - name: worker
16              image: busybox:1.36
17              command: ["/bin/sh", "-c", "run-report.sh"]

This lets you keep a fixed number of successes and failures, but it does not say anything about relative ordering between them. If a failed Job should remain visible until a later success proves the issue has cleared, the built-in limits are too blunt.

Model the Cleanup Rule Explicitly

The logic usually looks like this:

  1. List Jobs created by one CronJob.
  2. Sort them by start or completion time.
  3. Find the newest successful Job.
  4. Delete older failed Jobs that finished before that success.

That policy is cluster-specific, so it normally lives in an operational script, controller, or ad hoc cleanup Job rather than in the CronJob manifest itself.

Example Cleanup with kubectl

The following shell example shows the idea. It finds the most recent successful Job for a CronJob and deletes older failures owned by the same CronJob:

bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4namespace="default"
5cronjob="report-job"
6
7latest_success=$(
8  kubectl get jobs -n "$namespace" \
9    -l cronjob-name="$cronjob" \
10    -o jsonpath='{range .items[?(@.status.succeeded==1)]}{.metadata.name}{"\t"}{.status.completionTime}{"\n"}{end}' \
11| sort -k2 \ | tail -n1 \ | cut -f1 ) if [ -z "$latest_success" ]; then echo "No successful job found yet" exit 0 fi success_time=$( kubectl get job "$latest_success" -n "$namespace" -o jsonpath='{.status.completionTime}' ) kubectl get jobs -n "$namespace" -l cronjob-name="$cronjob" -o json \ | jq -r --arg success_time "$success_time" ' .items[] | select(.status.failed == 1) | select(.status.completionTime < $success_time) | .metadata.name ' \ | xargs -r kubectl delete job -n "$namespace" ``` This is not something Kubernetes does for you automatically. You are defining your own retention semantics. ## Run Cleanup as a Separate CronJob A common pattern is to keep the main CronJob focused on business work and run cleanup separately with a service account that can list and delete Jobs. ```yaml apiVersion: batch/v1 kind: CronJob metadata: name: report-job-cleanup spec: schedule: "0 * * * *" jobTemplate: spec: template: spec: serviceAccountName: job-cleaner restartPolicy: Never containers: - name: cleaner image: bitnami/kubectl:latest command: ["/bin/sh", "-c"] args: - /scripts/cleanup.sh ``` This keeps permissions and operational behavior separate from the workload being scheduled. ## Consider Simpler Alternatives If your real goal is just to avoid old clutter, `ttlSecondsAfterFinished` may be enough: ```yaml apiVersion: batch/v1 kind: Job metadata: name: one-off-example spec: ttlSecondsAfterFinished: 3600 ``` That deletes finished Jobs after a fixed time, regardless of whether they succeeded or failed. It does not answer the exact question, but it is much easier to operate. Another option is setting `failedJobsHistoryLimit` to a small number and relying on logs or external observability for failure history instead of preserving old Job objects. ## Common Pitfalls - Expecting `failedJobsHistoryLimit` to mean "keep failures until the next success." It only limits the count of retained failed Jobs. - Deleting failed Jobs too aggressively and losing evidence that is still needed for debugging. - Forgetting RBAC for the cleanup task. Listing and deleting Jobs requires explicit permissions. - Relying on name sorting instead of completion timestamps. CronJob names often include timestamps, but cleanup logic should use actual status fields. - Combining business execution and cleanup into one Job can make incident analysis harder. Separation is usually cleaner. ## Summary - Native CronJob history limits are count-based, not condition-based. - "Delete failed Jobs after a later success" requires custom cleanup logic. - A separate cleanup CronJob or controller is the usual implementation. - Use completion times and owner labels to decide what is safe to delete. - If exact conditional retention is unnecessary, `ttlSecondsAfterFinished` is the simpler solution.

Course illustration
Course illustration

All Rights Reserved.