skip to content

On a store that hands values back as opaque bytes, what does one entry grown to hundreds of megabytes cost to read, write and remove?

level: middleimportance: must knowfreq 60%

answer

  1. size, not operation count
  2. opaque bytes means no partial access
  3. a change is a full rewrite
  4. removal is work, and someone pays it

basics

~20 s

Every operation on that entry is proportional to its whole size: a read ships all of it, any change rewrites all of it, and removing it is real work that someone has to pay for.

solid answer

~50 s

When the server treats a value as opaque bytes it cannot touch part of one, so the unit of work is always the whole entry. A read assembles and ships hundreds of megabytes, and the calling process buffers all of it before your code sees anything. Changing one field is a full read-modify-write of the same hundreds of megabytes, and the store holds both copies while the write is in flight, so peak memory for that entry is roughly double. The surprise is removal: freeing a large value is work proportional to its size. Some stores do that on the calling path, which makes the removal about as expensive as the write; others answer immediately and reclaim on a background thread, which moves the cost off the caller without making it disappear. Size is what costs, not the number of operations.

go deeper

for a junior

Recall that this kind of store charges by how much data an operation moves, not by how many operations you issue. One very large entry is expensive every single time anything touches it.

for a middle

Explain why a value the server treats as opaque bytes has no partial read and no partial update, so every change is a full read-modify-write, and why the store holds two copies of the entry while that write is in flight.

for a senior

Show that you have thought about removal. Freeing a large value is proportional work; some stores pay it on the calling path and others defer reclamation to a background thread, and the ceiling only drops when that reclamation actually finishes.

for a principal

The tradeoff is where the bound lives. Where a store enforces a per-entry size ceiling it sits far above the size at which an entry is already an operational problem, so the limit worth having is the one your latency budget implies and your write path enforces.

## The entry is the unit of work An **entry** is one key plus the value it addresses. Whether an oversized entry is an annoyance or an incident turns on one property of the store, and this scenario fixes it: the server treats the value as **opaque bytes**. It has no idea whether those bytes are a list, a document or a compressed archive, so there is no operation that touches part of one. Every operation moves the whole thing. That premise matters because a large part of this class of store works exactly this way, and another large part does not. Where the server understands a value as structure it can read or change in part, a caller can touch one member without moving the rest, and the arithmetic below changes for reads and writes. It does not change for removal, and it does not change what the entry costs against the memory ceiling. An answer that does not say which kind of store it is describing is describing one product rather than the model. ## Reading Two costs scale with the value's size, and neither of them is the lookup. Finding the key is cheap and stays cheap; assembling a reply that carries hundreds of megabytes is not, and neither is writing that reply out. The caller pays a third cost that is easy to forget: the client library has to hold the whole value in your process before your code sees any of it. That is how one oversized entry becomes a memory problem inside a service that never stored anything large itself. ## Writing With opaque bytes there is no partial update, so a logical change to one field is three steps: 1. read the whole value into your process, 2. change it there, 3. write the whole value back. The size is paid twice per change. While the write is in flight the store holds both copies, because the new value has to exist before the old one can be released, so peak memory for that entry during its own update is roughly double its size plus whatever the allocator cannot immediately reuse. An entry that is a large fraction of the memory ceiling can therefore fail to accept its own update. ## Removing This is the cost candidates miss, and the one that produces real incidents. Freeing a large value is not a pointer flip: the memory genuinely has to be given back, and how much work that is depends on how much there is and how it is laid out internally. Stores differ in who pays for it. | | freed on the calling path | reclaimed in the background | |---|---|---| | what the caller observes | a removal roughly as slow as writing the same value | a removal that returns straight away | | when the memory returns | before the reply | some time after the reply | | what is different about the work | nothing | only who waits for it | The second column is **deferred reclamation**: the removal is acknowledged immediately and the memory comes back on a background thread. Several stores in this class offer it, sometimes as the default and sometimes as a separate way of asking for the removal. It is worth knowing because it turns one operation's cost from proportional-to-size into roughly constant. It does not reduce the total work, it does not shrink the entry, and it does nothing for the read that still ships the whole value. ## Why the size is the whole story The per-operation numbers a store is known for are quoted for ordinary entries. Nothing in an unschematised keyspace objects while one entry grows: there is no schema, no declared type, and usually no complaint at all until the store's own **per-entry size ceiling** is reached. Where a store enforces such a ceiling it sits far above the size at which the entry is already hurting you, and the numbers differ by orders of magnitude between stores, so the bound worth designing against is the one your latency and memory budget imply rather than the one the store happens to enforce. Three habits prevent the whole class of problem: - **Split the value across several keys** so no single operation is large, deriving the extra key segment from something deterministic and letting readers compute which entries they need. - **Cap the collection at write time** so it cannot grow past a size you chose, instead of correcting it later. - **Decide up front what one key is allowed to hold**, in bytes and in members, before the shape ships. Whether the rest of the tier stalls while any of this happens depends on the store's execution model, which is a separate subject: a store that runs one operation to completion before the next makes the delay total, and worker threads narrow it rather than remove it. Take that as a premise. What belongs to the entry itself is simpler and harder to escape — the cost is set by its size, and nothing about your call rate changes it.

  • Does the picture change if the server understands the value as a collection rather than as opaque bytes?
    For reads and writes, yes: a partial read and a partial write exist, so a caller can touch one member without moving the whole entry, and a bound can be enforced by the server on the write path. For removal and for the memory ceiling, no. The entry still has to be freed in full and still occupies its whole size. The remedy changes; the magnitude does not.
  • Why does peak memory during a write of a large entry exceed the entry's own size?
    The new copy has to exist before the old one can be released, so for the duration of the write the store holds both. With opaque bytes that means roughly twice the entry's size, plus whatever the allocator cannot immediately reuse. A design in which one entry is a large fraction of the ceiling can therefore fail to accept its own update.
  • If removal is deferred to a background thread, is the problem solved?
    Only for the caller issuing the removal. The work still happens, the memory comes back later rather than at once, and every other cost of the entry is untouched: the full-size read, the full-size rewrite, and its share of the memory ceiling. Deferred reclamation makes one operation cheap; it does not make the entry a normal size.

saying these in an interview costs you the question

  • Removing an entry is instant because it just drops a reference
  • Reading a large value is fine, the lookup is still constant time
  • Changing one field of the value is a small write
  • Only the server holds the large value, the client is unaffected
  • A background thread reclaiming the memory means the ceiling drops immediately