Subscriptions
A query returns once. A mutation returns once. A subscription is different: the client subscribes a single time, and the server keeps pushing new payloads for as long as the connection stays open. It is the third root type, and it is how GraphQL models real-time — a new review appears, a track finishes processing, someone starts typing — without the client polling over and over.
The in-browser runner used in the other lessons executes one operation and stops, so it cannot demonstrate a live stream. We will study subscriptions as code plus a sequence diagram, and then point you at a runnable Yoga server you can open and watch stream for real.
Declaring a subscription
Section titled “Declaring a subscription”A subscription field lives on the Subscription root type. It looks like a query field — a name, arguments, a return type — but its meaning is “push me one of these every time the matching event happens”:
type Subscription { reviewAdded(trackId: ID!): Review!}The trackId argument scopes the stream: the client only wants reviews for one track, not the whole catalogue. The return type, Review!, is the shape of each pushed payload.
subscribe vs resolve
Section titled “subscribe vs resolve”A subscription resolver is not one function but two, and keeping them straight is the whole concept:
subscribereturns an async iterator — the source of events. Each time something is pushed onto that iterator, the server wakes up and produces one result for the client.resolveis the ordinary field resolver that runs per event. It receives the value yielded by the iterator and maps it to the payload the client selected.
In graphql-yoga and most servers the event source is a pub/sub bus: a mutation publishes an event, and every matching subscription’s subscribe iterator yields it.
import { createPubSub } from 'graphql-yoga';
const pubSub = createPubSub<{ reviewAdded: [trackId: string, review: Review] }>();
export const resolvers = { Mutation: { addReview: (_parent, { input }) => { const review = saveReview(input); // Publish the event — every matching subscriber is woken up. pubSub.publish('reviewAdded', input.trackId, review); return { review, userErrors: [] }; }, }, Subscription: { reviewAdded: { // subscribe: the event source, scoped to one track. subscribe: (_parent, { trackId }) => pubSub.subscribe('reviewAdded', trackId), // resolve: map each published event to the payload. resolve: (review: Review) => review, }, },};The flow is: a mutation writes data and publishes; the pub/sub bus fans the event out; each subscriber’s subscribe iterator yields it; resolve shapes it; the client receives a payload. The same write that the earlier lessons returned synchronously now also notifies everyone listening.
sequenceDiagram participant C as Client participant S as GraphQL Server participant B as Pub/Sub bus participant W as Writer (another client) C->>S: subscription reviewAdded(trackId: "t1") S->>B: subscribe to "reviewAdded" for t1 Note over C,S: stream stays open W->>S: mutation addReview(input) S->>B: publish "reviewAdded" event B-->>S: yield event to subscriber S-->>C: push Review payload W->>S: mutation addReview(input) again S->>B: publish again B-->>S: yield event S-->>C: push next Review payload
Transports: how the stream reaches the client
Section titled “Transports: how the stream reaches the client”Queries and mutations ride a single HTTP request/response. A subscription needs a channel that stays open and pushes, so it uses a different transport:
- Server-Sent Events (SSE) — a long-lived HTTP response that streams text events one way, server to client. It works over ordinary HTTP, passes through most proxies, and is the modern default in
graphql-yogavia thegraphql-sseprotocol. It is one-directional, which is all a subscription needs. - WebSocket — a full-duplex connection, traditionally used with the
graphql-wsprotocol. It is more capable (two-way) and widely supported by clients, but it is a separate connection to manage and can be harder to route through some infrastructure.
Both carry the same GraphQL subscription operation; they differ only in plumbing. Start with SSE unless you have a concrete reason to need a WebSocket.
When NOT to use a subscription
Section titled “When NOT to use a subscription”Subscriptions are stateful, long-lived connections — they cost server memory and complicate scaling. Reach for them only when the data genuinely changes on the server’s schedule. Prefer alternatives when:
- A mutation result is enough. If a change is driven by the client’s own action, return the affected data from the mutation. Do not subscribe to your own writes.
- The data changes rarely or the client can poll. A dashboard that refreshes every 30 seconds does not need a persistent connection; a periodic query (or a refetch on focus) is simpler and cheaper.
- You need a one-time async result. “Generate this report, tell me when it’s done” is a job-status poll or a webhook, not necessarily a subscription.
A subscription earns its complexity when many clients must see the same event the instant it happens — a live feed, a presence indicator, a price tick.
A streaming server you can run
Section titled “A streaming server you can run”The schema below is a complete Yoga project: it exports typeDefs and resolvers with a mutation that publishes and a subscription that streams. The in-browser runner cannot keep a stream open, so open this in StackBlitz, fire addReview in one GraphiQL tab, and watch reviewAdded push the payload in another.
Needs the Node.js runtime — open in StackBlitz to run.
To see it stream: open the project, then in one GraphiQL tab run subscription { reviewAdded(trackId: "t1") { id rating } } and leave it running. In a second tab run the addReview mutation for trackId: "t1". The subscription tab updates the instant the mutation publishes — that is the whole value of the stream, and no amount of polling matches its latency.