Skip to content
DevOps Society

Kubernetes 1.37 Can Finally Tell You Which Volumes Nobody Is Using

Kubernetes v1.37 graduates the PVC Unused condition to beta. Here is how to read it and turn it into a storage cleanup workflow that will not lose data.

Published your local timeupdated

Kubernetes 1.37 Can Finally Tell You Which Volumes Nobody Is Using

Verified

Kubernetes 1.37

Every cluster over a year old has them. A PersistentVolumeClaim in a namespace nobody owns any more, bound to a 500 GiB gp3 volume, attached to nothing, billed every hour since the Deployment that used it was torn down in a migration. Kubernetes deliberately refuses to garbage collect that claim, because guessing wrong about storage means destroying data. So the cleanup job lands on you, and until now the only way to find unused PVCs at scale was to write something: pull every PVC, pull every Pod, join on spec.volumes[].persistentVolumeClaim.claimName, then join that against a cloud billing export to work out which ones are worth the argument.

Kubernetes v1.37 makes that join unnecessary. The v1.37 release blog on PVC last-used time, published 21 September 2026, confirms that the PersistentVolumeClaimUnusedSinceTime feature gate has graduated to beta and is on by default. The API now tells you directly whether anything is using a claim, and when that stopped being true. Orphaned volumes become a query instead of a project, which is the first time Kubernetes storage cost work has had a native signal behind it.

Several storage volumes sitting disconnected from any workload while their cost meters keep climbing

What the feature actually adds

The PersistentVolumeClaim protection controller, the same controller that already holds the kubernetes.io/pvc-protection finalizer to stop you deleting a claim out from under a running Pod, now also writes an Unused condition into status.conditions on every PVC. It already watches Pods for the finalizer logic, so it knows the answer; v1.37 just writes the answer down.

The condition object looks like this:

{
  "type": "Unused",
  "status": "True",
  "reason": "NoPodsUsingPVC",
  "message": "No pods are currently referencing this PVC",
  "lastTransitionTime": "2026-09-14T12:03:11Z",
  "lastProbeTime": null
}

status: "True" with reason NoPodsUsingPVC means nothing non-terminated references the claim. When a Pod picks it up, the controller flips the condition to status: "False" with reason PodUsingPVC. There is no separate top-level timestamp field, and this is the detail most of the early write-ups got wrong: the "since when" lives in the condition's own lastTransitionTime. If you see a blog post querying .status.unusedSince, close the tab.

The gate landed as alpha in v1.36 and is beta in v1.37, which is the usual shape of a storage KEP (KEP-5541, owned by SIG Storage). Beta and on by default means you get it on managed control planes as soon as your provider ships 1.37, without an API server flag. It also means the semantics can still change before GA, so treat it as an input to a human decision rather than the trigger for an automated kubectl delete. If you want the wider picture of which controller does what here, our breakdown of the Kubernetes control plane and its controllers covers where the PVC protection controller sits. Elsewhere in the same release, DRA extended resource support went GA.

Reading the condition

For a single claim, the JSONPath filter from the upstream docs is the short version:

kubectl get pvc my-data -o jsonpath='{.status.conditions[?(@.type=="Unused")].status}'

For a fleet-wide view, custom columns get you a table you can eyeball:

# one line; the JSONPath filter picks the Unused condition out of the array
kubectl get pvc -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,UNUSED:.status.conditions[?(@.type=="Unused")].status,SINCE:.status.conditions[?(@.type=="Unused")].lastTransitionTime'

An empty UNUSED column is not the same as False. It means the controller has not written a condition for that claim yet, which you should expect on a freshly upgraded cluster and on claims that have not changed usage state since the upgrade. Give the controller time before you conclude anything from a blank.

Three semantics matter more than the syntax, and the upstream post is explicit about all three. A Pod in phase Succeeded or Failed does not hold the condition at False, so a completed Job releases its claim immediately. A Pending Pod does hold it, including one that is unschedulable and will never run. And where several Pods share a claim, the condition only goes to True once the last non-terminated Pod is gone.

Building a cleanup workflow around it

The condition is a filter, not a decision. Here is the sequence I would run.

Find candidates over a threshold. The list of unused PVCs is only useful once you age it. Thirty days is a reasonable starting point for production namespaces, seven for anything labelled as ephemeral or preview.

kubectl get pvc -A -o json \
  | jq -r --argjson days 30 '
      .items[]
      | . as $pvc
      | (.status.conditions // [])[]
      | select(.type == "Unused" and .status == "True")
      # lastTransitionTime is the only timestamp; there is no unusedSince field
      | select((now - (.lastTransitionTime | fromdateiso8601)) > ($days * 86400))
      | "\($pvc.metadata.namespace)\t\($pvc.metadata.name)\t\(.lastTransitionTime)"'

Check for a snapshot before anything else. Query VolumeSnapshots in the namespace and confirm either that a recent one exists with the PVC as its source, or that your backup tool has the volume in a restorable set. If neither is true, take a snapshot first and let it complete. A snapshot costs a fraction of the live volume and buys you the ability to be wrong.

Label, do not delete. Mark the claim and leave it alone for a full notice period. Labels cannot hold RFC 3339 timestamps because of the colons, so put the flag in a label and the date in an annotation:

kubectl label pvc my-data -n analytics \
  storage.example.com/cleanup-candidate=true

kubectl annotate pvc my-data -n analytics \
  storage.example.com/unused-since='2026-08-14T09:21:04Z' \
  storage.example.com/delete-after='2026-10-14'

Know what deletion will actually do. Deleting the PVC is not the same as deleting the data, and the difference is the PV's persistentVolumeReclaimPolicy. Under Delete, which is what a dynamically provisioned volume inherits from its StorageClass unless the class sets reclaimPolicy: Retain, removing the claim removes the backing disk and the cloud spend with it. Under Retain, the PV goes to Released and keeps billing, so you have stopped nothing until you delete the PV and the underlying disk too. Statically provisioned volumes are almost always Retain, which means the tidiest-looking cleanup in a legacy namespace can produce exactly zero savings.

A cleanup flow that checks an unused volume against a snapshot archive, labels it, then removes it from the rack

Where the unused PVC condition will mislead you

Situation

What the condition says

What is actually true

What to do

CronJob or batch Job between runs

True from the moment the Pod completes

Claim is live and needed on the next schedule

Exclude namespaces or claims carrying batch ownership labels; compare the age against the schedule interval

StatefulSet scaled to zero

True, since no non-terminated Pod references the claim

Data belongs to a replica that will come back on scale-up

Check for an owning StatefulSet before acting; also check its persistentVolumeClaimRetentionPolicy

Rolling update or node drain

Brief True window while the old Pod terminates and the new one is Pending

Nothing is wrong

Any threshold longer than a few minutes filters this out on its own

Pod stuck Terminating

False, because a terminating Pod is not Succeeded or Failed

Workload is effectively gone

Investigate the stuck Pod; the claim will not surface as a candidate until it clears

Unschedulable Pod parked on the claim

False

Nothing is running and nothing will

Fix or delete the Pod, then let the condition settle

Dev or sandbox namespace

True for weeks at a time

Often correct, occasionally someone's in-progress work

Shorter threshold, but route the notice to a human owner rather than deleting on a timer

The condition answers one question well: is any non-terminated Pod referencing this claim right now, and since when. It knows nothing about whether the bytes matter. A claim that has been idle for 200 days might hold the only copy of a compliance dataset. It also does not see consumers outside the Pod API: an external process reading a Retain-policy volume directly, or a CSI driver mounting it out of band, is invisible here. Snapshot before delete is not belt and braces, it is the actual control.

A policy you can defend

Pick one threshold per namespace class and write it into the namespace labels so the query needs no special cases: 7 days for preview and sandbox, 30 for staging, 90 for production. Run the query weekly, not continuously, because weekly cadence forces batching and batching forces review.

Then split approval by blast radius. Anything in a preview namespace with a completed snapshot gets deleted by the platform team on the notice expiry, no ticket. Anything in staging or production needs the namespace owner's sign-off recorded against the annotation, and anything whose PV is Retain needs a second person, because that one requires deleting the underlying disk by hand and there is no undo. Claims owned by a StatefulSet are excluded from automation entirely until somebody confirms the set is not coming back.

The measurable win is not a deletion count. It is that the unused PVC backlog stops growing, because the weekly report now has a number in it that anyone can read, instead of a script only one engineer understands. More patterns for managing cluster state at scale live in our Kubernetes section.

Written by

DevOpsSociety Editorial Team

Editorial Team

The DevOpsSociety Editorial Team covers DevOps, cloud infrastructure, Kubernetes, AI infrastructure, platform engineering, cybersecurity, FinOps, and modern engineering practices. We publish practical insights, technical guides, architecture analysis, and research for engineers and technology leaders.

More from DevOpsSociety
The Infrastructure Briefing

Get the infrastructure briefing.

Practical DevOps, cloud, AI infrastructure and engineering insights, delivered weekly. Read by engineers and engineering leaders.

No spam. Unsubscribe anytime.

Kubernetes 1.37 Can Finally Tell You Which Volumes Nobody Is Using, DevOps Society