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.
A subscription lives under the Subscription root type. Each field describes one event stream and the payload a client receives per event:
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:
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.
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:
// 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.
Apollo Client routes subscriptions through GraphQLWsLink and everything else through HttpLink:
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.
| 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 |
What must a subscription field's `subscribe` function return?
subscribe resolver returns an AsyncIterator, usually from a PubSub; mutations call publish.graphql-ws serves subscriptions over WebSockets, with auth in connectionParams.split to route subscriptions through GraphQLWsLink.Next lesson: File Uploads in GraphQL — handle multipart uploads with the Upload scalar or hand them off to signed URLs.