resourceVersion and Optimistic Concurrency in Kubernetes

resourceVersion and Optimistic Concurrency in Kubernetes

How Kubernetes lets people, tools and controllers edit the same objects at the same time without silently overwriting each other, explained from first principles with real-world analogies and no code required.

Picture a big whiteboard in a busy school hallway. Dozens of students and teachers walk up to it all day long, each one reading what is written and then adding, erasing or fixing something. Now imagine two people reading the same corner of the board at the same moment. One decides to rewrite the lunch menu and the other decides to rewrite the same menu a different way. Both walk away happy, but only the second rewrite survives, and the first person never finds out their work vanished. A Kubernetes cluster is that whiteboard, except the people are developers, deployment pipelines, autoscalers and hundreds of automated controllers, and they write thousands of times every minute. Kubernetes needs a clever, lightweight rule to keep that crowd from erasing each other. That rule is a small piece of text called resourceVersion, and the idea behind it is called optimistic concurrency. This guide explains both, step by step.

1The Lost Update Problem

Before learning the solution, it helps to feel the problem it solves.

Kubernetes is a shared workspace. The things inside it, such as Pods, Deployments, Services, ConfigMaps and Nodes, are called objects, and no single person or program owns them exclusively. A developer might change a Deployment from a terminal. A deployment tool might push a new version of that same Deployment a few seconds later. An autoscaler might adjust the number of copies while both of them are still working. A controller might write progress information back onto the object. All of these writers talk to one central program, the API server, which keeps every object safe in a database called etcd.

When many writers share one object, a famous bug becomes possible. Computer scientists call it the lost update problem. It happens in three small steps. First, writer A reads the object. Second, writer B reads the same object. Third, each writer changes its own private copy and sends it back. The writer who saves last wins, and the earlier change disappears without any warning. Nobody did anything wrong. Each writer performed a perfectly normal read, edit and save. The danger hides in the gap of time between the read and the write.

Simple Analogy

Two cooks each photocopy the same recipe card from the kitchen board. One cook writes “add more salt” on her copy, and the other writes “use less sugar” on his. Each pins their copy back on the board, replacing the original. The board now shows only the last note, and the first cook’s improvement is gone. Neither cook did anything silly. The board simply had no way to notice that the card had changed while they were scribbling.

sequenceDiagram
    participant A as Writer A
    participant B as Writer B
    participant S as API Server
    A->>S: Read the object
    S-->>A: Copy 1
    B->>S: Read the object
    S-->>B: Copy 1
    A->>S: Save my edited copy
    S-->>A: Saved
    B->>S: Save my edited copy
    S-->>B: Saved, silently replacing A's work
        
FIG 1 — The lost update: both writers started from the same copy, so the last save erases the first

Why this matters even more in Kubernetes

In an ordinary website, two people rarely edit the same record in the same second. Kubernetes is different because automation never sleeps. Controllers run in endless loops, always comparing the world as it is with the world as it should be, and fixing any difference. Several of them may care about the same object at once. That means collisions are not rare accidents. They are a normal, daily part of how the system works, and the design must handle them calmly, thousands of times a day, without human help.

!
Why Silence Is Dangerous

A loud error is easy to fix because somebody notices it. A silent overwrite is far worse. A scaling change, a security setting or a label could vanish, and the cluster would keep running as if nothing happened until something broke much later.

1
shared source of truth: the API server
0
locks a client has to take or release
409
the HTTP status that says someone changed it first

2What Is resourceVersion?

A small piece of text on every object that works like a “last changed” stamp.

Every Kubernetes object has a section called metadata. Inside it you will find the name, the namespace, labels, annotations and several other bookkeeping fields. One of those fields is resourceVersion. It is a short string that the server changes every time the object is stored in a new form. You can think of it as a stamp that says “this is the exact edition of the object you are looking at.”

The way it is used is beautifully simple. When you read an object, the server hands you the object together with its current stamp. Later, when you want to save a change, you send the object back with the stamp you received. The server compares your stamp with the stamp on the stored object. If they match, nobody touched the object while you were working, so your change is accepted and a new stamp is created. If they do not match, somebody else got there first, so your change is refused and you are told to look again. No waiting, no locking, just one comparison.

Simple Analogy

A hospital keeps a chart at every bed with a “last updated” number in the corner. A nurse who wants to add a note first remembers the number she saw. Before writing, she checks that the number on the chart is still the same. If another nurse has updated the chart in the meantime, the number will be different, and she knows to read the new notes before adding hers.

Three facts to remember

First, resourceVersion is a string, not a number you should do arithmetic on. Second, it is opaque, which means its inside meaning belongs to the server and clients should only copy it and compare it for equality. Third, it is owned by the server. A client cannot make up a clever value to get its way. A client can only echo back a value it was previously given.

Its neighbours in the metadata

Several other metadata fields are easy to confuse with resourceVersion. Each answers a different question, so it is worth meeting them side by side.

Stamp

resourceVersion

Changes on every stored change to the object, including labels, status and metadata. Answers: “Is my copy still the latest?”

Identity

uid

A unique identity given when the object is created and never changed. If an object is deleted and recreated with the same name, the uid is different.

Desired State

generation

A counter that goes up when the desired state, the spec, changes. It ignores status updates, so it moves much more slowly than the stamp.

Progress Report

observedGeneration

Written by a controller into the status to say “I have already handled generation number N.” It lets readers know whether status is fresh.

Birth

creationTimestamp

The moment the object was created. Useful for age checks, but unrelated to concurrency.

Ownership

managedFields

A record of which tool owns which fields. It powers server-side apply and is a separate idea from the version stamp.

FieldQuestion it answersChanges whenSet by
resourceVersionIs this the latest stored edition?Any stored changeAPI server
generationHas the desired state changed?Spec changesAPI server
observedGenerationWhich desired state has the controller handled?Controller finishes workController
uidIs this the same object or a replacement?Never, for one objectAPI server
i
Remember

The stamp is about the whole stored object, not about one field. Even a status update by a controller changes it, which is why a spec edit and a status write can collide.

3Where resourceVersion Comes From

The stamp is borrowed from a counter inside etcd, the database underneath Kubernetes.

Kubernetes keeps all of its objects in etcd, a distributed key-value database. etcd has a feature that makes concurrency control easy: it keeps one global counter called the revision. Every successful write anywhere in the database moves that counter forward by exactly one. Each stored key also remembers two special numbers, the revision at which it was created and the revision at which it was last modified.

In the usual setup, the API server uses the last-modified revision of an object’s key as that object’s resourceVersion. This explains why the stamps look like large numbers that seem to jump around. If you write a ConfigMap, then a Pod, then the same ConfigMap again, the revisions might be 101, 102 and 103. The ConfigMap ends up with stamp 103 and the Pod keeps stamp 102. Each object only changes its stamp when that object itself is written, but the counter underneath moves for everyone.

flowchart LR
    W1["Write ConfigMap
counter becomes 101"] --> W2["Write Pod
counter becomes 102"] W2 --> W3["Write ConfigMap again
counter becomes 103"] W3 --> R["ConfigMap stamp: 103
Pod stamp: 102"]
FIG 2 — One global counter, many objects: each object keeps the counter value from its own last write
Simple Analogy

Think of a deli counter where one ticket machine hands out numbers to everyone in the shop. Your friend buying cheese and you buying ham both draw tickets from the same roll, so your numbers are not consecutive even though you came together. A gap in the numbers does not mean something was missed. It only means other customers were served in between.

What the numbers do and do not tell you

Because the counter is shared, the stamp does not tell you how many times your object has changed. A stamp that jumped by a thousand simply means the cluster was busy with other objects. Likewise, a small difference between two stamps does not mean “almost the same.” The only safe question is whether two stamps are equal.

etcd conceptMeaning in Kubernetes
Global revisionA cluster-wide counter that moves forward on every write
Create revisionThe counter value when the object first appeared
Mod revisionThe counter value at the object’s last change, used as its resourceVersion
CompactionOld history is discarded, so very old stamps can no longer be resumed from

Lists have a stamp too

When you ask for a list of objects, the answer carries one stamp for the whole list. It describes the moment the snapshot was taken. As you will see later, that stamp is the starting line for watching changes without missing or repeating anything.

!
Do Not Peek Inside

Today the stamp often equals an etcd revision, but the Kubernetes documentation tells clients to treat it as opaque. Other storage layers, such as aggregated API servers, may produce different values, and the format may change in the future. Compare for equality only.

4Optimistic versus Pessimistic Concurrency

Two very different philosophies for sharing something that several people want to change.

There are two classic ways to stop people from overwriting each other. The first is called pessimistic concurrency. It assumes trouble is likely, so it makes every writer take a lock before editing. While one writer holds the lock, everybody else must wait their turn. The second is called optimistic concurrency. It assumes trouble is unlikely, so it lets everyone work freely and checks only at the very end whether somebody else changed the thing in the meantime. If someone did, the late writer simply starts that one edit again.

Simple Analogy

Imagine a single popular library book. The pessimistic way is to let one reader check it out while everyone else queues, even if the first reader falls asleep with it. The optimistic way is to let everyone photocopy the book and, when they hand in their notes, have the librarian confirm the notes were made from the latest edition. Readers who used an old edition just redo their notes quickly.

Why Kubernetes chose optimistic

Several reasons point in the same direction. Clients in a cluster come and go constantly, and any of them can crash at any moment. A crashed client holding a lock would block everyone else until some timeout rescued the situation, and picking good timeouts for thousands of different clients is hard. The API is a stateless HTTP interface, so holding a lock across several separate requests would be awkward. Most objects are read far more often than they are written, and two writers rarely hit the same object in the same instant, so the common case is “no conflict, no cost.” Finally, etcd natively supports a transaction that says “write this only if the key is still at revision N,” which is exactly the check that optimistic concurrency needs.

QuestionPessimistic (locks)Optimistic (version check)
Do writers wait?Yes, in a queueNo, they proceed at once
What if a writer crashes?Lock may stay stuck until a timeoutNothing is held, so nothing is stuck
Cost when nobody collidesStill pays for lockingAlmost free
Cost under heavy collisionsOrderly but slowWasted work and retries
Fit for distributed clientsComplicatedNatural

Advantages

  • No lock service to run, monitor or recover
  • Crashed clients cannot block anyone else
  • Very cheap when collisions are rare
  • Simple for clients: read, change, save, retry on conflict
  • Collisions are reported loudly instead of hidden

Disadvantages / Trade-offs

  • Losers must redo their work
  • Heavy contention on one object causes retry storms
  • A slow writer can lose again and again
  • Clients must be written to handle conflicts properly
i
Compare-and-Swap

Engineers sometimes call this pattern compare-and-swap: compare what I expect with what is there, and swap in my new value only if they match. Processors use the same idea to update shared counters without locks, and Kubernetes applies it to whole objects.

5Anatomy of a Conflict: The Life of an Update

Follow one update from the moment a client reads an object to the moment it succeeds or is turned away.

Understanding a conflict is easiest when you walk through it slowly. Suppose a deployment tool wants to add a label to a Deployment while an autoscaler is changing its replica count. Here is the whole story, one step at a time.

1

Read

The client asks the API server for the object and receives it together with its current stamp, say version X.

2

Think and edit

The client changes its private copy. This may take milliseconds or several seconds, and meanwhile the world keeps moving.

3

Send the update

The client sends the edited object back, still carrying version X in its metadata.

4

Compare

The server checks whether the stored object is still at version X. This check is made atomically against the database, so two writers cannot both pass it.

5

Accept or refuse

If the versions match, the change is stored and the client receives the object with a brand new stamp. If they differ, the client receives a 409 Conflict response and nothing is stored.

6

Recover

After a refusal, the client must read the latest object again, apply its intended change to that fresh copy, and try once more.

sequenceDiagram
    participant C1 as Deployment Tool
    participant C2 as Autoscaler
    participant S as API Server
    C1->>S: Read Deployment
    S-->>C1: Object at version 7
    C2->>S: Read Deployment
    S-->>C2: Object at version 7
    C2->>S: Update replicas, expecting version 7
    S-->>C2: Accepted, now version 8
    C1->>S: Update label, expecting version 7
    S-->>C1: 409 Conflict, stored version is 8
    C1->>S: Read Deployment again
    S-->>C1: Object at version 8
    C1->>S: Update label, expecting version 8
    S-->>C1: Accepted, now version 9
        
FIG 3 — A refused update followed by a successful retry on fresh data

What the error looks like

The refusal arrives as an HTTP 409 with a reason of Conflict. The human-readable message is usually something like “the object has been modified; please apply your changes to the latest version and try again.” The message is advice as well as a complaint, and it describes exactly the right recovery. Note that a 409 can also mean something else, namely that you tried to create an object whose name already exists. The reason field, AlreadyExists, tells the two apart.

What happens if you send no stamp at all

If an update arrives without a resourceVersion, the outcome depends on the kind of object. Many built-in resources allow an unconditional update, which means the server skips the check and the last writer wins. Some resources refuse an update that has no stamp and answer with a validation error. Leaving the stamp out is therefore a way of switching the safety net off, and it should be a deliberate choice rather than an accident.

!
Retry Correctly

Sending the very same request again after a conflict will fail every single time, because it still carries the old stamp. A correct retry reads fresh data first and re-applies the intended change on top of it.

6Inside the API Server

What actually happens between a request arriving and the database accepting or rejecting it.

The API server is a front door with several checkpoints. Every write request walks through the same sequence, and the version check sits at the very end, right next to the database, where it can be made truly reliable.

flowchart TD
    A["Request arrives"] --> B["Authentication: who are you?"]
    B --> C["Authorization: may you do this?"]
    C --> D["Mutating admission: adjust the object"]
    D --> E["Schema validation: is it well formed?"]
    E --> F["Validating admission: any policy objections?"]
    F --> G["Storage layer: read current, run preconditions"]
    G --> H{"etcd transaction: is stored version still the expected one?"}
    H -->|Yes| I["Write and return new stamp"]
    H -->|No| J["Re-read, then retry internally or return 409"]
        
FIG 4 — The path of a write request, ending in the atomic version check

The early checkpoints

Authentication proves who is calling. Authorization, usually through role-based access control, decides whether that caller is allowed to update this kind of object. Mutating admission may adjust the incoming object, for example by adding default values or injecting extra settings. Validation then checks that the result is well formed, and validating admission applies policy rules. None of these steps look at the version stamp for concurrency purposes, because the world could still change after they finish.

The storage layer and the atomic check

The storage layer reads the currently stored object, sometimes using a recent cached copy as a quick hint, and applies the requested change to it. It then verifies the preconditions that the request carried, such as the expected stamp or expected uid. The final and authoritative step is a single etcd transaction that says, in effect, “if this key’s last-modified revision is still the one I read, store the new value, otherwise tell me what is there now.” Because etcd runs that transaction atomically, two competing writers can never both succeed from the same starting point.

If the transaction fails, what happens next depends on the request. When the client pinned a specific version, the server cannot guess what to do, so it returns a 409 Conflict and lets the client decide. When the client did not pin a version, for example in a patch that only describes a small change, the server can safely re-read the latest object, re-apply the small change and try again by itself. The client never even notices.

No-op writes are skipped

The server also notices when an update would store exactly the same bytes that are already stored. In that case it skips the database write, the stamp does not change, and no watch events are produced. This is a quiet but valuable optimisation. Controllers that re-apply the same desired state again and again cost almost nothing, and watchers are not woken up for nothing.

i
Webhooks and Conflicts

A conflict can occur after mutating webhooks have already run, because the version check happens last. Webhooks may therefore see the same logical request more than once, so they should behave the same way each time they are called.

7Update, Patch and Apply: Three Ways to Write

Kubernetes offers several write styles, and each one treats the version stamp differently.

Many people assume that every write in Kubernetes works the same way. In reality there are three main styles, and picking the right one can reduce conflicts dramatically. They differ in how much of the object they send and in who is responsible for resolving a collision.

Replace

Update (PUT)

Sends the whole object back. It normally carries the stamp, so a collision is reported to the client as a 409.

Change Only

Patch

Sends only the difference. The server applies it to the latest stored object, so no conflict occurs unless the patch itself includes a stamp.

Declare Ownership

Server-Side Apply

Sends the fields you care about and claims them. The server tracks ownership per field and reports conflicts only when two owners disagree about a field.

Separate Door

Subresources

Status and scale have their own endpoints so different writers can touch different parts, though they still share one stamp.

StyleWhat it sendsCarries stamp?Conflict whenBest for
UpdateWhole objectYes, normallyStored stamp differsRead-modify-write of a full object
PatchOnly the changeOptionalOnly if a stamp is includedSmall targeted edits
Server-side applyFields you ownNot requiredAnother owner holds the fieldShared objects with many tools

Patch is gentle, not magic

A patch describes what to change instead of what the whole object should look like, so it naturally avoids most collisions. If you only want to add one label, a patch leaves every other field exactly as the latest stored object has it. There are several patch formats. A JSON patch is a list of precise operations and can even include a test operation that fails if a value is not what you expect. A merge patch overlays your change on top of the object, and a strategic merge patch does the same while understanding how lists of built-in objects should be combined.

!
Lists Can Surprise You

A plain merge patch replaces an entire list instead of merging into it. If two tools both patch the same list that way, the second patch can still wipe out the first tool’s entries even though no conflict was reported.

Two different kinds of 409

Server-side apply introduces a second meaning for the same status code. A version conflict says “your copy is old.” A field conflict says “another manager owns this field and you tried to change it to a different value.” The first is solved by reading again. The second is a question of ownership: you can choose to give up the field, agree with the current value, or deliberately force your value and take ownership. Telling these two situations apart in logs saves a lot of confusion.

Simple Analogy

A full update is like handing in a whole rewritten page and hoping nobody else edited it meanwhile. A patch is like passing a sticky note that says “change this one word.” Server-side apply is like a shared document where each person puts their name next to the paragraphs they are responsible for, and the document only complains if two names claim the same paragraph.

8Preconditions on Delete and Other Operations

The same “only if unchanged” idea protects more than updates.

Version stamps are not limited to saving edits. Delete requests can carry preconditions too, which are conditions that must be true or the delete is refused. Two preconditions are available: the object’s uid and its resourceVersion. They let a caller say “delete this exact thing, and only if it is still the thing I looked at.”

Simple Analogy

Imagine you leave a note on a parked car that says “tow the blue car in space 12.” If the blue car drove away and a red car parked in the same space, a careless tow truck would remove the wrong car. A careful one checks the licence plate, which is what the uid precondition does for objects.

Why the uid matters

Names are reused. A StatefulSet may delete Pod “web-0” and create a new Pod with the very same name seconds later. A controller that decided to delete the old “web-0” could accidentally delete the healthy new one if it only used the name. Because the uid is different for every incarnation, a delete with a uid precondition fails safely with a conflict when the object has been replaced.

Delete only this incarnation

A controller wants to remove a particular Pod it inspected. It attaches that Pod’s uid. If the Pod was already replaced, the delete is refused and the new Pod survives.

Delete only if unchanged

A cleanup tool decided an object was unused based on what it read. By attaching the stamp, it ensures the delete only happens if nobody has touched the object since, such as by adding a new reference.

Garbage collection

Owner references record the uid of the owner, not just its name. The garbage collector relies on this to avoid removing dependents because of a different object that merely shares a name.

Create as a one-winner race

Creation is protected by name uniqueness. If two clients try to create the same name, one wins and the other receives an AlreadyExists error. Many coordination tricks, such as the first lease holder, are built on this.

i
Binding Is Protected Too

When the scheduler assigns a Pod to a Node, the API server refuses to bind a Pod that already has a Node. Two schedulers racing over one Pod cannot both succeed, which is the same principle at work.

9resourceVersion When Reading: Freshness Choices

On reads, the stamp means something different: how fresh does my answer have to be?

So far the stamp has been a guard on writes. When you read or list objects, you can also pass a resourceVersion, but here it acts as a freshness dial. It lets the caller choose between “give me the most accurate answer even if it costs more” and “give me something quick and good enough.”

What you ask forWhat you getTrade-off
Stamp left outThe most recent data, confirmed against the source of truthMost accurate, most expensive
Stamp set to zeroAny version the server can answer with, usually from its in-memory cacheFastest, possibly stale
A specific stamp with a “not older than” match ruleData at least as fresh as that stampBalanced
A specific stamp with an “exact” match ruleThe data exactly as it was at that stamp, if history still existsPrecise, can expire after compaction
Simple Analogy

Asking for “any version” is like grabbing whichever newspaper is lying on the table. Leaving the stamp out is like phoning the printing office and asking what today’s front page says right now. “Not older than” is like saying you want a paper from this morning or later, but you do not mind which edition.

Why the cache matters

The API server keeps a watch cache of recent objects in memory. Answering from that cache protects the database from a flood of heavy list requests, which is vital in big clusters. The price is that the cache can lag the database by a small amount. In recent Kubernetes releases the server can also serve consistent reads from its cache after confirming the cache is caught up, which brings together accuracy and speed.

Pagination and the continue token

Large lists can be fetched in pages using a limit and a continue token. The first page fixes a snapshot at a particular stamp, and every later page reads from that same snapshot, so you never see a half-old, half-new list. The snapshot cannot be kept forever, though. If the database compacts away that history before you finish, the next page is refused as expired and you have to start the listing again.

!
Stale Reads Cause Conflicts

Reading with the “any version” choice and then writing the result back is a perfectly legal pattern, but the stamp you hold may already be behind. Expect conflicts and be ready to retry, or use a patch that does not depend on the stamp.

10Watches, Informers and the 410 Gone Response

How programs follow a stream of changes without polling and without missing anything.

Controllers do not repeatedly ask “what changed?” every second. Instead they open a watch, a long-lived connection through which the server pushes an event whenever an object is added, modified or deleted. A watch takes a starting stamp, and the server sends every change that happened after that point. This is the second great use of resourceVersion.

The list-then-watch pattern

The standard recipe, built into the client libraries as an informer, has two parts. First, list all objects of a kind and remember the stamp attached to the list. Second, start a watch from exactly that stamp. Because the list describes the world at stamp N and the watch begins at N, there is no gap where a change could slip through and no overlap where a change could be delivered twice. The informer keeps a local cache updated from the events, and controllers read from that cache instead of hammering the server.

flowchart TD
    A["List all objects"] --> B["Save snapshot and its stamp"]
    B --> C["Watch from that stamp"]
    C --> D["Apply each event to local cache"]
    D --> E{"Connection lost or stamp too old?"}
    E -->|Connection lost| F["Reconnect from last stamp seen"]
    F --> C
    E -->|410 Gone| A
    D --> C
        
FIG 5 — The list-then-watch loop used by informers, including the relist on 410 Gone
1

List

The informer gets every object plus the list’s stamp.

2

Watch

It opens a stream starting at that stamp and updates its cache as events arrive.

3

Disconnect

Networks drop, servers restart, and proxies close idle connections. The informer remembers the stamp of the last event it processed.

4

Resume or relist

It reconnects from the remembered stamp. If that point is too old, the server answers 410 Gone and the informer starts over with a fresh list.

Why 410 Gone happens

The server only remembers a limited window of recent history. The watch cache holds recent events in memory, and the database periodically compacts old revisions away, by default roughly every five minutes. If a client asks to resume from a stamp older than the remembered window, the server cannot replay the missing events honestly, so it says 410 Gone, meaning “that point in history is no longer available.” The correct response is to relist and rebuild the cache.

i
Bookmarks

Watches can ask for bookmark events. These are small messages that carry no object change, only the latest stamp. They keep a quiet watcher’s resume point fresh, which reduces expensive relists after a reconnect.

Simple Analogy

Imagine following a group chat. You note the last message you read, and when you come back you scroll from that message onward. If the chat app only keeps the last hour of messages and you were away for a day, you cannot scroll from your spot. You have to reload the whole conversation and start fresh, which is exactly a relist.

11How Controllers Live with Conflicts

For a controller, a conflict is not an emergency. It is a normal signal that says “look again.”

A controller works in a loop: observe the current state, compare it with the desired state, and act to close the gap. To stay fast, it usually reads from its informer cache, which is a copy kept up to date by the watch stream. That copy is almost always right, but it can be a few moments behind. When the controller writes using a stamp taken from a slightly stale copy, the server may answer with a conflict.

This is expected, and the right reaction is calm. Kubernetes controllers are level-triggered, which means they act on the current state of the world rather than on a history of events. A failed write is therefore not a lost instruction. The controller puts the object’s key back on its work queue, waits a short and growing delay, and handles it again. By then the cache has caught up, the stamp is fresh, and the write succeeds. Because the reconcile logic is idempotent, meaning that running it twice gives the same result as running it once, repeating it is harmless.

flowchart TD
    A["Reconcile an object"] --> B["Read latest copy from cache"]
    B --> C["Decide the needed change"]
    C --> D["Send write with the stamp"]
    D --> E{"Result?"}
    E -->|Success| F["Done, forget this attempt"]
    E -->|Conflict| G["Requeue the key with a growing delay"]
    G --> A
    E -->|Other error| H["Log it and requeue"]
    H --> A
        
FIG 6 — A reconcile loop that treats conflicts as a cue to try again with fresh data

Short retry loops

Sometimes a controller prefers to retry immediately instead of requeueing. The client libraries provide a standard helper for this, usually called retry on conflict. It re-reads the live object, runs the caller’s change function on that fresh copy, and tries to write again, backing off between attempts and giving up after a few tries. The crucial detail is the order of operations. What is repeated is the intention, not the old object. Reapplying a stale copy over and over would never succeed.

Simple Analogy

A waiter adds dessert to a table’s order on a shared tablet. The tablet says the order changed because a colleague just added drinks. A good waiter does not argue with the tablet. They refresh, see the drinks, and add the dessert on top. The goal, “dessert for table five,” stays the same while the page they build on is updated.

!
Fighting Controllers

If two controllers each insist on a different value for the same field, they will undo each other forever, producing a storm of writes and conflicts. Retrying harder does not fix this. The fix is to give each field a single owner.

i
Rate-Limited Queues

Controller work queues add a delay that grows for each failing key, and they forget the delay after a success. This keeps a problem object from consuming all the controller’s attention.

12Leader Election: Optimistic Concurrency as a Lock

A clever use of the version check that lets many copies of a program agree on a single boss.

For high availability, important components such as the scheduler and the controller manager run as several identical copies. But only one copy should actively do the work at a time, otherwise they would make duplicate or contradictory decisions. The copies choose a leader by competing for a small Kubernetes object called a Lease.

A Lease records who currently holds it, how long the holder may keep it, when it was last renewed, and how many times leadership has changed. The leader renews the Lease regularly by updating it. The other copies keep watching. If they see that the Lease has not been renewed for longer than its allowed duration, they assume the leader is gone and try to take over.

Where optimistic concurrency saves the day

Suppose the leader crashes and two standby copies notice at the same time. Both read the Lease at the same stamp, both prepare an update that says “I am the holder now,” and both send it with that stamp. etcd accepts the first and the server answers the second with a conflict. The loser immediately knows it did not win and goes back to waiting. No special lock server, no extra coordination, only the version check doing exactly what it does everywhere else.

sequenceDiagram
    participant X as Candidate X
    participant Y as Candidate Y
    participant S as API Server
    X->>S: Read Lease
    S-->>X: Expired, version 40
    Y->>S: Read Lease
    S-->>Y: Expired, version 40
    X->>S: Claim Lease, expecting version 40
    S-->>X: Accepted, now version 41
    Y->>S: Claim Lease, expecting version 40
    S-->>Y: 409 Conflict
    Note over X: X is the leader and renews regularly
    Note over Y: Y keeps waiting and watching
        
FIG 7 — Two candidates race for a Lease and the version check picks exactly one winner
15s
typical lease duration before a takeover is allowed
10s
typical deadline for the leader to renew
2s
typical gap between acquire or renew attempts

The clock question

You might wonder whether different computers disagreeing about the time could break this. The design is careful about that. Each candidate does not trust the timestamp written by the leader. Instead it notes, using its own clock, how long it has seen the record stay unchanged. Only when that locally measured time exceeds the lease duration does it attempt a takeover. A leader that cannot renew within its deadline steps down on its own rather than carrying on and risking two active leaders.

i
A Fair Warning

Leader election limits who does the work, but a slow, paused leader may briefly still believe it is in charge. Well-designed programs stop acting the moment they fail to renew, and rely on the version check to reject any late writes.

13Hot Objects and Write Contention

Conflicts become a real problem when too many writers crowd around too few objects.

The chance of a conflict grows with two things: how many writers touch the same object and how often each one writes. An object that many components update frequently is called a hot object. Hot objects waste effort because losers redo their work, and in extreme cases the retries themselves create more traffic and more conflicts. Kubernetes itself has had to redesign a few of its own features to cool down hot objects.

Node heartbeats

Every node regularly tells the cluster it is alive. When that heartbeat was written into the large Node object, thousands of nodes produced a constant stream of big writes. Kubernetes moved heartbeats into tiny per-node Lease objects, so the heavy Node object is now rewritten only when something meaningful changes.

Service endpoints

An older design kept every address behind a Service in one single object. Each Pod change rewrote the entire list and every watcher received the whole thing again. EndpointSlices split the addresses into small chunks, by default up to one hundred per slice, so a single Pod change touches only one small object.

Replica count tug of war

A deployment pipeline that always writes a fixed replica number will keep resetting the value that an autoscaler just chose. Both sides think they own that field. The cure is to remove the replica field from the pipeline’s declaration so the autoscaler owns it alone.

The shared registry ConfigMap

Teams sometimes use one ConfigMap as a shared notebook where dozens of jobs add entries. It works with five writers and falls apart with five hundred. Giving each writer its own object removes the contention completely.

Simple Analogy

A single shared notebook for a whole classroom means students constantly wait or overwrite each other. Giving every student their own notebook, and letting the teacher gather them when needed, removes the crowding entirely. Splitting data so each writer has its own object is the same trick.

The one-writer principle

The most reliable way to prevent conflicts is to design so that each object, or at least each field, has exactly one writer. Spec fields are written by users and tools that express intent. Status fields are written by the controller that observes reality. Separate objects are used for separate concerns. When this principle is followed, the remaining conflicts are rare and harmless. When it is ignored, no retry strategy can save the system.

i
Spot a Hot Object

Look for objects whose stamp changes many times per minute, objects that are very large, and objects that many different components write. Those are the first candidates to split up.

14Design Patterns for Conflict-Safe Controllers

A small toolbox of habits that keeps controllers calm under concurrency.

Anyone who builds tools or operators for Kubernetes will meet conflicts sooner or later. The good news is that a handful of well-known patterns handle nearly every situation. Treat the following cards as a checklist when designing a new controller.

Pattern 1

Re-read, then re-apply

On a conflict, fetch the latest object and apply your intended change to it. Never resend the stale copy.

Pattern 2

Prefer narrow patches

Send only the fields you really change. Smaller writes overlap less and rarely need the stamp at all.

Pattern 3

Server-side apply with a named manager

Give each tool a stable, unique manager name so ownership is clear and conflicts point to a specific party.

Pattern 4

One writer per field

Divide responsibility so that no two components ever set the same field.

Pattern 5

Status through its own door

Write progress to the status subresource, keeping it apart from user intent in the spec.

Pattern 6

Idempotent reconcile

Design the loop so that running it again is always safe. Then every retry is free of side effects.

Pattern 7

Backoff with jitter

Wait a growing and slightly random time between retries so competing writers do not collide in lockstep.

Pattern 8

Leader election for singletons

Use a Lease when only one replica should do a job at a time.

A safe update recipe

Putting the patterns together gives a simple recipe that works for most controllers.

1

Read the freshest copy you can afford

Use the cache for speed, and read live from the server when accuracy matters more than cost.

2

Compute the smallest change

Decide exactly which fields must differ, and nothing more.

3

Write with the narrowest tool

Use a patch or an apply request, and attach the stamp only when you truly need a precondition.

4

On conflict, back off and repeat

Re-read, re-apply the intention, and wait a little longer each time.

5

Give up gracefully

After a few attempts, requeue the key and move on. The next loop will start from fresh state.

i
The Generation Handshake

Controllers can copy the object’s generation into observedGeneration in the status once they finish. Anyone reading the object can then tell whether the status describes the current desired state or an older one.

15Common Mistakes and Anti-patterns

Five habits that look harmless and cause real trouble in production.

Most conflict-related bugs come from a short list of misunderstandings. Studying them in advance is cheaper than discovering them during an outage.

ANTI-PATTERN-01 Avoid
Problem

A tool treats the stamp as a number, checks whether one is “bigger” than another, and uses that to decide which object is newer.

Why It’s Harmful

The stamp is opaque. It may not be numeric in every setup, its ordering is not promised, and numbers from different object kinds are not meant to be compared. The tool may work for years and then fail after an upgrade or against an extension API.

Correct Approach

Only test whether two stamps are equal. For ordering of events, rely on the order in which a single watch delivers them.

ANTI-PATTERN-02 Avoid
Problem

After a conflict, the client retries the identical request in a tight loop.

Why It’s Harmful

The request still carries the outdated stamp, so every attempt fails. The loop wastes capacity and adds load to an already busy API server.

Correct Approach

Re-read the object, apply the intended change to the new copy, and retry with backoff and a limit on attempts.

ANTI-PATTERN-03 Avoid
Problem

To make conflict errors disappear, the developer strips the stamp from updates so the server always accepts them.

Why It’s Harmful

The errors vanish but the underlying collisions remain, and now they erase other writers’ changes silently. This trades a visible, fixable problem for an invisible one.

Correct Approach

Keep the stamp and handle conflicts properly, or switch to patches or server-side apply, which are designed for shared objects.

ANTI-PATTERN-04 Avoid
Problem

A controller holds on to an object it read long ago and keeps writing it back with the original stamp.

Why It’s Harmful

Once anything else changes the object, every write is refused, and the controller may appear stuck. Long-lived copies also hide newer information.

Correct Approach

Treat each reconcile as a fresh start. Read the current state at the beginning, and never keep object copies across loops.

ANTI-PATTERN-05 Avoid
Problem

An application stores the stamp in its own database as if it were a business version number.

Why It’s Harmful

The stamp changes for reasons unrelated to business meaning, such as label edits and status updates, and it can become unusable after history is compacted. It also couples the application to internals it should not know.

Correct Approach

Keep an explicit version or hash of your own data, and use the generation field when you want to track changes to desired state.

Symptom to cause quick guide

SymptomLikely causeHealthy response
A burst of 409 errors from one controllerStale cache or two writers on one objectCheck ownership, add backoff
Controller never succeeds on one objectRetrying a stale copyRe-read before each attempt
Fields keep revertingTwo tools enforcing different valuesAssign one owner per field
Frequent 410 Gone and relistsWatchers falling behind or very quiet watchesEnable bookmarks, check resources
Updates vanish without errorsUnconditional updates or whole-list merge patchesUse stamps or server-side apply
!
A Rule of Thumb

If a conflict error makes you want to switch the check off, that is usually a sign the design needs fewer writers on that object, not a weaker guard.

16Security, Monitoring and Observability

Version stamps protect integrity, not secrecy, and they leave useful footprints you can measure.

Security thinking

It is important to be clear about what the stamp is not. It is not a password, a signature or an access control. It does not decide who may change an object. That job belongs to authentication and role-based access control. The stamp only answers the narrow question “has this changed since I looked?” Preconditions on delete protect against accidents and races, such as removing a replacement object by mistake, but they do not stop a person who has permission from doing something harmful on purpose. Policies, such as admission rules that make certain fields immutable, are the right tool for that.

Opacity also has a small security benefit. Because clients are told never to interpret the stamp, nobody builds trust decisions on top of its structure, and the platform stays free to change how it is produced.

What to watch

Conflicts leave clear traces in logs, metrics and audit records. A few conflicts per hour in a busy cluster are normal and prove the safety net is working. What deserves attention is a sustained high rate, a rate that keeps climbing, or conflicts on the same object that never resolve.

Metrics

API server request counts

The request counter at the API server, split by response code, shows how many 409 and 410 responses occur for each verb and resource kind.

Metrics

Client and queue metrics

Client-side request counters and work queue retry counters show which controller is struggling and how deep its backlog is.

Logs

Controller messages

Controllers usually log the phrase that an operation cannot be fulfilled because the object has been modified. A repeating message for one object is a clue.

Audit

Audit records

Audit logs record the user, the verb, the object and the response code, so you can see exactly who was colliding with whom.

SignalWhat it tells youLooks healthyLooks worrying
409 responses per resourceWhere collisions occurLow and steadyRising or concentrated on one object
Work queue retriesHow often controllers repeat workSmall bursts that settleConstant retries on the same keys
410 responses on watchesWatchers losing their placeRare, after restartsFrequent, causing relists
Watch and list latencyLoad on the API serverStableSpikes after many relists
Database size and compactionHistory retention pressureBoundedGrowing without limit
i
Look at Rates, Not Single Events

One conflict means nothing. Ask whether the conflict rate is stable, whether retries eventually succeed, and whether a few objects account for most of the noise.

17Advantages, Disadvantages and Trade-offs

A fair look at what the design buys and what it costs.

No concurrency strategy is perfect, and Kubernetes made a conscious bet. Seeing the whole picture helps you decide when to lean on the built-in behaviour and when to design around it.

Advantages

  • Lost updates are detected instead of hidden
  • No lock service, no lock timeouts and no deadlocks
  • Works the same for people, scripts and controllers
  • Scales to very large numbers of mostly independent writers
  • Gives watches a precise starting line with no gaps
  • Makes leader election possible with ordinary objects

Disadvantages / Trade-offs

  • Every client must be written to expect and handle 409 responses
  • Hot objects waste work through retries
  • The stamp covers the whole object, so unrelated edits can still collide
  • Opaque values give limited help for debugging
  • Fairness is not guaranteed, and an unlucky writer may retry many times

How it compares with other approaches

ApproachIdeaStrengthWeakness
Last write winsAccept every writeSimplestSilently loses work
Pessimistic lockingLock before editingOrderly under heavy contentionStuck locks, slow, complex
Optimistic version checkVerify nothing changed at save timeCheap and crash-safeRetries under contention
Field-level ownership mergeTrack who owns each fieldFewer false conflictsNeeds discipline about managers

Kubernetes actually uses a blend. The coarse version stamp guards whole-object writes, while server-side apply adds field-level ownership on top for shared objects. This combination keeps the cheap, crash-safe core and softens its biggest weakness, which is that unrelated edits to the same object can collide.

“Optimistic concurrency does not prevent collisions. It makes sure every collision is noticed.”

18Real-World and Industry Examples

Where these ideas appear in everyday cluster life.

You do not need to build a controller to meet the version stamp. It quietly shapes everyday tools and platform designs, and recognising it makes puzzling behaviour easier to explain.

Editing live objects by hand

When an engineer opens a live object in an editor and saves it, the saved copy carries the stamp from when it was opened. If a controller changed the object while the editor was open, the save is refused with a conflict, and the engineer’s edits are kept in a temporary file so they are not lost. This is optimistic concurrency protecting a human.

GitOps tools beside autoscalers

Platforms that continuously sync manifests from a repository often share objects with autoscalers and operators. Teams that adopt server-side apply with a clear manager name, and leave out fields owned by other tools, avoid endless reverting battles and noisy conflict logs.

Operators for databases and certificates

Operators write progress and health into the status of their custom resources while users edit the spec. Splitting spec and status across separate write paths, and recording the observed generation, keeps these two audiences from stepping on each other.

Scheduler and controller manager high availability

Production control planes run several replicas of core components, and each group elects one active leader through a Lease. The version check ensures exactly one replica wins each election, even when several notice the old leader’s disappearance in the same instant.

Large multi-tenant platforms

Companies that run thousands of nodes and many teams learn quickly that shared objects do not scale. They shard data into per-team or per-node objects, use small heartbeat objects, and monitor conflict rates as an early warning of design problems.

Disaster recovery tools

Backup and restore tools that capture and replay objects must be careful about stamps and uids. A restored object receives a new identity and a new stamp, and tools that assume the old values will still match find their preconditions failing, which is correct and expected behaviour.

i
Industry Habit

Mature teams track the conflict rate the same way they track error rates and latency. A calm graph is proof that ownership boundaries are clean.

19Frequently Asked Questions

Short answers to the questions that beginners ask most often.

Q1Is resourceVersion the same as the generation field?

No. The stamp changes on every stored change, including status and labels. Generation only increases when the desired state in the spec changes, so it moves far more slowly.

Q2Can I set resourceVersion myself to force an update through?

You can only send back a value you received. Sending a different value simply causes a conflict. Leaving it out may allow an unconditional update, but that removes your protection against overwriting other changes.

Q3Does a 409 mean something is broken?

Usually not. It means someone else changed the object first, and it is the normal way the system keeps writers honest. It becomes a concern only when it is constant or never resolves.

Q4Why does the stamp sometimes stay the same after I apply a change?

If the new object is identical to what is stored, the server skips the write, so nothing changes and no watch event is sent. The same thing happens when you re-apply an unchanged definition.

Q5Why can I not compare two stamps to see which is newer?

The format is opaque and may differ between storage setups. The official guidance allows equality checks only. Event order should be taken from the order of a single watch stream.

Q6Do patches avoid conflicts completely?

Mostly, because the server applies the small change to the latest object. A patch can still conflict if it includes a stamp, and some patch types replace whole lists, which can overwrite another tool’s entries.

Q7What does 410 Gone mean on a watch?

The point in history you asked to resume from has been forgotten, usually because it is older than the retained window. The right response is to list everything again and start a new watch from the new list’s stamp.

Q8Why do controllers read from a cache if it can be stale?

Reading from a local cache is fast and spares the API server from enormous load. Because controllers are level-triggered and idempotent, the occasional stale read just causes a harmless conflict and a retry.

Q9How is a field conflict in server-side apply different from a version conflict?

A version conflict says your copy is outdated. A field conflict says another manager owns a field you tried to set to a different value. The first is fixed by re-reading, the second by deciding who should own the field.

Q10Does the stamp protect secrets or control who can edit?

No. It is purely a freshness check. Access control belongs to authentication and authorization, and policy rules belong to admission control.

20Summary and Key Takeaways

Kubernetes is a crowded whiteboard where people and programs constantly read and write the same objects. Instead of making everyone queue behind locks, it stamps every object with a resourceVersion, lets writers work freely, and checks at save time that nobody changed the object in the meantime. A mismatch produces a clear 409 Conflict, and the loser simply reads the latest copy and tries again. The same stamp gives reads a freshness dial, gives watches an exact starting line, powers leader election, and underpins safe deletes through preconditions. Once you see the pattern, a great deal of Kubernetes behaviour, from controller retries to informer relists, becomes easy to explain.

Key Takeaways

  • The stamp is a freshness check — resourceVersion says which stored edition you saw, and the server refuses writes made from an outdated edition.
  • It is opaque — treat it as text, copy it back and compare for equality only, and never rely on its numeric order or inner structure.
  • Conflicts are normal — a 409 means “someone changed it first.” Respond by re-reading and re-applying your intention with backoff, never by resending a stale copy.
  • Choose the right write style — updates carry the stamp, patches send small changes, and server-side apply tracks field ownership. Narrow writes mean fewer collisions.
  • Watches depend on it — list, then watch from the list’s stamp. When the server answers 410 Gone, list again.
  • Design for one writer — give each field or object a single owner, split hot objects, and use small Lease objects for frequent heartbeats and leader election.
  • Measure the rate, not the event — track 409 and 410 responses, retries and audit records. Steady low numbers are healthy, and climbing numbers point to a design problem.