Subscriptions: Real-Time Data over WebSockets

Advanced
14 min

Subscriptions: Real-Time Data over WebSockets

Subscriptions are the third operation type: the client opens a long-lived connection and the server pushes a result whenever an event occurs. After this lesson you will be able to define a subscription, publish events from mutations, serve them over WebSockets with graphql-ws, and consume them from Apollo Client.

Defining a Subscription

A subscription lives under the Subscription root type. Each field describes one event stream and the payload a client receives per event:

graphql
type Subscription { commentAdded(postId: ID!): Comment! }

On the server, a subscription resolver is an object with a subscribe function returning an AsyncIterator; each yielded value becomes one response. The graphql-subscriptions package provides an in-memory PubSub that turns publish calls into such iterators:

javascript
import { PubSub, withFilter } from "graphql-subscriptions"; const pubsub = new PubSub(); const resolvers = { Mutation: { addComment: async (_p, { postId, body }, { db, user }) => { const comment = await db.comments.create({ postId, body, authorId: user.id }); pubsub.publish("COMMENT_ADDED", { commentAdded: comment }); return comment; }, }, Subscription: { commentAdded: { subscribe: withFilter( () => pubsub.asyncIterableIterator(["COMMENT_ADDED"]), (payload, args) => payload.commentAdded.postId === args.postId ), }, }, };

The published object is keyed by the subscription field name (commentAdded). withFilter drops events the subscriber did not ask for, so a client watching post 7 never sees comments for post 8.

Serving Subscriptions with graphql-ws

HTTP cannot push, so subscriptions use the WebSocket protocol defined by graphql-ws. Apollo Server keeps handling HTTP while a ws server on the same port handles subscriptions:

javascript
// npm install graphql-ws ws @graphql-tools/schema import { createServer } from "node:http"; import { WebSocketServer } from "ws"; import { useServer } from "graphql-ws/use/ws"; import { makeExecutableSchema } from "@graphql-tools/schema"; const schema = makeExecutableSchema({ typeDefs, resolvers }); const httpServer = createServer(app); const wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" }); const wsCleanup = useServer({ schema, context: (ctx) => buildWsContext(ctx.connectionParams) }, wsServer); const server = new ApolloServer({ schema, plugins: [{ async serverWillStart() { return { async drainServer() { await wsCleanup.dispose(); } }; } }], });

A WebSocket has no per-message headers, so clients send the token in connectionParams and the context callback reads it once.

Consuming from Apollo Client

Apollo Client routes subscriptions through GraphQLWsLink and everything else through HttpLink:

javascript
import { ApolloClient, InMemoryCache, HttpLink, split } from "@apollo/client"; import { GraphQLWsLink } from "@apollo/client/link/subscriptions"; import { getMainDefinition } from "@apollo/client/utilities"; import { createClient } from "graphql-ws"; const wsLink = new GraphQLWsLink(createClient({ url: "ws://localhost:4000/graphql", connectionParams: { authToken: token }, })); const httpLink = new HttpLink({ uri: "http://localhost:4000/graphql" }); const isSubscription = ({ query }) => getMainDefinition(query).operation === "subscription"; export const client = new ApolloClient({ link: split(isSubscription, wsLink, httpLink), cache: new InMemoryCache(), });

In a component, useSubscription(COMMENT_ADDED, { variables: { postId } }) re-renders with data.commentAdded on every event; subscribeToMore appends events to a list loaded by useQuery.

Scaling and Alternatives

| Concern | Guidance | |---|---| | Multiple server instances | In-memory PubSub only reaches one process; use graphql-redis-subscriptions | | Simple one-way streams | Server-Sent Events over HTTP (built into GraphQL Yoga) avoid WebSocket infrastructure | | Rarely changing data | Polling with pollInterval is simpler and often enough |

Quick Quiz
Question 1 of 2

What must a subscription field's `subscribe` function return?

Key Takeaways

  • Subscriptions are long-lived operations; the server pushes one result per event.
  • A subscribe resolver returns an AsyncIterator, usually from a PubSub; mutations call publish.
  • graphql-ws serves subscriptions over WebSockets, with auth in connectionParams.
  • Apollo Client uses split to route subscriptions through GraphQLWsLink.
  • Use a Redis-backed PubSub for multiple instances; consider SSE or polling for simpler cases.

Next lesson: File Uploads in GraphQL — handle multipart uploads with the Upload scalar or hand them off to signed URLs.

Subscriptions: Real-Time Data over WebSockets - GraphQL | CodeYourCraft | CodeYourCraft