Skip to main content
governance · kubernetes

CronJobs suspended and forgotten

resource types
1
rule IDs covered
3
severity
low

What does ZopNight detect here?

A CronJob with suspend set to true never fires. ZopNight surfaces these because a suspension is almost always temporary in intent: someone paused it during an incident 6 months ago and nobody resumed it, so a backup or reconciliation has silently not run since.

Signal and threshold

How ZopNight evaluates CronJobs suspended and forgotten.
Field Value
Rule IDsRC-1733 · RC-1833 · RC-1933
Categorygovernance
Severitylow
Metricnone — pure configuration read
Sourceinternal/rules/k8s/cronjob_suspended.go

Suspension is meant to be temporary

suspend: true is the pause button. It is used during incidents, migrations and deploys, and it is intended to be reverted within hours.

What makes it dangerous is that a suspended CronJob is not an error. It produces no failed runs, no alerts and no missed-schedule warnings, because from Kubernetes’ perspective nothing was scheduled to happen. The absence of a job is indistinguishable from a job that was never wanted.

What is usually behind one

Backups. Certificate renewals. Data reconciliation. Cleanup jobs that keep a volume from filling. These are exactly the workloads people suspend during an incident, and exactly the ones whose absence is invisible until the day you need the thing they were producing.

The discovery moment is typically a restore request against backups that stopped months ago.

Suspended is not the same as scheduled-and-failing

Worth separating, because the remediation differs completely:

  • Suspended: nothing ran, nothing failed, no history to inspect.
  • Failing: jobs ran and errored, and kubectl get jobs shows the wreckage.

A failing CronJob is loud. A suspended one is silent, which is why it needs a rule and the other one does not.

Checking it yourself

Terminal window
kubectl get cronjob -A -o json | jq -r '
.items[] | select(.spec.suspend == true)
| "\(.metadata.namespace)/\(.metadata.name)\tlast=\(.status.lastScheduleTime // "never")"'

lastScheduleTime is the useful column: it tells you how long the gap has been, and “never” means it was suspended before it ever ran.

Resuming carefully

Un-suspending a CronJob with a startingDeadlineSeconds set and a long backlog can trigger multiple immediate runs as the controller catches up on missed schedules.

For a backup job that is harmless. For anything that mutates data or sends notifications, check concurrencyPolicy first. Forbid or Replace prevents a thundering herd that Allow will happily produce.

See it fire on your bill.

Connect an account read-only. The first findings land in minutes.

417 rule families across 353 resource types on 22 platforms. Every threshold, metric, and IAM action is documented on these pages before you grant anything.

417 rule families documented
353 resource types covered
read-only default access level
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·