Skip to content
C Codeloom
GraphQL

GraphQL Subscriptions Tutorial

Add real-time data to your GraphQL API with subscriptions. Learn the transport, pubsub patterns, and a working Apollo example.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • When subscriptions are the right tool
  • How graphql-ws transports messages
  • Implementing a pubsub-backed subscription
  • Authentication on the WebSocket
  • Scaling with Redis

Prerequisites

  • Comfortable with GraphQL queries and mutations

What and Why

Queries and mutations are request-response. Subscriptions are a long-lived stream that pushes updates to the client whenever something interesting happens. They are great for chat, live dashboards, collaborative editing, and any UI that should update without polling.

Mental Model

A subscription is a query that returns a stream of results over time. The transport is usually WebSocket with the graphql-ws subprotocol. The server runs a resolver that returns an async iterator. Each value emitted gets shaped by the selection set and sent to the client.

Client --ws--> Server (graphql-ws)
     connection_init
     subscribe { id, payload }

Server publishes events:
     next { id, payload: data }
     next { id, payload: data }
     ...
     complete { id }
Subscription transport

Hands-on Example

Schema with a subscription.

type Message {
  id: ID!
  room: String!
  text: String!
}

type Query {
  messages(room: String!): [Message!]!
}

type Mutation {
  postMessage(room: String!, text: String!): Message!
}

type Subscription {
  messagePosted(room: String!): Message!
}

Apollo Server with graphql-ws.

import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import express from "express";
import { createServer } from "http";
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();
const messages = [];

const resolvers = {
  Query: {
    messages: (_, { room }) => messages.filter((m) => m.room === room),
  },
  Mutation: {
    postMessage: (_, { room, text }) => {
      const msg = { id: String(messages.length + 1), room, text };
      messages.push(msg);
      pubsub.publish(`MSG_${room}`, { messagePosted: msg });
      return msg;
    },
  },
  Subscription: {
    messagePosted: {
      subscribe: (_, { room }) => pubsub.asyncIterator(`MSG_${room}`),
    },
  },
};

const schema = makeExecutableSchema({ typeDefs, resolvers });

const app = express();
const httpServer = createServer(app);

const wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" });
const serverCleanup = useServer({ schema }, wsServer);

const apollo = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    {
      async serverWillStart() {
        return { async drainServer() { await serverCleanup.dispose(); } };
      },
    },
  ],
});

await apollo.start();
app.use("/graphql", express.json(), expressMiddleware(apollo));
httpServer.listen(4000);

Client with Apollo Link.

import { split, HttpLink, ApolloClient, InMemoryCache } from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { getMainDefinition } from "@apollo/client/utilities";

const wsLink = new GraphQLWsLink(createClient({ url: "ws://localhost:4000/graphql" }));
const httpLink = new HttpLink({ uri: "http://localhost:4000/graphql" });

const link = split(
  ({ query }) => {
    const def = getMainDefinition(query);
    return def.kind === "OperationDefinition" && def.operation === "subscription";
  },
  wsLink, httpLink,
);

const client = new ApolloClient({ link, cache: new InMemoryCache() });

Common Pitfalls

  • Building chat with polling and saying “it works”. It does, but a subscription cuts latency and load dramatically.
  • Forgetting authentication. connectionParams lets you send a token at connect time. Validate it before subscribing.
  • Using the in-memory PubSub in production. It does not span processes. Switch to graphql-redis-subscriptions.
  • Sending too much data. Only emit the fields the client needs and let the selection set filter the rest.

Practical Tips

  • Topic naming matters. Use MSG_<room> or similar so you do not have to filter every event in the resolver.
  • Always close subscriptions in tests and during deployment with a drain step.
  • Pair subscriptions with optimistic UI on the client for instant feedback on mutations.
  • Monitor open connection counts. WebSockets are stateful and can pile up.
  • Document the subscription contract in your schema docs. Clients need to know what triggers an event.

Wrap-up

Subscriptions let GraphQL keep up with the user. Set up the WebSocket transport, back it with a pubsub the matches your scale, and design topics that fit how clients listen. Once the plumbing is in place, real-time features feel like a natural extension of the API rather than a separate system.