Schedule icon
Get icon
Loop icon
Get icon
Script icon
Process icon
If icon
Delete icon
Set icon
SlackIncomingWebhook icon

Kubernetes CronJob Silent Failure Watchdog with Stuck Run Recovery

Watch Kubernetes CronJobs for missed successes, diagnose failing, stuck or suspended jobs, alert owners once with reminders, and auto-clear hung runs.

Categories
Infrastructure

Kubernetes CronJobs fail quietly. A backup that hits its backoff limit, a report that was suspended during an incident and never resumed, or an ETL run hung on a lock under concurrencyPolicy: Forbid all look the same from the outside: nothing happens, and nobody notices until the data is missing. This blueprint watches the one signal that matters, the time since each CronJob last succeeded, tells you why it is late, and keeps a proper incident lifecycle so owners are told once, reminded while it stays broken, and told again when it recovers.

How it works

  1. every_15_minutes (Schedule) starts a read-only run. Flow-level concurrency: limit 1 keeps runs from overlapping.
  2. incidents (core.kv.Get) loads the incidents opened by earlier runs.
  3. inventory (core.flow.Loop over namespaces) lists CronJobs and Jobs with two kubernetes.kubectl.Get calls per namespace and exposes them through the loop's outputs. Jobs are needed because they hold the failure reason and the start time that the CronJob status does not.
  4. diagnose (scripts.python.Script, Process runner) compares status.lastSuccessfulTime (or the creation time for a CronJob that never succeeded) with the CronJob's allowed silence and classifies each late CronJob:
    • stuck: a run has been active longer than the allowed silence, and blocks every later schedule under Forbid.
    • failing: the latest Job carries a Failed condition, quoted with its reason, for example BackoffLimitExceeded or DeadlineExceeded.
    • not scheduling: no run was scheduled within the window, because the CronJob is suspended, keeps missing startingDeadlineSeconds, or the controller is not picking it up.
    • no success: runs are scheduled but none succeeded and their Jobs were already garbage-collected. It then turns the findings into incident transitions: opened, reminded (when the diagnosis changes or reminder_hours passed) and resolved.
  5. remediation (core.flow.If) runs only when remediate is true and deletes stuck runs with kubernetes.kubectl.Delete, only on CronJobs annotated cronjob-watchdog/kill-stuck: "true". The next schedule then starts normally.
  6. save_incidents (core.kv.Set) stores the open incidents, and notify posts one Slack message only when something changed. errors alerts if the watchdog itself fails.

What you get

  • Silent CronJob failures caught within 15 minutes of crossing their allowed silence, across any number of namespaces.
  • A diagnosis in every alert, so the first message already says "BackoffLimitExceeded", "hung for 9h" or "never scheduled".
  • No alert fatigue: one message per incident, reminders on a cadence, and a recovery message with how long it was broken.
  • Per-CronJob control through annotations: cronjob-watchdog/max-silence-hours, cronjob-watchdog/owner (a Slack mention), cronjob-watchdog/ignore and cronjob-watchdog/kill-stuck.
  • Opt-in self-healing for hung runs, off by default.
  • Flow outputs (open_incidents, opened, resolved, stuck_runs) ready for dashboards or downstream flows.

Who it's for

  • Platform and SRE teams who host batch workloads for other teams on Kubernetes.
  • Data teams whose backups, exports and ETL run as CronJobs and must not silently skip a day.
  • Anyone who has found out from a customer that a nightly job stopped running a week ago.

Why orchestrate this with Kestra

Kubernetes does not alert on a CronJob that stopped succeeding, and a check that lives in the cluster tends to fail with it. Kestra runs the watchdog from outside, keeps incident state in the KV store between runs, retries API calls, masks the cluster token, logs every decision, and lets you replay or run the same flow on demand with different namespaces or thresholds. Alerting, state and remediation are one declarative flow instead of a script, a cron entry and an alert rule maintained separately.

Prerequisites

  • A Kubernetes cluster (1.21 or later, for status.lastSuccessfulTime) reachable from the Kestra worker.
  • A ServiceAccount allowed to get and list cronjobs.batch and jobs.batch in the watched namespaces, plus delete on jobs.batch if you enable remediation.
  • Python 3 on the worker (the default Kestra image has it), or switch diagnose to the Docker task runner.
  • A Slack incoming webhook.

Secrets

  • K8S_MASTER_URL: Kubernetes API server URL.
  • K8S_TOKEN: ServiceAccount token with the permissions above.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for incident and failure alerts.

Quick start

  1. Create a Role with get, list on cronjobs and get, list, delete on jobs in the batch API group, bind it to a ServiceAccount in each watched namespace, and mint a token with kubectl create token.
  2. Create the secrets above and set namespaces.
  3. Set default_max_silence_hours to fit your most common schedule (26 covers daily jobs) and annotate the exceptions, for example cronjob-watchdog/max-silence-hours: "2" on an hourly job.
  4. Add cronjob-watchdog/owner: "@team-handle" so alerts reach the right people.
  5. Execute once, read the open_incidents output and the Slack message.
  6. Annotate CronJobs whose hung runs are safe to kill with cronjob-watchdog/kill-stuck: "true" and set remediate: true on the trigger.

How to extend

  • Fetch the failed Pod's last log lines with kubernetes.kubectl.Get on pods plus a log task and add them to the alert.
  • Open a PagerDuty or Opsgenie incident for CronJobs annotated as critical instead of a Slack message.
  • Re-run a failed backup immediately by applying a Job built from the CronJob's template with kubernetes.kubectl.Apply.
  • Feed open_incidents into a Kestra dashboard to track batch reliability per team.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.