skip to content

Optimistic Check-and-Set

Checking that nothing moved since you read it - a version token compared on write, or a declared read set that aborts the group - then retrying: cheap when contention is rare, waste when it is not.

on this pageshow

questions

5

A write presented with the version token read alongside the value is refused - what happened, and what must the caller do next?

level: juniorimportance: must knowfreq 64%

answer

  1. something moved under you
  2. a refusal, not a silent overwrite
  3. the token is a check, not a permit
  4. re-read before each new attempt
  5. recompute from the value just read

basics

~20 s

Another caller changed that entry between the read and the write, so the store refused it instead of overwriting. The caller must re-read the entry, recompute the change from the new value, and write with the new token.

solid answer

~50 s

The refusal is the mechanism working. The caller read the value together with a `version token` - an opaque marker the store changes whenever that entry is written - and presented it back on the write; the store compared it, found the entry had moved, and rejected the write with no effect at all. Two things follow. First, the repair is a *fresh read*: re-read the entry, recompute the change from what is there now, take the token that came with that read, and write again. Re-reading only to collect a new token and then writing the value computed from the stale read defeats the whole mechanism. Second, the refusal is not proof another caller wrote: where entries can be evicted or reach a deadline, an entry that vanished and was created again carries a new token too.

code

pseudocode · 11 lines
pseudocode
attempts = 0
loop:
    attempts = attempts + 1
    value, token = read entry with its version token
    new_value = apply my change to value        // recomputed from THIS read
    accepted = write entry only if version token unchanged(new_value, token)
    if accepted:
        done
    if attempts >= max_attempts:
        give up and report contention on this entry
    pause briefly before the next attempt

go deeper

for a junior

Recall the three beats: the token comes back with the read, it goes out with the write, and a mismatch means the write did not happen. Then say what the caller does - read again, redo the change on the new value, write again.

for a middle

Explain why a fresh read is mandatory rather than polite: writing a value computed from the old read is accepted by the store and silently undoes the other caller's change, which is the exact failure the token exists to catch.

for a senior

Show that a refusal is information, not an error to swallow. Say what it does not prove - it does not name the other caller, and on a tier that evicts or expires entries it does not even prove a caller was involved.

for a principal

Frame the guarantee honestly: the check gives detection with no effect on conflict, and nothing more. Durability of an accepted write, and enforcement of anything spanning entries, are separate arrangements this mechanism does not make.

## What the token is Optimistic check-and-set is a bet: you assume nothing else will touch the entry between your read and your write, and you ask the store to verify the bet at the moment of the write. The verification is carried by a **version token** - an opaque marker the store returns alongside the value and changes whenever that entry is written. You present the token you read; the store compares it with the entry's current one; on a match the write is applied, and on a mismatch it is refused and nothing changes. Stores in this class do not all offer the same shape of this. Some expose a per-entry token with a conditional write. Others provide a **declared read set**: the caller names the entries it is about to depend on, queues an **operation group**, and the store abandons the group before applying any step if a declared entry changed. Some offer neither, and the only multi-step atomicity available is a **submitted program** or an **expiring claim**. The token's representation varies too - an opaque handle, a counter, a modification stamp - so treat it as opaque. Comparing two tokens for order, or doing arithmetic on one, is not portable. ## Why a refusal is the good outcome Compare the alternative. With no check, the second write simply lands: it overwrites whatever the first caller computed, the first caller's change is gone, and no error is raised anywhere. That is **last-writer-wins**, and its defining property is silence - nothing in the system reports the loss. A refusal is not a malfunction; it is the store telling you the one fact you could not otherwise learn. Be precise about what the token does *not* do. It does not make read-then-write atomic. The read and the write are still two separate operations with a gap in between, and any caller may use that gap. What the token buys is **detection at the write**, plus the guarantee that a detected conflict leaves no effect behind. | what the caller does | what the loser sees | what ends up in the entry | |---|---|---| | reads, then writes, no check | nothing at all | the later write's value; the earlier change is gone | | reads with a token, writes with it | an explicit refusal | the other caller's value, untouched by the refused write | | declares, reads, queues a group | the group abandoned | the other caller's value, untouched by the group | ## The retry that is actually correct 1. **Re-read the entry from the store, now** - not from a local variable, a request-scoped copy, or the value already in hand. 2. **Recompute the change from the value just read.** If the change was "add seven", add seven to the number that is there now, not to the one read a moment ago. 3. **Write with the token that came back with this read.** 4. **Repeat under a bound**, until the write is accepted or the attempts run out - and have an answer ready for the caller that exhausts them. Step 2 is where the common bug lives. A caller that re-reads only to collect a fresh token, then writes the value it computed from the stale read, has defeated the entire mechanism: the write is accepted, and the other caller's change is overwritten exactly as it would have been with no check at all. The token is not a permit to write. The fresh read is the repair. ## What a refusal does not tell you - **Not who.** You learn the entry moved, not who moved it or what they put there. - **Not that a caller was involved at all.** On a tier that evicts under memory pressure or honours an entry deadline, the entry can disappear and be created again, and the new entry carries a new token. - **Not that the next attempt will succeed.** Nothing reserves the entry between attempts; the caller is re-entering the same race, and a caller whose computation is slower is exposed for longer and loses more often. - **Nothing about durability.** A refusal is a clean no-op, but an accepted write is only as durable as the tier is. Where replication is asynchronous, accepted is not survived; some stores in this class acknowledge only once a replica holds the write, and some let the caller ask for that per call. ## When the bet stops paying The check is close to free while collisions are rare: one extra field on the read, one comparison at the write, nothing held and no caller blocked. It stops being free when many callers want the same entry, because then every attempt but one is discarded work. At that point the honest moves are to stop doing the read-modify-write over the network at all - a **server-side in-place update**, available only where the server understands the value - or to stop racing by holding an **expiring claim** so callers wait instead of colliding. Bounding the attempts and spacing them out keeps a losing caller from spinning, but it does not make the entry less contended.

  • What if the entry is simply gone when the caller re-reads it?
    Then the refusal was not another caller's write at all - the entry was evicted, reached its deadline, or was lost with the node holding it. The caller must decide whether absence is a legitimate starting state: create the entry with a conditional create if it should exist, or treat the work as no longer applicable. Assuming absence means "start from zero" is how a counter quietly loses history.
  • Does presenting the token make the read and the write one atomic operation?
    No. They remain two operations with a gap between them, and any other caller may write in that gap. The token adds a condition to the second operation: apply only if the entry still carries the token that was read. So interference is detected, not prevented, and a detected conflict produces no effect - which is what makes retrying safe.
  • Is a refused write cheaper than an accepted one?
    Only marginally. The caller still paid a read round trip, its own computation and a write round trip, and the server still spent time receiving the write and comparing the token. That is why refusals are the thing to measure: they cost close to a full attempt and produce nothing.

It is the shared document you edited from a copy you downloaded earlier. When you upload, the service checks whether the copy you started from is still the current one and refuses the upload if it is not. The fix is to pull what is there now and redo your edit on top of it - not to force your stale copy over the top, which is exactly what losing the check would do.

saying these in an interview costs you the question

  • Retries by writing the value computed from the stale read.
  • Thinks a refused write leaves the entry partly updated.
  • Calls the token a lock, or says it reserves the entry.
  • Believes a refusal always means another caller wrote.
  • Assumes every store in this class offers a version token.
  • Treats a refusal and a silent overwrite as the same outcome.
open as a page

Forty callers a second read one shared entry and rewrite it under a version token, and adding callers lowers the rate of accepted writes - why, and what would you change?

level: seniorimportance: must knowfreq 57%

basics

~20 s

Only one caller per generation of the entry wins; every other in-flight attempt holds a token that the accepted write invalidates, so its work is discarded and repeated. More callers, more collisions, fewer accepted writes.

open as a page

How does a declared read set that abandons an operation group differ from a version token presented on one write?

level: middleimportance: should knowfreq 48%

basics

~20 s

A version token guards one entry as one write is applied. A declared read set guards every entry the caller named, written or not, abandoning a queued operation group before any step applies. Same bet, wider scope.

open as a page

You bound optimistic retries at five attempts on one contended entry - what contract does that give callers, and what happens to the one that exhausts the bound?

level: principalimportance: should knowfreq 38%

basics

~20 s

The bound trades an open-ended attempt for bounded latency and an explicit failure, turning a correctness mechanism into a best-effort one. The exhausted caller needs a real answer: fail outward, enforce downstream, serialise, or accept last-writer-wins.

open as a page

A conditional write under a version token keeps being refused although no caller touched the field you changed - why, and what do you change?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

The token marks the whole entry, not the field. Any write to that entry moves it and refuses the next token holder, however unrelated the changes were. Fix the granularity, or move the edit to the server.

open as a page