API concurrency: treat resourceVersion as an opaque token
Handle update conflicts by rereading and recomputing intent without turning API version tokens into timestamps or cross-object clocks.
On this page
The question for today
What you’ll understand
- Trace an optimistic update conflict between two writers.
- Distinguish object UID, resourceVersion, and generation.
- Choose a conditional update strategy that preserves unrelated fields.
Two controllers can read the same object and independently decide to change it. Without a concurrency check, the second write could overwrite the first writer’s work. Kubernetes resourceVersion supports the documented optimistic concurrency protocol. In the v1.34 API contract used here, clients treat it as an opaque server-issued string: carry it back where required, but do not parse it as elapsed time, increment it yourself, or compare tokens from different objects to invent a global clock.
Identity and version answer different questions
A namespace/name locates an object, while its UID distinguishes that incarnation from a later object recreated with the same name. resourceVersion identifies a persisted version for API operations that use it. generation, where defined by a resource API, tracks specified changes according to that resource’s semantics. These fields are not interchangeable. An external resource associated only with a name may be mistaken for a new incarnation’s property after delete-and-recreate, even though the new object has a different UID.
For a normal version-checked object replacement, send the resourceVersion from the object you read. If another accepted update changed the current version first, the API server rejects the stale replacement with a conflict rather than silently losing that update. The retry should fetch current state and recompute the desired change. Replacing only the version token on an otherwise stale object would preserve the stale overwrite and defeat the purpose of the check. Bounded retries and appropriate backoff protect the control plane under contention.
Select field ownership and patch semantics deliberately
A read-modify-write operation should preserve fields owned by other actors. Different patch formats have different merge and precondition semantics; an unconditional patch that overwrites a shared field can still lose logical updates. Server-Side Apply uses field-management semantics and conflict handling, which is a different mechanism from replacing an entire object with resourceVersion. Do not turn force ownership into a default retry policy. Decide which actor is responsible for each field, especially when a human and a controller both edit the same object.
A status subresource can separate supported status writes from desired-spec writes, but the resource definition determines what is exposed and validated. Do not assume that every status write can ignore concurrency or that any resourceVersion change means the desired configuration changed. Cache reads may lag a recent successful write, so a tight reread-and-overwrite loop can conflict unnecessarily. Use the client’s documented consistency behavior and observe the exact condition you need, rather than guessing freshness from a token that merely looks like a decimal number.
Make conflict handling preserve intent
A conflict is often useful evidence that somebody else made progress. Distinguish it from authorization failure, invalid data, transport uncertainty, or deletion of the object. On NotFound, determine whether the desired external resource should be cleaned up or whether a new incarnation now occupies the same name; a UID check prevents confusing those cases. Preserve cancellation and maximum-attempt bounds, and expose persistent contention rather than silently spinning forever. A retry that changes the desired behavior is a semantic decision, not a mechanical networking trick.
The modular lab provides a warning against assuming numeric-looking labels encode unlimited progress. Reset, set m=8 and a=1, predict residues 0 through 7 and then zero again, and Step eight times. The lab’s labels are explicitly modular integers; Kubernetes resourceVersion is not modeled by them and is not claimed to wrap this way. The point is to read the actual contract before inferring order from labels. For real concurrency, replay the equality-token trace and verify the server’s conflict response.
Work it through
Two writers preserve both intended changes
Writers A and B read one object with the same opaque token, symbolically called α. Its labels are empty. A wants team=blue and B wants zone=west. Use version-checked replacements and preserve other labels.
A submits its replacement with token α and team=blue. It succeeds and returns a different opaque token, symbolically β. These symbols stand for server-provided values, not client-generated API strings.
B’s stale replacement with token α conflicts. It must not merely attach β to its original empty-label copy. B rereads the current object containing team=blue and recomputes adding zone=west.
B’s second attempt uses the freshly read token and preserves team=blue. If that succeeds, there were three update attempts and one conflict, an observed conflict fraction of 1/3. Both intended labels remain, without numeric token arithmetic.
Rereading and recomputing preserves concurrent intent; substituting a fresh token into stale content does not.
The living notebook
Explore the model
Interactive lab · modular
Labels need an ordering contract
Reset, set m=8 and a=1, predict a return to residue zero after eight additions, then Step eight times. This modular-label counterexample does not model resourceVersion or imply Kubernetes tokens wrap.
- Residue r0
- 0
- Orbit period · m/gcd(m,a)
- 12
- Distinct residues visited
- 1 of 12
Addition modulo m starts at zero: rₙ = na mod m. Playback advances two additions per second. The orbit has period m/gcd(m,a), including period one for a = 0. This is not a cryptographic security test or a primality algorithm. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forOnly the documented API contract determines which comparisons a version token supports.
Pause & reason
Check your understanding
Your turn
Try it yourself
A test makes ten normal version-checked update attempts, of which two conflict. Compute the conflict fraction. Can subtracting two objects’ resourceVersion strings tell you their age difference?
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Use response counts for the fraction and the opaque-token contract for the second question.
Compare with the solution
The observed fraction is 2/10=0.2, or 20 percent. Subtracting resourceVersion strings is not a valid age calculation under this API contract; they are opaque and not a cross-object physical clock. Measure elapsed time with a suitable timer and documented observation scope.
Keep the idea
Take it with you
- UID identifies an incarnation; version tokens and generation have different roles.
- A conflict retry must reread and recompute rather than bless stale content.
- Field ownership and patch preconditions determine what concurrent work is protected.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.