In a GraphQL subscription, what value does the selection set execute against per event?
answer
- The event is a parent, not a payload
- Same algorithm a query uses
- The root field resolves twice
- Missing key, silent null
- Thin event means resolvers load state
basics
~20 sThe event itself. Each source event becomes the initial value for one ordinary execution of the selection set, so the subscription root field is resolved from that event by a normal field resolver, just as a query field is resolved from a root value.
solid answer
~50 sWhen an event arrives, the server runs the same selection-set execution a query uses, passing **the event as the initial (root) value**. That means the subscription's root field is not skipped: it is resolved a second time, now by an ordinary field resolver whose parent value is the event. Most servers fall back to reading a same-named property off that parent, so an event published as `{ "segmentTranslated": { ... } }` resolves, while an event published as the bare payload resolves to `null` and the subscriber receives a result whose payload key is present but empty — with no error, because a nullable field returning null is not an error. Because the event is only a root value, it can be a thin identifier and the resolvers below can load current state per subscriber, applying that subscriber's own selection set and authorization.
code
pseudocode · 5 linesfunction executeSubscriptionEvent(document, schema, variables, event):
subscriptionType = schema.subscriptionRootType
selectionSet = document.operation.selectionSet
data = executeSelectionSet(selectionSet, subscriptionType, initialValue = event, variables)
return { data: data, errors: collectedFieldErrors }go deeper
Remember that the payload the client sees is built by executing the document's selection set against the event, so the fields in a subscription result are the ones the client asked for, not whatever the server happened to publish.
Explain that the event becomes the initial value and that the root field is therefore resolved again by an ordinary resolver. Be able to trace a null payload back to an event whose shape does not match the field name.
Argue the thin-versus-fat event tradeoff with numbers: per-event execution cost, subscriber fan-out, authorization placement, and whether resolving current state can collapse two changes into one delivered value.
Own the coupling question. Deciding that events carry identifiers rather than payloads keeps publishers ignorant of the schema and pushes cost and authorization into resolvers; decide it deliberately and make it a house rule rather than a per-feed accident.
## The one sentence that explains most subscription bugs Each event is executed, not serialized. The server takes the source event, uses it as the **initial value** for a normal execution of the subscription operation's selection set, and yields the resulting `{ data, errors }` map. The algorithm for that execution is intentionally the same one a query uses, which is why the answer to "how is my subscription payload built?" is always "the same way a query's payload is built — from a root value, through resolvers". ```pseudocode function executeSubscriptionEvent(document, schema, variables, event): subscriptionType = schema.subscriptionRootType selectionSet = document.operation.selectionSet data = executeSelectionSet(selectionSet, subscriptionType, initialValue = event, variables) return { data: data, errors: collectedFieldErrors } ``` ## The root field is resolved twice, by two different things This is the detail interviews probe, and it is worth stating in exactly these terms. The subscription's root field has **two** jobs on a server: 1. At subscribe time it is resolved by the **event-stream resolver**, which returns the source event stream. 2. Per event it is resolved again, this time by an **ordinary field resolver** whose parent value is that event, producing the value that appears under the field's response key in the result. Step two is easy to forget because the event feels like it *is* the payload. It is not: it is the parent the payload is resolved from. If the server provides no explicit resolver for that root field, the widespread fallback reads a property with the field's name off the parent object. ## The half-empty result, and why it is silent On a translation-memory graph, the subscription is `segmentTranslated(projectId:)` and the publisher emits one record per translated segment. Publish this and everything works: ```json { "segmentTranslated": { "segmentId": "seg-88213", "matchScore": 0.94 } } ``` Publish the bare record instead — `{ "segmentId": "seg-88213", "matchScore": 0.94 }` — and the lookup for a property named `segmentTranslated` finds nothing, the field resolves to `null`, and every subscriber receives: ```json { "data": { "segmentTranslated": null } } ``` A response that arrives half-empty with **no errors entry at all**, because a nullable field resolving to null is a perfectly legal result, not a failure. Events flow at the right rate, the transport is healthy, dashboards are green, and every payload is empty. The fix is either to publish events keyed by the field name or to give the root field an explicit resolver that maps the event to the payload — the second is the more honest design, because it stops the wire shape of your internal events from dictating your schema's field names. ## Thin events versus fat events Because the event is only a root value, you get a real design choice. A **fat event** carries the whole payload. Execution below the root is then mostly reading properties off it, so per-event cost is low and predictable — attractive when you are holding a 340 ms p99 from publish to delivered result. The cost is coupling: every field a subscriber might select has to be in the event at publish time, and the publisher now has to know your schema. A **thin event** carries an identifier and enough context to filter — say `{ segmentId, projectId }`. Resolvers below the root load the current state. Now each subscriber's own selection set decides what is fetched, authorization runs in the resolvers per subscriber rather than at publish time, and a subscriber who selects three fields does not pay for thirty. The cost is that every event is a real fetch, so a burst of events is a burst of backend work, and two subscribers to the same event do that work twice unless per-request batching collapses it. Note also that a thin event resolves *current* state, so a rapid sequence of changes can deliver the same latest value twice rather than the two intermediate ones. ## Details worth having ready The per-event execution is a normal, non-serial selection-set execution — the serial rule belongs to mutation root fields, not to subscription events. Variables are the ones supplied when the subscription was created; there is no per-event variable set. And the response key rules are ordinary: an alias on the root field changes the key in every event's `data` map without changing which event stream was opened.
- Why does the half-empty result carry no errors entry?Because nothing failed. The root field is nullable, its resolver read a property that was not there and returned null, and a nullable field returning null is a legal value. Errors appear only when a resolver raises or a non-null position is nulled. That is exactly what makes this failure mode expensive to find: it is invisible to error-rate alerting and shows up only as empty payloads.
- What changes if the subscription's root field is declared non-null?The same missing-key null becomes an error rather than a silent hole. Nulling a non-null field raises a field error that propagates to the nearest nullable ancestor — here the root — so that event's result carries `data: null` plus an errors entry. You trade a silent empty payload for a loud one, which is usually the better bargain for a feed you have to operate.
- Two subscribers hold the same subscription with different selection sets. What does each receive?Each receives a result from its own execution of its own document against the same event. One may select three fields and another twenty; both executions run independently, each applying that subscriber's authorization in its resolvers. The shared thing is the event, not the result, which is why per-event cost scales with subscriber count and selection breadth rather than with event size.
The event is a forwarding address, not a parcel: the server still has to go there and collect exactly what this particular subscriber asked for.
saying these in an interview costs you the question
- Says the event is serialized straight into data
- Thinks the root field is skipped during per-event execution
- Expects an errors entry for a missing event key
- Believes events must carry every selectable field
- Thinks each event can supply new variable values
- Says subscription event fields execute serially like mutation roots