In GraphQL, why does one event delivered to 3,000 subscribers cost 3,000 executions?
answer
- One event, many operations
- The stream is per subscriber, not per event
- Selection set, variables, identity all differ
- Multiply, then look at backend reads
- Sharing only where nothing per-subscriber leaks
basics
~20 sBecause each subscriber has its own document, variables and context, the server executes the subscription's selection set once per subscriber and serializes a different payload for each. Only the source event is shared; everything downstream of it multiplies.
solid answer
~50 sA subscription operation produces a response stream, and each subscriber has a stream of their own. When a source event arrives, the server runs the specification's per-event execution for **every** subscriber whose stream is fed by that event: their selection set, their variables, their identity. Two subscribers watching the same flight may select different fields, alias them differently, or be allowed to see different ones, so there is no single payload to broadcast. Each execution also calls resolvers and gets its own per-request batch cache, so a batching pattern such as DataLoader dedupes within one subscriber's execution and not across subscribers. The cost of one event is therefore N times (execute + serialize + write), not one serialization plus N socket writes. Grouping subscribers with identical documents, variables and visibility to execute once is a real server optimisation, but it is convention, not specified behaviour.
code
graphql · 16 linessubscription SeatCanvas($flightId: ID!) {
seatStateChanged(flightId: $flightId) {
seatNumber
status
heldUntil
}
}
subscription OpsBoard($flightId: ID!) {
seatStateChanged(flightId: $flightId) {
status
cabin
availableCount
fareRules { refundable }
}
}go deeper
Be ready to say that each subscriber is a separate operation with its own document and variables, so one event becomes many executions rather than one shared message. Recall that only the source event is shared.
Explain the mechanics and do the multiplication out loud: subscribers times event rate equals executions per second, plus whatever backend calls each execution makes. Be able to name why the payloads legitimately differ — selections, aliases, variables, visibility.
Show that you find the real ceiling. Interviewers expect you to separate the shared source event from the per-subscriber work, to note that a per-request batch cache does not span executions, and to reach for grouping or an event-scoped cache with their correctness limits.
Own the trade the design makes: per-recipient shaping is what GraphQL sells and multiplied execution is what it costs. Be ready to argue when that price is worth paying versus pushing clients to refetch a shared, cacheable result.
## What is shared, and where sharing stops One source event — seat 14C on flight QX414 moved from AVAILABLE to HELD — is produced once. That is the only thing the subscribers have in common. From the moment the event reaches the execution step, everything is per subscriber. The GraphQL specification defines the mapping precisely enough to make this unavoidable: a subscription operation's result is a response stream, and each source event is turned into a response by executing *that operation's* selection set with the event as the root value, using *that operation's* variables. Subscribers are separate operations. Three thousand subscribers are three thousand response streams, so one event that is relevant to all of them is three thousand executions. ## Why the payloads genuinely differ It is tempting to assume the results are identical and could be computed once. Usually they are not: - **Different selection sets.** A seat-map canvas wants `seatNumber`, `status` and `heldUntil`; an operations dashboard on the same event wants `status`, `cabin` and the aggregated `availableCount`. Different fields, different resolver work. - **Different aliases and fragments.** Two clients selecting the same data can still produce different response keys, so the serialized bytes differ even when the values do not. - **Different variables.** A subscriber filtered to `cabin: ECONOMY` and one filtered to `cabin: BUSINESS` are fed by the same source stream but must not receive the same thing. - **Different visibility.** Corporate-fare inventory and the identity of the holder are visible to a ticketing agent and not to a passenger, so the same event yields two different documents' worth of truth. None of those can be reconciled after the fact. Broadcasting one payload works for a raw message bus precisely because the bus has no schema, no selection set and no per-recipient view; GraphQL has all three, and that is the trade being made. ## The arithmetic, with real shapes A fare sale puts **3,187** concurrent watchers on one flight's seat map. At peak the seat-state stream produces **2.4 events per second** for that flight. Every event is relevant to nearly every watcher, so: ``` 3,187 subscribers x 2.4 events/s = ~7,650 executions per second (one flight) 7,650 executions/s x 1.8 ms CPU = ~13.8 CPU-seconds per wall second ``` That is roughly fourteen cores fully committed to *one* flight's seat map, before any backend call. The same 2.4 events per second delivered as an opaque broadcast would be 2.4 serializations and 7,650 socket writes — cheap enough that nobody would think about it. Backend amplification is the second half. Each execution gets a fresh per-request batch cache, because that cache is scoped to one execution by design. If completing the payload needs a fare-rules lookup, the batching pattern collapses the lookups *within* one subscriber's execution and does nothing across the other 3,186. Without a cache scoped to the event rather than the execution, one seat change can produce thousands of identical reads of the same row. ## The levers, and which of them are conventions - **Group and reuse.** Servers may bucket subscribers whose document text, variables and effective visibility are identical, execute once per bucket, and write the same serialized bytes to every socket in it. This is the single biggest win when clients are homogeneous — one mobile app version behaves as one bucket. It is unsound the moment any part of the result depends on the individual subscriber, and it is an implementation optimisation with no standing in the specification. - **Cache across executions of one event.** Memoise the backend reads for the lifetime of a single event's fan-out, so the 3,187 executions share one fare-rules read instead of 3,187. - **Shrink the work per execution.** A subscription payload that carries a handful of scalars off the event itself costs a fraction of one whose fields reach into other services. - **Do not execute at all for subscribers who will not receive the result.** That is the filtering question, and it is the one worth reaching for first because it removes executions rather than making them cheaper. ## Specified versus conventional — say it explicitly Specified: one root field per subscription operation; a response stream per operation; one execution of the selection set per source event with the event as root value. Conventional: everything about fan-out across subscribers. The specification describes a single operation in isolation and says nothing at all about many subscribers sharing one event source, about grouping, about caching between executions, or about how the event reaches the server in the first place. Anyone who says "GraphQL broadcasts the payload" is describing a message bus, not GraphQL.
- When is it safe to execute once and reuse the payload for many subscribers?Only when nothing in the result can vary by subscriber: identical document text, identical variables, and an identical visibility outcome. Any field whose value or presence depends on identity, entitlement, tenant or locale breaks the bucket, and reusing bytes across that boundary leaks data. Servers that do this key the bucket on a visibility token precisely so an authorization difference forces a separate execution.
- Does a batching pattern such as DataLoader reduce the fan-out cost?Not across subscribers. Its batch cache is scoped to a single execution, so it collapses duplicate loads inside one subscriber's payload and nothing more. To stop 3,187 executions issuing the same read, you need a cache whose lifetime is the event's fan-out rather than one execution — which is a deliberate extra layer, not a default.
- Which is more expensive: 3,000 subscribers to one flight or 3,000 subscribers spread over 300 flights?The concentrated case, usually by a lot. Spread out, each event touches around ten subscribers; concentrated, each event touches three thousand, and those executions land on whichever nodes hold that flight's watchers at the same instant. Fan-out cost tracks subscribers-per-event, not total subscribers, which is why a hot entity is the number worth alerting on.
A broadcast is a printed newsletter: set the type once, run off three thousand copies. A subscription fan-out is three thousand personalised letters generated from one piece of news — the news is shared, the typesetting is not.
saying these in an interview costs you the question
- Says the server serializes one payload and broadcasts it
- Claims the specification defines fan-out across subscribers
- Thinks DataLoader dedupes work between subscribers
- Sizes capacity from event rate alone, ignoring subscriber count
- Reuses one execution's result across different identities
- Assumes identical subscriptions always produce identical bytes