skip to content

The Round Trip

The server's share of a call is microseconds and the network's share is not, so a loop of one-key calls is the commonest way an application makes this tier look slow.

on this pageshow

questions

4

A request makes 500 single-key reads, each answered in microseconds, yet takes 300 ms overall — where did the time go?

level: juniorimportance: must knowfreq 76%

answer

  1. two different clocks
  2. only one term scales
  3. one crossing per iteration
  4. trips times round-trip time

basics

~20 s

Almost all of it went into the network. Each read pays one full crossing, so 500 sequential reads pay 500 crossings. The store's microseconds are far too small to explain the delay; the number of crossings is the cost.

solid answer

~40 s

A call to this tier has two independent costs: the server's service time, which for a single-key read is microseconds, and the caller's wall time, which includes a full network crossing out and back. Only the second one is repeated by a loop, because the caller cannot issue call number two until reply number one has arrived. Five hundred reads at, say, 0.6 ms of round-trip time is roughly 300 ms of waiting, against about 2.5 ms actually spent inside the store. The remedy is fewer crossings, not a faster store: ask for fewer keys, ask for many keys in one operation where the store offers one, send a run of operations without waiting for each reply, or move the decision to where the data is.

code

pseudocode · 7 lines
pseudocode
// one crossing per key: 500 crossings
values = []
for key in keys:                 // 500 keys
    values.append(read(key))     // waits for the reply before the next iteration

// the same data, one crossing
values = readMany(keys)

go deeper

for a junior

Remember that a call to this tier is microseconds of work plus a network journey, and that a loop pays the journey once per key. If a request makes hundreds of calls, count them before blaming the store.

for a middle

Be able to do the multiplication out loud: trip count times round-trip time, with the service time dropped because it is too small to matter. Then say which placement your number assumes.

for a senior

Show that you know why nothing on the server side reports this, and name the pair of numbers that does: the caller's wall time for the path and the number of calls it made. Recognise the timeline shape on sight.

for a principal

The interesting question is why the path was allowed to make five hundred crossings at all. Argue for a stated trip allowance per request path, and for removing crossings rather than shortening them.

## Two numbers, not one A single read against an in-memory store costs the caller two things that scale for entirely different reasons. - **The server's service time** — what the store spends executing the operation once the request is in its hands. For a single-key read of a small value this is a hash lookup and a copy into an outbound buffer: microseconds, sometimes a fraction of one. - **The caller's wall time** — what the caller measures from issuing the call to holding the value: the service time, the network journey out and back, and the encoding and decoding at both ends. On any deployment where the store is not on the caller's own host, the second number is one to three orders of magnitude larger than the first, and everything here follows from that ratio: **you are not designing against the store's speed, you are designing against the number of times you cross the network.** ## Why a loop multiplies only the network term "Sequential" has a precise meaning on the wire: the caller cannot send call number two until reply number one has arrived — because the next key depends on what came back, or simply because the code is written as a loop. The calls cannot overlap, so the request's wall time is roughly > trip count × (one round-trip time + service time) Service time is negligible inside that bracket, so it reduces to **trip count × round-trip time**. Five hundred reads against a store answering in 5 microseconds spend about 2.5 ms inside the store, and the remaining 297 ms waiting for the network, five hundred separate times. The same loop is invisible at one placement and fatal at another, so redo the arithmetic at each. Orders of magnitude only — measure your own path: | Placement of the tier | One round trip | 500 sequential reads | The same 500 keys in one operation | |---|---|---|---| | Same host, over a local socket | tens of microseconds | tens of milliseconds | well under a millisecond | | Same zone, one network hop | a few hundred microseconds | a few hundred milliseconds | roughly a millisecond | | Different zone, same region | around a millisecond | half a second or worse | a few milliseconds | | Different region | tens of milliseconds | many seconds — a design error | tens of milliseconds | The right-hand column is the point. Collapsing the crossings changes the answer by two or three orders of magnitude; making the store itself twice as fast changes it by nothing anyone could measure. ## Why every server-side number stays green This is the failure mode's signature, and the reason it survives so long in production: 1. **The store's own accounting is honest and useless here.** It reports service time, which really is microseconds, and nothing in it counts how many times one request came back for more. 2. **The throughput ceiling is nowhere near.** Five hundred operations spread over one request is nothing against a tier answering hundreds of thousands per second, so utilisation looks idle. 3. **Per-call latency measured at the caller also looks fine.** Each call genuinely is fast. The cost lives in the count, and no per-call statistic — mean or high percentile — has a count in it. The cost becomes visible only where someone measures **the caller's wall time for the whole request path alongside the number of calls that path made.** A pair of numbers, never one. ## The shape it makes in a request trace Drawn on a timeline, the pattern is recognisable before you read a single label: a long row of very narrow calls, all about the same width, none overlapping another, together filling most of the request. Each attribute carries information. - **Uniform width** says the work per call is constant — no single call is doing anything unusual. - **Strict sequencing with no overlap** says nothing was sent before the previous reply landed. - **The count** is the cost, and it is the only thing on that row that is large. Two neighbouring shapes look nothing like it: one genuinely expensive operation is a single wide bar, and contention shows as varying widths separated by gaps. ## The directions out All of them do the same thing — remove crossings — and none makes the store faster: - **Ask for fewer keys.** The cheapest crossing is the one the path never needed. - **Ask for many keys in one operation**, where the store offers such an operation and the keys can be served together. - **Send a run of operations without waiting for each reply**, so the waiting happens once instead of once per call. - **Move the decision to where the data is**, so the caller asks one question rather than fetching the inputs to answer it itself. - **Move the tier closer**, shrinking every crossing rather than removing any. ## What varies between stores The arithmetic is universal; its inputs are not. Some stores in this class offer an operation taking many keys and some answer only single-key reads and writes, which decides whether the second direction exists at all. Some execute operations one at a time while others serve requests from a thread pool — that distinction changes who is delayed behind an expensive call, but not this arithmetic, because the term being multiplied is negligible under either model. And where the keyspace is split across nodes, one operation over many keys may not be servable by a single node, so the honest count is the number of nodes involved rather than one.

  • The service is moved onto a host in the same rack as the store, and the loop now takes 12 ms. Has the design problem been fixed?
    No — it has been hidden. The trip count is unchanged; each crossing simply became cheap. The cost returns in full the moment anything changes the distance: the tier moves to another zone, a managed deployment lands somewhere else, an extra hop appears in the path, or the keyspace is split so calls fan out to several nodes. The durable fix is a smaller trip count.
  • How would you tell this pattern apart from one genuinely expensive call?
    By shape and by where the time sits. This pattern is hundreds of narrow, equal-width, strictly sequential calls, each with a microsecond-scale service time at the server; one expensive call is a single wide bar whose server-measured service time is itself large. The first is fixed by removing crossings, the second by changing what the caller asks the server to do.

A courier sent across town for each item on a shopping list. The shop hands over an item in seconds; the journey takes twenty minutes each way. With fifty items, what you are waiting for is not the shop's speed — it is the number of journeys, and the only real fix is to send one courier with the whole list.

saying these in an interview costs you the question

  • Says the tier is in-memory, so the loop cannot be what is slow
  • Adds up the microseconds and expects them to explain 300 ms
  • Proposes a bigger or faster store instance for a trip-count problem
  • Treats green server-side service time as proof the tier is uninvolved
  • Blames bandwidth for a delay made of five hundred tiny crossings
open as a page

A page render needs 60 values from a tier in another zone within a 50 ms budget; how do you check it fits?

level: middleimportance: must knowfreq 64%

basics

~20 s

Measure one round trip from the real caller to the real tier at a high percentile, multiply by the crossings the path makes, and compare with the share of the 50 ms the tier gets. Sixty sequential crossings will not fit.

open as a page

Your tier moves from the same zone to a second region; what happens to a path making 12 sequential calls?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Each of the twelve crossings now pays a cross-region round trip, so a path that cost a few milliseconds costs hundreds. Distance is set by physics and topology, not configuration, so only fewer crossings or a closer tier help.

open as a page

How would you set and enforce a per-request trip budget for a shared in-memory tier across many teams?

level: principalimportance: should knowfreq 38%

basics

~20 s

State, per request path, how many crossings it may make and at what placement, derived from the user-facing deadline and a measured per-crossing cost. Then measure trips per path, not calls per second, and treat a removed crossing as better than a tuned one.

open as a page