Unbound PersistentVolumeClaims
A PersistentVolumeClaim that never binds does not fail loudly. The workload
that mounts it stays unschedulable — or its StatefulSet stays half-up — while
everything around it keeps reporting green: the claim is not an error state
the API server flags, it is a Pending phase that can sit there for days.
This page covers the two views of that signal: the one-shot report from a laptop, and the standing Grafana view.
The report: scripts/ops/k8s/pvc-unbound.sh
scripts/ops/k8s/pvc-unbound.sh prints one line per claim whose phase is not
Bound, oldest first:
$ scripts/ops/k8s/pvc-unbound.sh
db-team/data-warehouse Pending standard-rwo 500Gi 400d
apps/checkout-cache Lost premium-rwo 50Gi 3d
apps/scratch Pending fast-rwo 1Gi 2m
The line is <namespace>/<name> <phase> <storageClass> <requested size> <age>, where the age is kubectl’s own shorthand. Lost is reported
alongside Pending — a Lost claim is one whose volume is gone, and it is
at least as interesting as one that never bound.
It is a report, not a gate:
- it exits
0whatever the count, so it never breaks whatever runs it; - it reads only (
kubectl get), changes nothing; - if the cluster is unreachable it still exits
0, with a warning on stderr and whatever it could list on stdout.
scripts/ci/tests/test-pvc-unbound.sh runs the script against a canned
kubectl get pvc -A -o json fixture, covering the Pending, Lost, and Bound
cases, the ordering, and the unreachable-cluster path.
The panel: the Storage dashboard
The Storage dashboard (observability/base/grafana-operator/dashboards/)
carries two panels on the same signal:
- Claims not Bound — a table of every claim whose phase is not
Bound, one row per claim, fromkube_persistentvolumeclaim_status_phase{phase!="Bound"}; - Requested capacity by storage class — the total bytes claims have
asked for, split by
storageclass, fromkube_persistentvolumeclaim_resource_requests_storage_bytes.
Both come from kube-state-metrics, which the platform already runs, so there
is nothing to enable. The dashboard is provisioned by the grafana-operator
like every other dashboard in the repository, into the kubernetes folder.
How to read it
- An empty table — every claim on the cluster is Bound. The capacity panel is still worth a glance: it is the standing demand on the storage classes, whether or not anything is stuck.
- A row that appears and disappears — a claim in flight, or a transient provisioning failure the CSI driver retried. Wait for it to settle.
- A row that stays — a claim that is genuinely stuck. Run the script above to see how long it has been Pending, then read on.
What a Pending claim usually means
The phase is Pending because no PersistentVolume satisfied the claim, and
the reasons why cluster by cloud.
On GKE, the two common ones:
- No default storage class, or the wrong one. A claim with no
storageClassNamebinds against the cluster’s default — which isstandard-rwoon new clusters, but is not guaranteed to exist on one that predates the feature. The claim then waits forever on a class that is not there. - PD capacity or quota. Compute Engine persistent disks are regional
quota; a claim for a size or type the project has no quota for stays
Pendinguntil the quota moves. The place to look is thepersistentvolumeclaim-controllerevents on the claim itself (kubectl describe pvc -n <ns> <name>), which saygoogleapi: Error 403: quota exceeded.
On EKS, the two common ones:
- The EBS CSI driver addon is not there. EKS does not install
ebs-csi-controllerby default, and without it no claim for anio1/io2/gp3volume is ever provisioned. The addon’s absence is silent from the claim’s side: the events sayprovisioning waiting for first Consumer to be consumedor nothing useful at all. - No node in the right zone. The EBS CSI driver provisions a volume in the zone of the pod that first consumes the claim, and only when a consumer exists. A claim nothing schedules against — a pod pinned elsewhere, a node group with no capacity — has no consumer, and no volume. Check that something actually mounts the claim and can be scheduled.
On both, kubectl describe pvc -n <namespace> <name> is the first stop: the
Events at the bottom carry the reason the controller did not bind it, and
that reason names the fix.
Reading on
- Flux health — the report-and-panel pair for the reconciliation layer
- Observability — where the dashboards live and how they reach the cluster
- Storage classes — the classes the platform defines per cloud