A GraphQL subscription is a long-lived operation: the client sends a subscription document once, and the server pushes a new result each time the matching event happens. GraphQL does not fix the transport. In practice it runs over WebSockets, using the graphql-ws protocol, or over Server-Sent Events. Each subscription operation must select exactly one root field.
| Approach | How updates arrive | Good fit |
|---|---|---|
| Subscription | Server pushes a result per event | Small, frequent changes users expect to see at once |
| Polling | Client repeats a query on a timer | Data that changes now and then |
| Refetch on action | Client queries again after a user step | Screens that only change when the user acts |
| Push notification | A message outside the app | Mobile users with the app closed |
Updated
The stepper below plays one subscription over the graphql-ws protocol, message by message. Each step shows the JSON frame that crosses the socket and which side sends it, from the first handshake to the final complete.
{ "type": "connection_init", "payload": { "token": "abc123" } }Sent right after the socket opens. The payload is free-form; servers often read a token from it.
Subscription is a root type like Query and Mutation. Each field is one stream a client can open.
type Review {
rating: Int!
commentary: String
}
type Subscription {
reviewCreated(movie: ID!): Review!
}createClient opens the socket lazily, and iterate returns an async iterator that yields one result per event.
import { createClient } from "graphql-ws";
const client = createClient({ url: "wss://api.example.com/graphql" });
const reviews = client.iterate({
query: "subscription ($movie: ID!) { reviewCreated(movie: $movie) { rating commentary } }",
variables: { movie: "m7" },
});
for await (const result of reviews) {
if (result.errors) console.warn(result.errors);
else addReview(result.data.reviewCreated);
}A single subscription listing reviewCreated and another root field fails validation. Two named operations work.
subscription OnReview($movie: ID!) {
reviewCreated(movie: $movie) { rating }
}
subscription OnRating($movie: ID!) {
ratingChanged(movie: $movie) { average }
}
The client sends a document that starts with the subscription keyword, picks one root field and lists the fields it wants in each event. The server validates it once and keeps it open.
When something publishes a matching event, such as a new review, the server runs the selection set against that event and sends the result. Each result has the normal data and errors shape, so client code reads it the same way it reads a query.
After the WebSocket upgrade, the client sends connection_init, often with an auth token in its payload, and waits for connection_ack. Only then may it send subscribe frames. Each subscribe carries a unique id, and every next, error or complete frame for that operation repeats it, so many subscriptions share one socket.
Either side can send ping and the other replies pong. The server closes the socket with code 4408 if connection_init never arrives in time, and with 4409 when a client reuses an id that is still running.
Subscriptions need sticky connections, a pub/sub layer and careful client code for resubscribing and races. For a dashboard that refreshes every minute, a query on a timer is simpler and fails more gracefully.
graphql.org suggests subscriptions for data that changes often and in small steps, and polling, push notifications or refetching for slower data or low-bandwidth mobile clients.
Each subscription maps to one stream of events. graphql.org shows that putting two root fields in one subscription operation is invalid; write two named subscriptions instead.
It negotiates graphql-transport-ws during the handshake, with frame types such as connection_init, subscribe, next and complete. Other GraphQL WebSocket libraries may use a different protocol, so check that client and server agree before debugging anything else.
Yes. The graphql-sse protocol sends each result as a Server-Sent Event named next and ends with complete. It works over plain HTTP, which helps behind proxies that block WebSocket upgrades.
No. graphql.org points out that each subscribed client is tied to a server instance and needs state kept for its lifetime, so you also need a pub/sub system to fan events out across servers.
Related: SSE vs WebSocket, GraphQL vs REST, GraphQL mutations, WebRTC vs WebSocket.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.