PersistentVolumeClaims stuck in Pending for over an hour
What does ZopNight detect here?
ZopNight flags a Kubernetes PersistentVolumeClaim on EKS, GKE or AKS that has stayed in the `Pending` phase and is at least 1 hour old. A claim that cannot bind keeps the pods that mount it from being scheduled, and usually points to a missing StorageClass, no matching PersistentVolume, or a size or access mode nothing can satisfy.
Signal and threshold
| Field | Value |
|---|---|
| Rule IDs | RC-1740 · RC-1840 · RC-1940 |
| Category | orphan |
| Severity | medium |
| Metric | status.phase |
| Threshold | Pending, claim at least 1h old |
| Evaluation window | 1h |
| Source | ZopNight |
| Permissions used | list persistentvolumeclaims · list storageclasses.storage.k8s.io |
Where it applies
Pending is meant to be brief
A claim moves from Pending to Bound once a PersistentVolume is matched or provisioned for it.
The
PersistentVolumeClaim API reference
describes Pending simply as not yet bound. On a healthy cluster with dynamic provisioning that
takes seconds, and the docs note that claims remain unbound indefinitely if no matching volume
exists. Pods wait with them: the scheduler’s
VolumeBinding plugin only accepts a
node where the pod’s requested volumes can be bound.
The usual causes, all visible in the claim’s events:
- The named StorageClass does not exist or has no working provisioner.
- The claim has no class and the cluster has no default.
- With static provisioning, no PersistentVolume matches the requested size and access mode, or the one it names is already bound elsewhere, which the PersistentVolumes documentation says leaves the binding stuck in a pending state.
Listing pending claims with their class
kubectl get pvc -A -o json | jq -r ' .items[] | select(.status.phase == "Pending") | "\(.metadata.namespace)/\(.metadata.name) class=\(.spec.storageClassName // "none") created=\(.metadata.creationTimestamp)"'Then read the reason:
kubectl describe pvc <name> -n <namespace>kubectl get storageclassAn hour of grace before it fires
ZopNight fires when the claim’s phase is pending and the claim is at least 1 hour old, measured
from its creation timestamp. The hour exists because Pending is the normal first phase of every
claim. If the creation time is unavailable, the rule stays silent rather than risk flagging a
claim that was created a minute ago.
The WaitForFirstConsumer caveat
A StorageClass with volumeBindingMode: WaitForFirstConsumer delays binding and provisioning
until a pod using the claim is created, according to the
storage classes page. A claim made
in advance of its pod will be pending well past an hour and is flagged anyway, because the rule
does not check the binding mode or look for a consuming pod. Claims with no class at all are also
reported by PVC has no StorageClass.
Blocked workloads, not a bill
A pending claim has no disk behind it and therefore no storage charge, so the finding has no savings figure. It is filed as an orphan because the claim is doing nothing, and it matters because something that depends on it cannot run.
Getting the claim bound
- Read the events in
kubectl describe pvcfor the provisioner or binding error. - If the class is missing, create it or change the claim template to a class that exists.
- For static volumes, create a PV with matching capacity, access modes and class.
- If the workload that wanted the claim is gone, delete it with
kubectl delete pvc <name>.