Learn
Lesson · Partitioning and Load Balancing · Crossing shards

Scatter-Gather Cost

A query whose predicate is not the partition key cannot be routed, so it goes to all N shards and waits for the last answer. Both halves of that sentence cost something: the fleet's request rate is multiplied by N, and the query's latency is the maximum of N shard latencies rather than one of them. The partition count is the same dial for both, and for how many consumers can read in parallel.

shard requests = N × query rate 24 × 400/s = 9 600/s a routed query would leave 400/s amplification = N 24× query latency = max of N shard latencies one shard's p99 90 ms → the query's p99 250 ms at N = 24 consumer parallelism ≤ N the same dial as the fan-out
One query, N requests, and the slowest of N waits

Core ideas

What this lesson establishes

Two costs from one fan-out, plus a third consequence of the partition count that is easy to forget because it lives on a different team's diagram.

1

Sharding divides the work per query and multiplies the requests

Both are true and only one of them is usually said. Each shard does 1/N of the scan, which is why the query is fast at all; and the fleet receives N requests where it used to receive one. At 24 shards and 400 queries a second that is 9 600 shard requests a second against the 400 a routed query would leave — and it is the second number that sizes connection pools, thread pools and every per-request cost in the system.

2

The query waits for the slowest shard

This is the maximum of “Stragglers” in a different costume. A shard whose own p99 is 90 ms gives a 24-way query a p99 of 250 ms, because the query is fast only if all 24 were fast. The lab computes the query's distribution exactly from the per-shard one and also samples it from a seeded run, and the two agree at 250 ms.

3

The partition count is one dial with three effects

Raising N reduces per-shard work, raises the fleet's request rate, worsens the latency tail, and raises the ceiling on how many consumers can read the data in parallel — that ceiling is N itself, because a partition is the unit of consumption. Choosing N for any one of those in isolation is how a system ends up with a tail nobody chose.

The material

What an unrouted query costs

Why some queries cannot be routed, what the fan-out does to the request rate and to the tail, and what else the partition count is quietly deciding.

Definition · Scatter-gather

A scatter-gather query is one whose predicate does not determine the shard, so the router sends it to all N shards (scatter) and combines the partial results (gather). A routed query names the partition key and goes to one shard. The request amplification of a scatter-gather is N: shard requests per client query.

Which queries are unroutable is decided by the partitioning function and nothing else. Partition by user and “everything for user 91” is routed while “everything created yesterday” is a scatter; partition by day and the two swap. Every partitioning choice makes one family of queries cheap and the rest of them scatters, which is why the choice belongs with the access patterns rather than with the data model.

N = 24 shards, 400 client queries a second shard requests = 24 × 400 = 9 600 /s amplification = 24× routed instead = 400 /s per-shard latency, in ms with weights out of 100 10:80 25:15 90:4 250:1 one shard mean 17.85 ms p50 10 p90 25 p99 90 ms the query N = 24 p99 250 ms exact 250 ms, and 250 ms in a seeded run of 400 queries

The amplification is the figure that surprises people, because the sentence “we sharded, so each node does less work” is true and is not the whole of it. Each node does less work per query; the node receives N times as many queries, so a fleet serving 400 client queries a second is handling 9 600 requests a second. Everything that is priced per request — a connection, a lock acquisition, a log line, a tracing span — is multiplied by 24.

The tail, again

the same per-shard distribution, against the shard count N shard requests /s query mean (ms) p50 p90 p99 1 400 17.85 10 25 90 2 800 24.92 10 25 250 4 1 600 37.22 25 90 250 8 3 200 56.72 25 90 250 16 6 400 84.74 90 250 250 24 9 600 105.24 90 250 250 32 12 800 121.40 90 250 250 64 25 600 163.47 90 250 250

At two shards the query's p99 is already 250 ms — the worst outcome in the per-shard distribution — because 0.99² = 0.9801, which is below the 0.99 a p99 needs. By sixteen shards the p90 has reached 250 ms too. The per-shard distribution never changed; the query is simply asking more shards to all be fast at once, and “Tail Amplification under Fan-out” is the same calculation stated in general.

Example · The third effect: consumer parallelism

A partition is also the unit of parallel consumption. If a downstream reader wants to process this dataset with more than N workers, it cannot: worker N + 1 has no partition to read, and at N = 24 the ceiling is 24 regardless of how much hardware is available. So the partition count that was chosen for storage and load has also set a limit on a pipeline nobody in that conversation was thinking about, and raising it later means the rehash of “Rehashing When N Changes”.

What to do instead

The direct fix is to make the query routable, by partitioning on the attribute it filters on — which usually means another copy of the data partitioned differently, and that is exactly the global secondary index of the next lesson. Failing that, the levers are the ones from “Stragglers”: narrow the per-shard tail, accept a partial result, or hedge. What does not help is raising N, which makes both costs worse.

It is worth writing the two numbers down together whenever a scatter-gather is proposed, because they trade against each other in a way that a single figure hides. 9 600 requests a second is a capacity question and 250 ms is a latency question; a fleet can be sized for the first and still fail the second, and a tail fix that adds hedged requests makes the first worse.

Interactive

Scatter-gather cost

N requests per query, and a tail that is the maximum of N

Scatter-gather costs N requests and waits for the worstsharding divides the work of a query and multiplies the number of requests
shard requeststhe query's tailone shard

Method

Pricing an unrouted query

Two numbers, always both. A scatter-gather that has been costed only for capacity or only for latency has been costed for neither.

1

Establish that the query really cannot be routed

A query is unroutable when its predicate does not determine the partition key. If it does determine it and the router is not using it, that is a routing bug and none of this arithmetic applies.

2

Multiply the query rate by N for the fleet's request rate

N × rate is what the shards receive, and it is the number that sizes connection pools, per-request overheads and anything billed per call. Write the routed rate beside it so the multiple is visible.

3

Compute the query's tail from the per-shard distribution

P(query ≤ t) = P(shard ≤ t)ᴺ. Take the p99 and compare it with one shard's p99. At the lab's numbers those are 250 ms and 90 ms, and the gap is the whole cost of the fan-out on the latency side.

4

Check what else N has decided

Consumer parallelism is capped at N, and the rehash cost of changing N later is N/(N+1) of the dataset under mod assignment. Both belong in the same decision, because both are hard to revisit once the data is placed.

Amplification and the tail

Score 0 / 0

400 queries a second against 24 shards

The request rate first, because it is a multiplication; then the tail, because it is not.

N = 24, client query rate = 400 /s requests shard requests = 24 × 400 = 9 600 /s routed would be 400 /s amplification 24× per-shard latency, ms with weights out of 100 10:80 25:15 90:4 250:1 mean = 0.80(10) + 0.15(25) + 0.04(90) + 0.01(250) = 8 + 3.75 + 3.6 + 2.5 = 17.85 ms P(shard ≤ 25) = 0.95 P(shard ≤ 90) = 0.99 so p99 = 90 ms the query P(query ≤ 90) = 0.99²⁴ = 0.7857 below 0.99, so p99 is above 90 P(query ≤ 250) = 1 so the query's p99 = 250 ms seeded run of 400 queries also reports 250 ms and where the p99 first reached 250 ms N = 2: 0.99² = 0.9801 < 0.99 already there

Two shards. The p99 of a fan-out query reaches the worst per-shard outcome at N = 2 with this distribution, which is worth holding on to: the tail cost of scatter-gather is not something that arrives at large fleet sizes, it arrives immediately.

For a faded rehearsal, keep 24 shards and narrow the per-shard tail to 10:80, 25:16, 90:4 — the 250 ms outcome removed entirely. The supplied first move is the new per-shard p99, which is still 90 ms. Compute the query's p99 at N = 24, say how much the fleet's request rate changed, and then state which of the two costs of scatter-gather that change addressed and which it left exactly where it was.

Common mistakes

1
Costing the fan-out for capacity and not for latency, or the reverse9 600 /s and 250 ms are both consequences of the same N and neither implies the other. A fleet sized comfortably for the request rate can still miss its latency target by a factor of three, and a tail fix built from hedged requests makes the request rate worse.
2
Expecting the tail cost to arrive only at large fleet sizesWith the lab's per-shard distribution, the query's p99 is already at the worst per-shard outcome at two shards, because 0.99² is below 0.99. Fan-out tail amplification is not a big-cluster problem; it starts at the second shard.
3
Raising the shard count to make a scatter-gather fasterIt raises the request rate proportionally and pushes the tail further out, and each shard's smaller slice rarely compensates. The fixes are to make the query routable, to narrow the per-shard tail, or to stop waiting for every shard.

Check your understanding

✓
Finish when a proposed scatter-gather makes you write down two numbers before anything else.

You should be able to compute the amplified request rate and the query's tail from a per-shard distribution, say at what shard count the tail reached the worst per-shard outcome, and name what else the partition count has decided.

Note: The standing answer to an unroutable query is to keep a second copy of the data partitioned by the attribute the query filters on. That is a global secondary index, and it is not free either: it turns a scatter on every read into an extra write on every write. “Local vs Global Secondary Indexes” prices both and finds the ratio at which they change places.

Kept in this browser only, for you. Nothing is sent anywhere and nothing checks it. Carry it to another device.

Feedback and recommendations

Something to change about this lesson? Write it down, tick it off when it is handled, and export the list when you want to act on it. Kept in this browser only: nothing is sent anywhere, and nobody is notified.

No recommendations yet for this lesson.