skip to content

Atomic Counters

A number the server changes on your behalf, so two concurrent increments both land. Interviewers probe it because the first write, not the hot path, is where designs go wrong.

on this pageshow

questions

5

Why do in-memory stores offer an operation that adds to a stored number instead of leaving the arithmetic to the caller?

level: juniorimportance: must knowfreq 70%

answer

  1. who does the arithmetic
  2. two round trips become one
  3. the caller never holds the old number
  4. both changes land, neither overwrites

basics

~10 s

Server-side arithmetic means the caller never holds the old number: one round trip replaces a read-modify-write round trip, and two concurrent callers each contribute a change instead of one silently overwriting the other.

solid answer

~50 s

Held as an ordinary value, a counter costs a **read-modify-write round trip** on every change: whole-value read, add in the caller, whole-value write. That is two exchanges, and between them another caller can read the same number, so both write the same total and one change disappears with no error anywhere. An operation that adds to the stored number moves the arithmetic into the server, which applies it as one operation it runs to completion — the caller never holds the old number, so it has nothing stale to write back. Where the operation also hands back the resulting number, which is the common case, the caller learns the new total in the same round trip. Stores in this class differ enormously in what else they understand about a value, but arithmetic on a number is offered widely, including by stores that otherwise return only the bytes they were given.

go deeper

for a junior

Recall the shape: a counter changed by a read and then a write costs two round trips and can lose changes, while asking the server to add moves the arithmetic to where the number lives.

for a middle

Explain the window between the read and the write, and why two callers both writing the same total loses a change with no error. Name the round-trip saving as the second, smaller benefit.

for a senior

Show the boundary: one change is indivisible, a sequence of changes is not, and the arithmetic is unconditional. Say where the number cannot live server-side — inside a value the server treats as bytes.

for a principal

Set the platform rule: shared numbers are changed by the server, never by a caller that read them first, and a counter whose loss would be material does not live only on a tier that can drop it.

## The smallest interesting value An in-memory store holds values under keys, and a counter is the smallest value worth arguing about: one number under one key, usually changed far more often than it is read. Every store of this class can hold such a number. The question an interviewer is actually asking is **who performs the arithmetic**: the caller, after reading the number out, or the server, in place. Keep the premise of this tier alive while you answer. The number lives in memory, it can be evicted, it can vanish on a restart, and it is not the only copy of anything that matters. None of that changes who should do the arithmetic; it changes what the number is allowed to mean. ## The caller-side version With no help from the server, one change looks like this: 1. **Whole-value read** — the caller asks for the value under the key and gets the number back. 2. The caller parses it, adds its amount, and serializes the result. 3. **Whole-value write** — the caller writes the new number back over the old one. Two things are wrong with this, and only one of them is speed. - It is **two round trips**, not one. On a tier where the server answers in microseconds and the network costs hundreds of them, doubling the round trips roughly doubles the cost of the change. - Between step 1 and step 3 there is a **window** in which the number the caller is holding stops being the current number. Two callers — two threads, or two service instances behind a load balancer, or one request racing its own retry — both read 41, both compute 42, and both write 42. Two visits happened; the counter moved by one. Nothing failed, nothing logged, and the number is simply wrong from then on. The general shape of that race and the remedies available on a store with no transactions are a subject of their own. What belongs to the counter is the conclusion: **a shared number must not be changed by a read-then-write in the caller.** ## The server-side version A **server-side increment** sends the key and the amount, and the server reads the current number, applies the change and stores the result as **one operation it runs to completion** — no other caller observes it half applied, and no other caller's change can interleave inside it. Two callers incrementing at the same instant both land, in some order, and the total moves by two. The caller never holds the old number. That is the whole mechanism in one sentence: there is no stale value in the caller's hands, so there is nothing for a later write to overwrite. | | read-modify-write round trip | server-side increment | |---|---|---| | Round trips per change | two | one | | Who holds the old number | the caller, for the length of the window | nobody | | Two concurrent changes | one may be silently lost | both land | | What the caller learns | nothing; it must read again | usually the resulting total, in the same reply | ## What varies between stores This is a class of products, not one product, and a good answer says what differs: - **Whether the server can see inside a value at all.** A byte-opaque store returns exactly the bytes it was given and has no operation that changes part of a value — yet arithmetic on a number is still commonly offered, precisely because the read-modify-write cost makes counters unusable otherwise. "The server does not understand my values, so it cannot count for me" is a wrong inference. - **Whether the operation hands back the result**, and what widths and formats the arithmetic accepts. Common, not universal; check rather than assume. - **Why the server can promise the change is not interleaved.** Some designs run one operation at a time; others are multi-threaded and take a lock on the entry. The caller-visible property is the same, and that property is what you should name. - **Where the number lives.** If it is one field inside a larger value that the server treats as opaque bytes — part of the shared value format every writer agrees on — the server cannot apply arithmetic to it, and every change is a read-modify-write round trip again. A store that can address named fields inside a value can change that one field in place. ## What it does not buy you - It is **not a transaction**. One change is indivisible; two changes from the same caller are two operations, and another caller's work can land between them. - It is **not a bound**. The arithmetic is unconditional: the server does not know your maximum and will happily carry the number past it. - It is **not durability**. The tier is volatile, so the number can disappear between two changes, and a counter that must be recoverable needs a home that can be recovered. ## How to answer it out loud Name the two costs of the caller-side version — an extra round trip and a window in which the number goes stale — say that the server-side version removes both by never letting the caller hold the old number, and then volunteer the boundary: it makes one change safe, not a sequence of them. ## The interleaving a caller-side change allows. Both callers read the same number before either writes, so the second write carries no knowledge of the first ``` caller A: read_whole(views) -> 41 caller B: read_whole(views) -> 41 caller A: write_whole(views, 42) caller B: write_whole(views, 42) stored: 42 (two visits happened) ```

  • Does a server-side increment protect a sequence of operations, such as changing one number and then another?
    No. Each change is applied as one operation the server runs to completion, but two separate operations from the same caller are two units of work, and another caller's change can land between them. Grouping several changes so that nobody interleaves is a different mechanism, and what a given store offers for it varies. A single arithmetic operation promises only that this one change is not lost.
  • What is the number the operation hands back actually worth?
    It is the exact total at the instant that change was applied — the caller's own result, not an estimate, even under heavy concurrency. It saves a follow-up read and it is what makes "apply the change, then look at what came out" possible, for example noticing that a total has crossed a threshold. By the time the caller acts on it, other callers may have moved the number again.
  • Can the server do the arithmetic on a number held inside a larger serialized value?
    Only if it can address parts of that value. Where the server treats the value as opaque bytes, the number is invisible to it, so every change is a whole-value read, an addition in the caller and a whole-value write — with the lost-change window back. Where the server can address named fields, it can change that one field in place, and the counter keeps its properties without needing its own key.

Telling a bank to deposit twenty, rather than reading your balance, adding twenty at your desk, and writing the new balance back. The bank applies the change to whatever the balance is at that moment, so two people paying in at once both land — neither of them ever held the old figure.

saying these in an interview costs you the question

  • Says reading then writing is fine because each operation is fast
  • Reaches for a lock inside one process to protect a key many processes write
  • Calls the lost change a rare race not worth designing against
  • Believes a store that returns opaque bytes can never do arithmetic
  • Thinks the number handed back is approximate under concurrency
open as a page

A stored number must never exceed a cap, but the store's arithmetic is unconditional; how do you enforce the bound?

level: seniorimportance: must knowfreq 54%

basics

~20 s

Apply the change first, inspect the total the operation hands back, and undo it if it went over. The bound is therefore breached transiently, concurrent readers can see the overshoot, and a caller that dies before undoing leaks capacity.

open as a page

What happens when a server-side increment lands on a key that does not exist, or on a value that is not a number?

level: middleimportance: should knowfreq 46%

basics

~20 s

Stores differ: many create the entry at zero and apply the change, so no initializing write is needed; others refuse. A value that is not a number fails when the increment arrives, not when it was written.

open as a page

A counter must restart from zero each hour, and its first increment creates the entry; what must the caller get right?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A lifetime belongs to the entry, and the entry does not exist until the first change creates it, so the deadline has to be attached by the caller that created it. Miss that step and the counter never resets.

open as a page

How does holding a number written 20,000 times a second differ from holding one read 20,000 times a second?

level: middleimportance: nice to knowfreq 36%

basics

~20 s

A write-heavy number costs one round trip per change, so savings come from the caller accumulating a delta and applying it as one addition, paying staleness. A read-heavy number is cheap; the question becomes where it lives.

open as a page