Skip to main content

Access modes and volume binding

Inspect 1.36

A PVC that reads Pending is usually working exactly as designed. Two properties decide how a volume behaves: its access mode, which sets how many nodes can write it, and its binding mode, which sets when and where the volume is created. Get these right and most storage confusion disappears.

Access modes: how many nodes can write#

A volume's access mode sets how many nodes can mount it at the same time.

  • ReadWriteOnce (RWO): one node mounts the volume read-write. Every pod that uses it must run on that node. Hetzner Cloud volumes and local TopoLVM volumes are RWO.
  • ReadWriteMany (RWX): many nodes mount the volume read-write at once. This needs a shared filesystem such as JuiceFS or NFS. Block and local volumes cannot do it.
  • ReadOnlyMany (ROX): many nodes mount the same volume read-only. Useful for static data that many pods read but none change.
Access mode Nodes that can write Example backend
ReadWriteOnce (RWO) One node Hetzner Cloud volume, local TopoLVM
ReadWriteMany (RWX) Many nodes at once JuiceFS, NFS
ReadOnlyMany (ROX) None, read-only on many nodes Static data shared across pods

RWO is per node, not per pod. Several pods can share one RWO volume as long as they all run on the same node. Set a node affinity on the pods to keep them together.

Note

Hetzner Cloud volumes are ReadWriteOnce only. To let pods on different nodes write the same directory, use .

The pod is scheduled first, then the storage follows#

The most common storage mistake is thinking backwards about placement. People assume they pick where a volume lives on the PVC and the pod then schedules to meet it. It works the other way around: the scheduler places the pod first, and the storage has to be there.

Which side leads depends on whether the volume exists yet:

  • A new volume follows the pod. With WaitForFirstConsumer binding, nothing is created until a pod that uses the PVC is scheduled. The scheduler picks the node, then the driver provisions the volume in that pod's topology: on that node for a local disk, in that location for a cloud volume. You never set the volume's place; the pod's placement sets it. This is zone-aware binding: the volume is created in the same location the pod runs, so a cloud volume never lands in a zone the pod cannot reach.
  • An existing volume pins the pod. Once a volume exists, it has a fixed home: a local volume lives on one server, a bound cloud volume lives in one Hetzner location. Kubernetes copies that home onto the pod as a volume node affinity, so the pod can only schedule where the volume already is. If that node is full, cordoned, or gone, the pod waits.

The diagram shows both directions:

flowchart TD
    subgraph fresh["A new volume follows the pod"]
      direction TB
      P1["Scheduler places the pod on a node"] --> V1["Driver provisions the volume<br/>in the pod's node or location"]
    end
    subgraph bound["An existing volume pins the pod"]
      direction TB
      V2["Volume already lives on one node or location"] --> P2["Volume node affinity forces the pod there"]
    end

This is why a single-writer workload on a cloud or local volume can only run where its data is, and why moving that workload means moving or recreating the data with it.

Binding: why Pending is normal#

A PVC binds to a real volume at one of two moments, set by the class's volumeBindingMode.

  • WaitForFirstConsumer: the volume is not created until a pod that uses the PVC is scheduled. Until then the PVC stays Pending. This is deliberate, not a failure, and it is what lets a new volume follow the pod.
  • Immediate: the volume is created as soon as the PVC exists, before any pod asks for it.

The two timelines diverge on when the volume is created:

flowchart LR
    subgraph wait["WaitForFirstConsumer"]
      direction TB
      W1["PVC created"] --> W2["Pending, no volume yet"]
      W2 --> W3["Pod that uses the PVC is scheduled"]
      W3 --> W4["Volume provisioned, PVC Bound"]
    end
    subgraph imm["Immediate"]
      direction TB
      I1["PVC created"] --> I2["Volume provisioned at once, PVC Bound"]
    end

The standard class and the local classes use WaitForFirstConsumer. A PVC you just created on these classes shows Pending and stays there until you schedule a pod that mounts it. Nothing is broken.

When Immediate binding fits#

Object-backed volumes have no node or location to match, so they can bind immediately. JuiceFS uses Immediate binding: its data lives in object storage that every node can reach, so there is nothing to wait for, and its PVCs bind straight away instead of sitting Pending.

A PVC that stays Pending even after its pod is scheduled is a different problem. See for the causes and fixes, and the for the binding mode of each class.