Subscriptions
|
This section documents the current GraphQL specification (October 2021), plus the working draft for
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as GraphQL tooling iterates quickly. |
Subscriptions are GraphQL’s third operation type, alongside queries and mutations, and the only one that is long-lived and event-driven rather than a single request/response round trip. This page covers the subscription execution model, the transports used to carry subscription events, and when a subscription is the right tool instead of polling.
Queries and mutations vs. subscriptions
A query or a mutation is a single request that resolves once and returns a single response: the client sends the operation, the server executes it against the schema, and the connection (or HTTP request) is done. A subscription instead opens a standing connection over which the server pushes a stream of responses, one per event, for as long as the client stays subscribed. The client does not ask "what is the status of this order right now" repeatedly — it asks once to be told whenever the status changes, and keeps receiving payloads until it unsubscribes or the connection closes.
This makes subscriptions the GraphQL analogue of a publish/subscribe feed layered on top of the same type system, selection sets, and validation rules used for queries and fields: a subscription’s selection set is validated and shaped exactly like a query’s, but it is executed once per event instead of once per request. The official introduction to the operation type, its execution model, and example client code is graphql.org/learn/subscriptions.
Exactly one root field per subscription
The GraphQL specification restricts a subscription operation to a single root-level field. A query or mutation can select several top-level fields in one operation; a subscription cannot:
# Valid: one root field.
subscription OnOrderStatusChanged($orderId: ID!) {
orderStatusChanged(orderId: $orderId) {
id
status
updatedAt
}
}
# Invalid: two root fields on a subscription -- rejected at validation time.
subscription Invalid {
orderStatusChanged(orderId: "123") { status }
paymentReceived(orderId: "123") { amount }
}
The restriction exists because each event on the underlying source stream produces exactly one execution of the subscription’s selection set; a second root field would leave no single, well-defined event to trigger against. If a client genuinely needs to react to several kinds of event, it opens several subscription operations (or a schema author models one root field whose payload is a union/interface covering the several event shapes).
The event-stream execution model
Conceptually, a GraphQL server executes a subscription in two phases:
-
CreateSourceEventStream— the subscription’s root field resolver (often called the subscribe function, distinct from the field’s usualresolvefunction) is invoked once, and returns a source stream: an internal, transport-agnostic sequence of raw events for this subscription (an in-process pub/sub topic, a Kafka/Redis stream, a database change feed, and so on). -
ExecuteSubscriptionEvent— every time a new event arrives on that source stream, the server runs the normal GraphQL execution algorithm against it (resolving the selection set, applying the schema’s types) and emits one response payload for that event.
So a single subscription operation produces a source stream of raw events feeding a response stream of shaped GraphQL payloads — one execution per event, each independently validated and typed the same way a query response would be. This two-phase model is exactly what the GraphQL specification (referenced in the disclaimer above) formalizes; the practical mechanics of wiring an actual source stream to a message broker or change-data-capture feed on the JVM are covered in Spring Boot: Transports, Security & Testing.
Transport landscape
The GraphQL specification defines the language and execution semantics for subscriptions but deliberately says nothing about how events reach the client — that is a transport concern, layered on top the same way serving GraphQL over HTTP layers queries and mutations onto plain HTTP. Four transports cover the vast majority of real deployments:
| Transport | Notes |
|---|---|
|
The modern, actively maintained WebSocket sub-protocol (package name |
|
The original Apollo WebSocket sub-protocol (protocol id |
Server-Sent Events (SSE) |
A one-way, HTTP-native event stream ( |
Multipart HTTP ( |
Subscription responses streamed as successive parts of a single chunked HTTP response, one part per event.
Avoids opening a WebSocket at all, reuses ordinary HTTP semantics (status codes, standard proxying), and is
the basis of the incremental-delivery work ( |
A WebSocket-based transport (graphql-ws, or the legacy subscriptions-transport-ws) is still the default
choice when the client needs to send further messages after subscribing (unsubscribe, or multiplex several
subscriptions over one socket); SSE and multipart HTTP suit environments that would rather avoid a WebSocket
upgrade entirely — infrastructure that does not proxy WebSockets well, or a client that only ever needs the
server-to-client half of the conversation. See
Spring Boot: Transports, Security &
Testing for how a JVM server actually exposes these.
The graphql-ws protocol handshake
graphql-ws layers a small message protocol on top of the WebSocket connection. The client first sends
connection_init (optionally carrying connection parameters such as an auth token) and waits for the
server’s connection_ack before issuing any operation; only then does it send subscribe for each
subscription, receiving a next message per event until the server (or client) sends complete:
Each message carries the subscribing operation’s id, so several subscriptions can be multiplexed over the
same WebSocket connection, each with its own independent stream of next messages and its own complete.
The client can also send complete itself to unsubscribe early without closing the socket. The legacy
subscriptions-transport-ws protocol follows the same broad shape (an init/ack handshake, then per-operation
data messages) but uses different message type names (connection_init/connection_ack/start/data/
stop) and is not interchangeable with graphql-ws on the wire.
Subscriptions vs. polling
Polling means the client re-runs a plain query on a fixed interval and compares the result to what it already has. It is simple, needs no persistent connection, and works with infrastructure that has no special handling for WebSockets or long-lived HTTP responses. A subscription instead keeps one connection open and lets the server push a payload exactly when something changes.
| Concern | Polling a query | Subscription |
|---|---|---|
Latency |
Bounded by the poll interval — a change is visible only on the next poll |
Near-immediate — the server pushes as soon as the event occurs |
Server load |
Every client re-executes the full query on every tick, whether or not anything changed |
Work happens only when an event actually occurs; no wasted re-execution on unchanged data |
Connection model |
Stateless, ordinary request/response — trivial to load-balance and cache |
Long-lived connection per subscribed client — more server-side state, harder to scale naively |
Best fit |
Infrequent changes, tolerable delay, or when the client only occasionally cares about freshness |
Frequent or unpredictable changes where the client needs to react as soon as they happen (status changes, live chat, live scores, collaborative editing cursors) |
A reasonable default: reach for polling first, since it is simpler to build, test, and scale, and it composes with any transport and any caching layer. Move to a subscription only once polling’s trade-offs actually bite — the interval is either too slow to be useful or too fast to be cheap, or the number of concurrent long-lived connections the deployment needs to hold is something the infrastructure can realistically support.
Further reading
-
Queries and Fields — the selection-set and field-resolution model that subscriptions reuse for each event’s execution.
-
Serving over HTTP — how queries and mutations are carried over plain HTTP, for contrast with the WebSocket/SSE/multipart transports used for subscriptions above.
-
Spring Boot: Transports, Security & Testing — wiring
graphql-ws/SSE transports, authenticating a long-lived connection, and testing subscription resolvers on the JVM. -
graphql.org — Subscriptions for the specification-level introduction referenced throughout this page.