The Object Model: Spec, Status & resourceVersion
Summary
- Every Kubernetes object shares one envelope -
metadata,spec, andstatus- regardless of kind. - Insight:
specis your intent,statusis the controller's report, and mixing them is the root of many operator bugs. - Key Concepts: spec vs status, resourceVersion (optimistic concurrency), ownerReferences (garbage collection), finalizers (cleanup hooks), UID.
- When to Use: Before writing controllers, doing server-side apply, or reasoning about deletes and update conflicts.
- Limitations/Trade-offs: Optimistic concurrency means writers must handle conflicts and retry, never assume exclusive access.
- Related Topics: The reconciliation loop, watches and informers, etcd and Raft.
Foundations
Every object in Kubernetes has the same top-level shape.
metadata holds identity: name, namespace, labels, annotations, UID, and resourceVersion.
spec holds desired state - what you want to be true.
status holds observed state - what the controller reports is actually true.
This uniformity is powerful. Pods, Deployments, Nodes, and your own custom resources all obey the same rules, so tools and controllers work the same way across every kind.
You write spec. Controllers write status. Keeping that boundary clean is the single most important habit when building on the API.
The API server even enforces this split for many resources through a separate status subresource, so a normal update to an object cannot accidentally change its status.
Mechanics & Interactions
resourceVersion is how Kubernetes prevents lost updates.
Every write to an object bumps its resourceVersion, which is derived from etcd's internal revision counter. It is an opaque token, not a number you should do math on.
When you send an update, you include the resourceVersion you last read. If the stored version has moved on, the API server rejects your write with a 409 Conflict.
This is optimistic concurrency: no locks, just detect-and-retry. Two writers race; the loser re-reads and tries again.
kubectl get deploy web -o jsonpath='{.metadata.resourceVersion}{"\n"}'Controllers embrace conflicts as routine. On a 409 they simply re-read the object and reconcile again - which is safe because reconcile is idempotent.
ownerReferences wire objects into a tree. A ReplicaSet's Pods carry an ownerReference to the ReplicaSet; the ReplicaSet carries one to the Deployment.
The garbage collector uses these links. Delete the Deployment and its ReplicaSet and Pods are collected automatically through cascading deletion.
Cascading delete has two modes. Foreground deletion removes children before the parent disappears; background deletion removes the parent immediately and cleans up children after.
finalizers are the counterweight to deletion. A finalizer is a string on an object that blocks final removal until a controller does required cleanup.
When you delete an object with finalizers, the API server does not erase it. It sets metadata.deletionTimestamp and leaves the object present.
metadata:
finalizers:
- example.com/drain-connections
deletionTimestamp: "2026-07-15T10:00:00Z"The owning controller sees the timestamp, performs cleanup (drain a load balancer, delete cloud resources), then removes its finalizer. Only when the finalizer list is empty does the object actually vanish.
Advanced Considerations & Applications
The spec-status split shapes correct controller design.
A controller reconciles by reading spec, observing the world, acting, and then writing status to report what it saw. Status must never be the thing that triggers behavior.
Server-Side Apply builds on this model with field-level ownership. Each field tracks which "manager" set it, so multiple controllers and users can own different parts of one object without clobbering each other.
Server-Side Apply lets you send only the fields you own and merge them, replacing the fragile read-modify-write dance for many workflows.
The object UID is the durable identity. Names can be reused after deletion, so ownerReferences match on UID to avoid adopting a same-named replacement.
Stuck deletes almost always mean a finalizer whose controller is gone or failing. The object sits with a deletionTimestamp forever because nothing removes the finalizer.
Force-removing a finalizer unblocks the delete but skips the cleanup it guarded, so leaked cloud resources are the common aftermath. Fix the controller first when you can.
Common Misconceptions
"resourceVersion is a number I can compare or increment." It is an opaque token tied to etcd revisions. Only use it as an equality check for concurrency, never as an ordinal you compute on.
"I can set status in my manifest." For resources with a status subresource, the API server ignores status on normal writes. Controllers own status.
"A finalizer makes an object undeletable." It delays deletion until cleanup runs. Once the controller removes the finalizer, the object is deleted.
"Deleting a parent orphans its children." By default, ownerReferences cause cascading deletion. Children are garbage-collected with the parent unless you request orphaning.
"Update conflicts are errors to fear." A 409 is expected under concurrency. The correct response is to re-read and retry, not to fail.
FAQs
Why separate spec and status? So intent and observation never collide. You declare in spec; the controller reports in status, and neither can silently overwrite the other.
What causes a 409 Conflict on update?
Your update carried a stale resourceVersion. Something wrote the object after your last read; re-read and retry.
How do I delete an object stuck with a deletionTimestamp? Find the blocking finalizer. Fix or restart its controller so cleanup runs, or, as a last resort, remove the finalizer manually and clean up leaked resources yourself.
What is the difference between name and UID? Name is human-friendly and reusable; UID is a cluster-unique, immutable identifier. OwnerReferences use UID to avoid adopting a reused name.
When should I use Server-Side Apply? When multiple actors edit one object, or when you want field-level ownership and safe merges instead of read-modify-write.
Related
- The Reconciliation Loop Explained
- Watches, Informers & the List-Watch Pattern
- etcd, Raft & Cluster State
- Control Plane Internals
- Kubernetes Internals Best Practices
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).