Subscriptions

This section documents the current GraphQL specification (October 2021), plus the working draft for @defer/@stream/@oneOf, and the GraphQL-over-HTTP specification, as published at graphql.org and spec.graphql.org — which are the references these pages are written and verified against — and, for the integration pages, against Spring for GraphQL (2.0.x), Strawberry, Ariadne, and the client docs for Apollo Client / urql / Relay. No specific patch version is pinned. Some surfaces (Apollo Router/managed federation, graphql-ws internals, the Relay compiler internals, GraalVM native) are linked rather than documented in depth.

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 usual resolve function) 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

graphql-ws

The modern, actively maintained WebSocket sub-protocol (package name graphql-ws, protocol id graphql-transport-ws). Bidirectional, single WebSocket connection carrying every subscription plus an explicit acknowledgement handshake and per-operation subscribe/complete/error messages. The transport most new servers and clients (Apollo Client, urql, Relay) default to today.

subscriptions-transport-ws

The original Apollo WebSocket sub-protocol (protocol id graphql-ws, confusingly the same name later reused by the newer library). No longer actively maintained; still found in older deployments and some legacy client configurations. Message names and the handshake sequence differ from graphql-ws and the two protocols are not wire-compatible, so client and server must agree on which one they speak.

Server-Sent Events (SSE)

A one-way, HTTP-native event stream (text/event-stream) instead of a WebSocket. Simpler infrastructure (plain HTTP, works through most proxies/load balancers without special-casing), automatic browser reconnection, but strictly server-to-client — there is no equivalent path back to the server on the same connection, so unsubscribing or sending further client messages needs a separate request.

Multipart HTTP (multipart/mixed)

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 (@defer/@stream) mentioned in the disclaimer above — though for long-lived subscriptions specifically it is less common than graphql-ws or SSE.

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:

sequenceDiagram participant C as Client participant S as Server C->>S: connection_init S->>C: connection_ack C->>S: subscribe (id: "1", query: OnOrderStatusChanged) S-->>C: next (id: "1", event 1) S-->>C: next (id: "1", event 2) S-->>C: next (id: "1", event 3) S->>C: complete (id: "1")

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.