GraphQL Subscriptions, Step by Step

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.

Subscriptions, polling and refetching
ApproachHow updates arriveGood fit
SubscriptionServer pushes a result per eventSmall, frequent changes users expect to see at once
PollingClient repeats a query on a timerData that changes now and then
Refetch on actionClient queries again after a user stepScreens that only change when the user acts
Push notificationA message outside the appMobile 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.

One subscription, frame by frame

  1. Client sends connection_init
  2. Server sends connection_ack
  3. Client sends subscribe
  4. Server sends next
  5. Server sends next again
  6. Client sends 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.

Schema and client code

Declaring a subscription field

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!
}

Subscribing with the graphql-ws client

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);
}

The one-root-field rule

Split two streams into two operations

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 }
}

What happens when you subscribe

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.

The graphql-ws protocol in brief

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.

When polling is the better choice

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.

How it works

  1. Press Next to move through the conversation one frame at a time.
  2. Read who sends each frame and what it means.
  3. Watch the operation id: every frame for one subscription carries the same id.
  4. Use Back or Restart to look at a step again.

Tips

Frequently asked questions

Why only one root field per subscription?

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.

Which WebSocket subprotocol does graphql-ws use?

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.

Can subscriptions run without WebSockets?

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.

Do subscriptions scale like queries?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap