Subscription
subscription OperationName($var: Type) { eventField(filter: $var) { id payload } }
A GraphQL subscription establishes a long-lived connection over which the server pushes events to the client as they occur. Subscriptions are executed once per event – when a subscribed event fires, the server runs the subscription selection set against the event payload and pushes the result. The dominant transport is WebSocket using the graphql-ws protocol (RFC: github.com/enisdenjo/graphql-ws). Server-Sent Events (SSE) is also used for unidirectional push.
How it works
Subscriptions enable real-time data in GraphQL. Unlike queries and mutations which are request-response, subscriptions keep a connection open and deliver a stream of results.
Execution model: when a client subscribes, the server registers an event handler for the specified topic. Each time the event fires (a new message, an order status change, a price tick), the server executes the subscription's selection set against the event payload and sends the result to the client. This is fundamentally different from polling – the server initiates the push.
Transport – graphql-ws: the dominant protocol for GraphQL subscriptions over WebSocket. The graphql-ws spec defines message types: connection_init, connection_ack, subscribe, next, error, complete. Each subscription within a single WebSocket connection is identified by a client-chosen string ID, enabling multiplexing.
Transport – SSE: for unidirectional push (server to client only), Server-Sent Events over HTTP work well. The GraphQL over HTTP draft spec defines how to use multipart/mixed responses for incremental delivery. SSE subscriptions work through firewalls and proxies that block WebSocket.
Resolver model: subscription resolvers have two parts – an async iterator (the event source, e.g., a Redis pub/sub channel) and a resolve function (transforms the raw event into the GraphQL result type).
Single root field: the spec (§6.2) requires a subscription operation to have exactly one selection at the root level. Multiple root fields in a subscription are forbidden.
Examples
subscription OnNewMessage($channelId: ID!) {
messageSent(channelId: $channelId) {
id
text
sender {
name
avatarUrl
}
sentAt
}
}// Client → Server: initialize connection
{ "type": "connection_init", "payload": { "Authorization": "Bearer <token>" } }
// Server → Client: acknowledge
{ "type": "connection_ack" }
// Client → Server: start subscription
{ "type": "subscribe", "id": "sub_1",
"payload": { "query": "subscription { orderUpdated(orderId: \"ord_1\") { status } }" } }
// Server → Client: event received
{ "type": "next", "id": "sub_1",
"payload": { "data": { "orderUpdated": { "status": "SHIPPED" } } } }
// Client → Server: stop subscription
{ "type": "complete", "id": "sub_1" }subscription PriceTicker($symbol: String!) {
priceUpdated(symbol: $symbol) {
symbol
price
change
changePercent
timestamp
}
}Spec Rules
A subscription operation MUST have exactly one root field – multiple root fields are forbidden by spec §6.2
Subscriptions must use a persistent transport – WebSocket (graphql-ws) or SSE, not plain HTTP
Each subscription within a WebSocket connection needs a unique client-chosen ID for multiplexing
Subscription resolvers require two parts: an async iterator (event source) and a resolve function
Always handle connection_ack timeout – if no ack within N seconds, the connection should be considered failed
Never put heavy computation in the subscription resolver – it runs on every event for every subscribed client