In GraphQL, what does a subscription's root field resolve to, unlike a query field?
answer
- Two phases, not one
- The root field returns a pipe
- Server events first, results second
- Opened once, executed per event
basics
~20 sA subscription's root field resolves to a source event stream rather than a value. Subscribing runs that one field once to open the stream; every event on it then produces its own separate execution result for the client.
solid answer
~40 sExecuting a subscription is two phases, not one. The first phase resolves the operation's single root field through a dedicated **event-stream** resolver: it coerces that field's arguments and returns a *source event stream* — an asynchronous sequence of ordinary server-side values that are not GraphQL results and never travel to the client verbatim. The second phase maps that source stream into a *response stream*: for each source event the server executes the operation's selection set and yields one `{ data, errors }` map. So a query's execution returns exactly one result, while a subscription's returns a long-lived stream of results. The event-stream resolver runs once per subscription, at subscribe time; the per-event work is ordinary selection-set execution. If opening the stream fails, the client gets a single error result and no subscription exists.
code
graphql · 7 linessubscription SegmentFeed($project: ID!) {
segmentTranslated(projectId: $project) {
segmentId
targetText
matchScore
}
}go deeper
Be ready to say plainly that a subscription's root field yields a stream of events instead of a value, and that the client receives one normal-looking result per event rather than one result overall.
Explain the two phases by name and in order: open the source event stream once at subscribe time, then execute the selection set once per event. Be able to say where argument coercion happens and why it happens only once.
Show that per-event work is a full execution, not a serialization, and reason about what that costs under a burst. Be ready to separate a subscribe-time failure from a per-event failure when diagnosing a feed that never started versus one that started badly.
Own the consequence for system shape: because the event payload is private and the client-visible data is produced per subscriber, you can choose between fat events and thin identifiers, and that choice sets your fan-out cost, your authorization model and your upstream coupling.
## Why subscriptions need a different execution algorithm A query or a mutation is a request-response exchange: one document in, one `{ "data": ..., "errors": ... }` map out. A subscription is long-lived: one document in, many such maps out over time. The specification does not get there by letting a resolver "return many values". It splits execution into **two phases with two different kinds of stream**, and almost every subscription misconception comes from collapsing them. ## Phase one: create the source event stream The first phase takes the schema's subscription root operation type and the operation's top-level selection set, collects the fields on it, and works with the single entry it finds. It reads that entry's **field name** — an alias changes the response key in the results but not which field is looked up — coerces the field's arguments from the document's literals and the request's variables, and invokes the server's event-stream resolver for that field. What comes back is a **source event stream**: an asynchronous sequence of values that are entirely the server's business. They are not GraphQL results, they are not validated against the schema, and they are not sent to the client as they are. On a translation-memory graph, a subscription written as ```graphql subscription SegmentFeed($project: ID!) { segmentTranslated(projectId: $project) { segmentId targetText matchScore } } ``` might have an event-stream resolver that registers a consumer on an event backbone filtered to project `tm-4417` and yields one small record each time a segment is translated. Two consequences follow immediately. First, this phase runs **once per subscription**, at subscribe time — not once per event. Second, argument coercion happens once too, so a filter argument is evaluated when the stream is opened and cannot change afterwards; a client that wants a different filter opens a different subscription. ## Phase two: map source events to response events The second phase wraps the source stream in a **response stream**. For each event that arrives on the source stream, the server executes the operation's selection set and yields the resulting `{ data, errors }` map as one event of the response stream. When the source stream completes, the response stream completes. That per-event execution is deliberately the same algorithm a query uses — field collection, resolvers, value completion, error collection — which is why a subscription's payload is shaped by the client's selection set exactly like a query's. In pseudocode the whole subscribe path is short: ```pseudocode function subscribe(document, schema, variables, rootValue): sourceStream = createSourceEventStream(document, schema, variables, rootValue) if creating the stream raised an error: return { errors: [error] } # no subscription exists return mapSourceToResponseEvent(sourceStream, document, schema, variables) ``` ## What the split buys you **Two distinct failure surfaces.** Failing to open the stream is a subscribe-time failure: there is no subscription, and the caller gets a single result carrying errors. Failing inside one event's execution is an ordinary execution failure that affects that one result. **Privacy of the event payload.** Because the source event is a private server value and only the second phase produces client-visible data, a server can publish a tiny event — an identifier and a project key — and let resolvers fill in the rest per subscriber, applying that subscriber's authorization and their own selection set. **A real cost model.** Every event costs a full selection-set execution, not a serialization. A feed that emits a burst of events is running that many executions, and a per-event latency budget — say a 340 ms p99 from event to delivered result — has to cover resolver work, not just transport. ## Reading the algorithm back off a real trace A useful way to fix the two phases in mind is to describe what a trace of one subscription looks like. There is a single short span at the beginning: argument coercion and the event-stream resolver, which registered a consumer and returned. Then, for the next hour, there is one span per event, each containing the whole tree of ordinary field resolvers the selection set needs. The subscribe span never repeats. If a trace shows the event-stream resolver running again, either the subscription was torn down and recreated, or the server is re-opening a stream it should have held. This also explains a question candidates often stumble on: what happens when a hundred subscribers open the same subscription with the same arguments? At the level of the algorithm, nothing is shared — each subscription has its own source event stream and its own per-event executions. Any sharing of an upstream consumer between them is a server optimisation living underneath the event-stream resolver, invisible to the algorithm, and it brings its own questions about filtering and lifetime that the specification does not answer. ## What the specification deliberately leaves out Where the source stream comes from, how many subscribers may share one upstream stream, how events are framed on the wire, and what happens across a reconnect are all outside the execution algorithm. The algorithm's contract is narrow and worth stating in an interview in exactly these terms: one root field yields a stream of server events; each event yields one execution result; cancelling the response stream ends the subscription.
- How many times does the server invoke the event-stream resolver for one subscription that runs for an hour?Once, at subscribe time. That call coerces the root field's arguments and returns the source event stream; everything afterwards is the second phase executing the selection set per event. A server that reopened the stream per event would re-register its upstream consumer thousands of times and would re-evaluate arguments that the client has no way to change.
- Can two events on one subscription come back with different response keys?No. Every event executes the same selection set from the same document, so the set of response keys is fixed for the life of the subscription. What varies is the values: one event may carry a populated object, another may carry nulls and an errors entry for the same keys. Clients can therefore rely on the shape and must still tolerate nulls.
- Does aliasing the subscription's root field change which event stream is opened?No. The first phase looks the field up by its name in the subscription root type, so an alias has no effect on which event-stream resolver runs or on the arguments coerced. The alias only changes the response key under which each event's result carries that field's value.
The event-stream resolver opens a tap; the selection set is the filter every drop passes through on its way to the glass. Opening the tap happens once, filtering happens per drop.
saying these in an interview costs you the question
- Says the root subscription field returns the payload directly
- Thinks the event-stream resolver runs once per event
- Believes a subscription execution returns one result like a query
- Confuses the source event stream with the client connection
- Assumes raw event payloads are sent to the client unchanged
- Thinks arguments are re-coerced on every event