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.
- The supply/demand model
- Static vs dynamic provisioning
- A complete worked example
- The binding lifecycle
- Reclaim policies — and the data-loss gotcha
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 is | A piece of provisioned storage in the cluster | A request for storage by a user/workload |
| Scope | Cluster-scoped (no namespace) | Namespaced |
| Created by | Admin (static) or provisioner (dynamic) | Developer, or a StatefulSet's volumeClaimTemplates |
| Knows about | The actual backend — EBS volume ID, NFS server, Ceph image | Only size, access mode, StorageClass |
| Lifecycle | Independent of any Pod; can outlive everything | Lives in a namespace; deleted like any other resource |
| Analogy | Inventory on the shelf | The purchase order |
| Pod interaction | Never referenced directly by a Pod | Mounted 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.
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/reportsDynamic 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:
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-dataTwo 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 theclaimRefand, 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?
| Policy | On PVC deletion | Use when |
|---|---|---|
Delete | PV and the backing disk are destroyed | Scratch space, CI, anything reproducible |
Retain | PV goes Released; data and disk survive | Databases, 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:
# 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:
| Mode | Abbrev | Meaning |
|---|---|---|
| ReadWriteOnce | RWO | Read-write by one node (not one Pod) |
| ReadOnlyMany | ROX | Read-only by many nodes |
| ReadWriteMany | RWX | Read-write by many nodes |
| ReadWriteOncePod | RWOP | Read-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:
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/dataThat 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:
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:
- Check the reclaim policy first.
kubectl get pv <name> -o jsonpath='{.spec.persistentVolumeReclaimPolicy}'— if it saysDeleteand you want the data, patch it toRetainbefore touching the PVC. - Delete the Pod/workload using the claim. A PVC in active use is protected by the
kubernetes.io/pvc-protectionfinalizer — deleting it just leaves it inTerminatinguntil the last Pod goes away, which confuses people into force-removing finalizers. Don't; delete the consumer first. - Delete the PVC. With
Retain, the PV goesReleasedand your data is intact; withDelete, volume and disk are gone. - 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/emptystorageClassName.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 theno persistent volumes availableevent instead — the field simply stays unset.)waiting for first consumer to be created— not an error.WaitForFirstConsumermeans 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
- Kubernetes Persistent Volumes: Production Guide — the deep-dive follow-on: snapshots, topology, and hardening
- Rook Ceph vs Longhorn vs OpenEBS — choosing the storage engine behind your StorageClass
- Kubernetes Storage and CSI Drivers — the machinery that provisions and attaches volumes
- Fixing "pod has unbound immediate PersistentVolumeClaims" — the error this post helps you never see
- Fixing Kubernetes Pending Pods — when the Pod, not the PVC, is stuck
Official References
- Debug Pods — reading pod status, events and container states
- kubectl reference — command syntax, output formats and selectors
Was this article helpful?
Be the first to rate this article
Related Topics
Found this useful? Share it.


