Kubernetes
14 min readAugust 12, 2026Updated August 19, 2026

PersistentVolume vs PersistentVolumeClaim: The Difference, Explained Properly

AJ
Ajeet Yadav
Platform & Cloud Engineer
PersistentVolume vs PersistentVolumeClaim: The Difference, Explained Properly

Quick answer

A PersistentVolume is storage that exists; a PersistentVolumeClaim is a request for some of it. That one sentence resolves most of the confusion — but the binding lifecycle, reclaim policies, and access modes hide real data-loss traps. Here's the full picture, from mental model to production gotchas.

14 min read · Kubernetes

A PersistentVolume is storage that exists in the cluster; a PersistentVolumeClaim is a request to use some of it. PV is supply, PVC is demand, and Kubernetes is the broker that matches one to the other. Once you internalise that, the rest of Kubernetes storage — StorageClasses, binding, reclaim policies — stops being a pile of YAML and becomes a supply chain you can reason about.

The reason two objects exist at all is separation of concerns. Whoever provisions storage (a cluster admin, or more commonly a CSI driver acting on their behalf) knows about NFS exports, EBS volume types and Ceph pools. The developer writing a Deployment should not have to. The PVC is the interface between those two worlds: the app says "I need 10Gi, read-write, reasonably fast" and never learns — or cares — where the bytes physically live.

The supply/demand model

Three objects do all the work:

PersistentVolume (PV)PersistentVolumeClaim (PVC)
What it isA piece of provisioned storage in the clusterA request for storage by a user/workload
ScopeCluster-scoped (no namespace)Namespaced
Created byAdmin (static) or provisioner (dynamic)Developer, or a StatefulSet's volumeClaimTemplates
Knows aboutThe actual backend — EBS volume ID, NFS server, Ceph imageOnly size, access mode, StorageClass
LifecycleIndependent of any Pod; can outlive everythingLives in a namespace; deleted like any other resource
AnalogyInventory on the shelfThe purchase order
Pod interactionNever referenced directly by a PodMounted by name in spec.volumes

The StorageClass is the third player: a named template that says how to make new PVs on demand (which CSI driver, which parameters, which reclaim policy). It is what turns the model from "admin hand-carves volumes" into "storage appears when requested."

Pods never mount PVs. Pods mount PVCs, and the PVC is bound to exactly one PV. That indirection is the entire design — and it is a strict 1:1 relationship. One PV binds to one PVC, full stop. Multiple Pods can share a PVC (subject to access modes, below), but two PVCs can never share a PV.

Static vs dynamic provisioning

There are two ways a PV comes to exist, and knowing which one your cluster uses explains most of its behaviour.

Static provisioning is the original model: an admin creates PV objects ahead of time, each pointing at real storage that already exists. PVCs then bind to whichever available PV satisfies them. You still see this for NFS shares, pre-existing SAN LUNs, and bare-metal clusters without a storage operator.

yaml
1apiVersion: v1
2kind: PersistentVolume
3metadata:
4  name: pv-nfs-reports
5spec:
6  capacity:
7    storage: 50Gi
8  accessModes:
9    - ReadWriteMany
10  persistentVolumeReclaimPolicy: Retain
11  storageClassName: nfs-static
12  nfs:
13    server: 10.0.12.40
14    path: /exports/reports

Dynamic provisioning is what every managed cluster does today: the PVC names a StorageClass, and the class's provisioner creates a matching PV (and the real disk behind it) automatically. Nobody writes PV YAML at all — the PV appears with a generated name like pvc-9f8a... the moment the claim needs it.

One field decides which mode you're in: storageClassName on the PVC. Name a class with a provisioner and you get dynamic provisioning. Set it to "" (empty string, deliberately) and you opt out of dynamic provisioning entirely, telling Kubernetes to bind only against PVs that themselves have no class — a static PV with a named class (like nfs-static above) is matched by naming that class, not by "". Omit the field and the cluster's default StorageClass is filled in for you — a common source of surprise, because "I didn't ask for a class" and "I asked for no class" are different requests.

If you're choosing what sits behind that StorageClass on self-hosted clusters, that's a separate decision with real trade-offs — see Rook Ceph vs Longhorn vs OpenEBS. And the machinery that actually creates the disks is a CSI driver, covered in Kubernetes storage and CSI drivers.

A complete worked example

StorageClass → PVC → Pod, the whole chain on an AWS cluster:

yaml
1# 1. The StorageClass — usually created once by the platform team
2apiVersion: storage.k8s.io/v1
3kind: StorageClass
4metadata:
5  name: gp3-retain
6provisioner: ebs.csi.aws.com
7parameters:
8  type: gp3
9reclaimPolicy: Retain
10allowVolumeExpansion: true
11volumeBindingMode: WaitForFirstConsumer
12---
13# 2. The claim — this is what "create a persistent volume claim" means
14apiVersion: v1
15kind: PersistentVolumeClaim
16metadata:
17  name: postgres-data
18  namespace: db
19spec:
20  accessModes:
21    - ReadWriteOnce
22  storageClassName: gp3-retain
23  resources:
24    requests:
25      storage: 20Gi
26---
27# 3. The Pod mounts the claim by name — never the PV
28apiVersion: v1
29kind: Pod
30metadata:
31  name: postgres
32  namespace: db
33spec:
34  containers:
35    - name: postgres
36      image: postgres:17
37      volumeMounts:
38        - name: data
39          mountPath: /var/lib/postgresql/data
40  volumes:
41    - name: data
42      persistentVolumeClaim:
43        claimName: postgres-data

Two settings in that StorageClass are doing quiet, important work.

volumeBindingMode: WaitForFirstConsumer delays provisioning until a Pod actually schedules. Without it (Immediate, the default), the disk is created the moment the PVC exists — in whatever availability zone the provisioner picks. If the scheduler then places your Pod in a different zone, the Pod can never attach the disk and sits Pending forever. WaitForFirstConsumer lets the scheduler pick the node first, then provisions storage in the right zone. On any multi-zone cluster this should be your default.

allowVolumeExpansion: true is what makes growing the volume later possible at all. More on that below.

The binding lifecycle

A PV moves through a small state machine, visible in kubectl get pv under STATUS:

  • Available — provisioned, not yet claimed. Static PVs start here.
  • Bound — matched to exactly one PVC. The healthy steady state.
  • Released — the PVC was deleted, but the PV still holds the old data. A Released PV is not reusable. It still carries a reference to the dead claim (spec.claimRef), and Kubernetes will never bind a new PVC to it automatically — by design, so a stranger's claim can't land on a volume full of someone else's data. To reuse it, an admin must clear the claimRef and, in any sane world, wipe the data first.
  • Failed — automatic reclamation failed. Manual intervention required.

For binding to succeed, the PV must satisfy the PVC on every axis: capacity at least as large as requested, access modes a superset of those requested, and matching storageClassName. A subtle consequence of static provisioning: binding matches the best available PV, not an exact one — a 10Gi claim can bind to a 100Gi PV if that's the smallest match, and the other 90Gi is simply stranded. Dynamic provisioning sidesteps this by always creating an exact-size volume.

Kubernetes Production Readiness Checklist

The pre-launch checks we run before calling a cluster production-ready — probes, resources, RBAC, upgrades, and backups. Plain Markdown you can commit to your repo.

Free. Instant download. You'll also get the occasional deep-dive from the newsletter — unsubscribe anytime.

Reclaim policies — and the data-loss gotcha

The reclaim policy answers one question: what happens to the PV (and the real disk) when its PVC is deleted?

PolicyOn PVC deletionUse when
DeletePV and the backing disk are destroyedScratch space, CI, anything reproducible
RetainPV goes Released; data and disk surviveDatabases, anything you'd cry about

Here is the gotcha that has ended careers: most default StorageClasses ship with reclaimPolicy: Delete. The standard gp2/gp3 classes on EKS, standard on GKE, default on AKS — all Delete. Which means the default behaviour of every dynamically provisioned volume in your cluster is: delete the claim, lose the data, permanently, at the cloud-provider level. No trash bin, no undo. Someone cleaning up a namespace with kubectl delete ns staging deletes every PVC in it, and every Delete-policy volume underneath evaporates.

Two defences, use both:

bash
# Flip an existing bound PV to Retain — takes effect immediately
kubectl patch pv pvc-9f8a1b2c-... \
  -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

And for anything stateful that matters, use a StorageClass with reclaimPolicy: Retain from day one, plus real backups — reclaim policy protects you from accidental deletion, not from corruption. The full production hardening story (snapshots, topology, StatefulSets) is in the Kubernetes persistent volumes production guide, which is the natural next read after this post.

Access modes — and what actually enforces them

Four modes, and the names lie slightly:

ModeAbbrevMeaning
ReadWriteOnceRWORead-write by one node (not one Pod)
ReadOnlyManyROXRead-only by many nodes
ReadWriteManyRWXRead-write by many nodes
ReadWriteOncePodRWOPRead-write by exactly one Pod, cluster-wide

The subtlety everyone misses: RWO is per-node, not per-Pod. Two Pods on the same node can both write to an RWO volume simultaneously. If your application can't handle that, you want RWOP (stable since Kubernetes 1.29), which is enforced at the Pod level.

The second subtlety: access modes are attachment-time enforcement, not a capability query. Kubernetes itself never verifies that the backend can deliver a mode — some CSI drivers reject an impossible request at provisioning time (the modern EBS CSI driver refuses an RWX filesystem claim outright), but many will provision and bind happily. On those, you discover the truth when the second node tries to attach and fails. Modes describe what you'll be allowed to do; whether the backend can physically do it is a property of the storage (NFS and CephFS do RWX; EBS, GCE PD and most block storage do not).

How to check persistent volumes and claims

The inspection workflow, in the order you actually use it:

bash
1# The two views of the same relationship
2kubectl get pv                                  # cluster-wide: capacity, access modes, reclaim policy, STATUS, which claim
3kubectl get pvc -n db                           # namespaced: STATUS, bound volume, size, StorageClass
4
5# Everything about one claim — events at the bottom are where errors live
6kubectl describe pvc postgres-data -n db
7
8# Which Pod is using a PVC (describe shows "Used By"), or search the hard way
9kubectl describe pvc postgres-data -n db | grep "Used By"
10
11# Trace claim → volume → real-world disk ID
12kubectl get pvc postgres-data -n db -o jsonpath='{.spec.volumeName}'
13kubectl get pv <that-name> -o jsonpath='{.spec.csi.volumeHandle}'
14
15# Actual disk usage — PVCs report requested size, not used bytes.
16# The kubelet stats API is the source of truth without exec'ing in:
17kubectl get --raw /api/v1/nodes/<node>/proxy/stats/summary | jq '.pods[].volume'
18
19# Or the low-tech version inside the Pod:
20kubectl exec -n db postgres -- df -h /var/lib/postgresql/data

That kubelet stats detail matters more than it looks: kubectl get pvc shows CAPACITY — what was provisioned — and nothing anywhere in the API shows utilisation unless you scrape it. The kubelet exposes kubelet_volume_stats_used_bytes and kubelet_volume_stats_capacity_bytes to Prometheus, and an alert on their ratio is the difference between expanding a volume calmly on a Tuesday and doing it during an outage.

Expanding a PVC

If the StorageClass has allowVolumeExpansion: true, growing is one edit:

bash
kubectl patch pvc postgres-data -n db \
  -p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'

The rules: you can grow, you can never shrink. Filesystem resize usually completes online, but some drivers require the Pod to restart before the filesystem expansion finishes — watch kubectl describe pvc for a FileSystemResizePending condition, which tells you a Pod restart is needed to complete the job. If the class doesn't allow expansion, the patch is rejected outright and your path is the tedious one: snapshot, new bigger PVC, restore.

Deleting safely

The safe teardown order, given everything above:

  1. Check the reclaim policy first. kubectl get pv <name> -o jsonpath='{.spec.persistentVolumeReclaimPolicy}' — if it says Delete and you want the data, patch it to Retain before touching the PVC.
  2. Delete the Pod/workload using the claim. A PVC in active use is protected by the kubernetes.io/pvc-protection finalizer — deleting it just leaves it in Terminating until the last Pod goes away, which confuses people into force-removing finalizers. Don't; delete the consumer first.
  3. Delete the PVC. With Retain, the PV goes Released and your data is intact; with Delete, volume and disk are gone.
  4. Clean up Released PVs deliberately — verify backups, then delete the PV object and the backing disk yourself.

When a PVC is stuck Pending

The most common failure in this whole system is a PVC that sits Pending forever. kubectl describe pvc events tell you which flavour you have:

  • no persistent volumes available for this claim — static provisioning with no matching PV: wrong size, wrong access mode, or wrong/empty storageClassName.
  • storageclass.storage.k8s.io "foo" not found — typo in the class name, or the class the PVC was defaulted to has since been deleted. (Omitting the field when no default class exists gives the no persistent volumes available event instead — the field simply stays unset.)
  • waiting for first consumer to be created — not an error. WaitForFirstConsumer means the PVC binds only when a Pod uses it. Create the Pod.
  • Provisioner errors — quota exhausted, IAM permissions missing on the CSI driver, backend full. The event text quotes the driver's error verbatim.

And the mirror image on the Pod side — pod has unbound immediate PersistentVolumeClaims — gets its own dedicated walkthrough in fixing unbound PersistentVolumeClaims. If the Pod is Pending for reasons beyond storage, start with the general Pending Pods debugging guide.

Frequently Asked Questions

Can a Pod mount a PersistentVolume directly, without a PVC?

No. Pods reference PVCs by name in spec.volumes; there is no field for mounting a PV directly. The claim is the mandatory intermediary — that indirection is what keeps Pod specs portable across clusters with different storage backends.

Can two PVCs bind to the same PV?

Never — binding is strictly 1:1. What can happen is multiple Pods mounting the same PVC, if the access mode allows it (RWX, or RWO with all Pods on one node). If two workloads in different namespaces need the same data, you need an RWX-capable backend and, typically, two PV/PVC pairs pointing at the same underlying export.

Does deleting a PVC delete my data?

It depends entirely on the PV's reclaim policy. Delete (the default on most cloud StorageClasses) destroys the backing disk; Retain keeps the PV and data in a Released state. Check before you delete, not after — there is no undo at the cloud-provider level.

Why does my PVC say Bound but show no usage data?

Because the PVC API only tracks requested capacity, never utilisation. Actual usage comes from the kubelet stats API (kubelet_volume_stats_used_bytes in Prometheus) or df inside the Pod.

What's the difference between a StorageClass and a PV?

A PV is one concrete piece of storage; a StorageClass is a recipe for making PVs on demand. If PVs are inventory, the StorageClass is the factory — plus policy defaults (reclaim policy, expansion, binding mode) stamped onto everything it produces.

The one-paragraph summary

PV is supply, PVC is demand, StorageClass is the factory that makes supply appear on demand. Pods mount claims, never volumes. Binding is 1:1 and one-way. The two settings that will actually hurt you are reclaimPolicy: Delete on data you care about and volumeBindingMode: Immediate on multi-zone clusters — fix both in your StorageClass before the first stateful workload ships, then go read the production persistent volumes guide for the rest.

See also

Official References

Was this article helpful?

Be the first to rate this article

Related Topics

Kubernetes
PersistentVolume
PersistentVolumeClaim
StorageClass
Storage
kubectl
Stateful Workloads

Found this useful? Share it.

Practice this

Related tools

Read Next