Subscriptions and real-time updates
The WebSocket transport, pub/sub design, the subscription lifecycle, backpressure and scaling past a single instance.
Defining a subscription
type Subscription {
bookPublished(authorId: ID!): Book!
orderStatusChanged(orderId: ID!): OrderStatusEvent!
}
type OrderStatusEvent {
orderId: ID!
status: OrderStatus!
changedAt: DateTime!
}import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub(); // in-memory: single instance only
const resolvers = {
Subscription: {
bookPublished: {
subscribe: (_p, { authorId }) => pubsub.asyncIterableIterator("BOOK_PUBLISHED_" + authorId),
resolve: (payload) => payload.book,
},
},
Mutation: {
async publishBook(_p, { id }, ctx) {
const book = await ctx.db.book.publish(id);
// one topic per author, so only interested subscribers are woken
await pubsub.publish("BOOK_PUBLISHED_" + book.authorId, { book });
return book;
},
},
};- Subscriptions run over WebSockets using the
graphql-transport-wsprotocol. HTTP and plaingraphql-wsdo not carry them. - Authentication happens during the connection handshake, not per message. Re-authenticate on a timer or close the socket when the token expires.
- A subscription returns an async iterator. Every subscription must have a matching unsubscribe path, or the socket and its listeners leak.
⚠️
A subscription per entity is a client-controlled fan-out. Without a limit on concurrent subscriptions per connection, one client can subscribe to ten thousand ids, and every publish then walks that list on your server.
Delivery and reliability
| Concern | Approach | Why |
|---|---|---|
| Multiple instances | Redis pub/sub or a broker | An in-memory PubSub only reaches one process |
| Missed messages | Client re-syncs on reconnect | WebSockets drop; subscriptions do not replay |
| Slow consumer | Bound the queue per socket | Otherwise the publisher blocks or memory grows |
| Ordering | Partition by entity id | Global ordering is expensive and rarely needed |
| At-least-once | Send an event id, client dedupes | A reconnect can replay a frame |
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";
const pubsub = new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL),
subscriber: new Redis(process.env.REDIS_URL),
});
// a client that reconnects must catch up
// query the delta since lastEventId before resuming the subscription
const { data } = await client.query({ query: ORDER_EVENTS_SINCE, variables: { since: lastEventId } });Treat a subscription as an optimisation, never as the only source of truth. The client should be able to reconstruct current state with a query after a reconnect; the subscription only keeps it fresh between queries.
Running subscriptions in production
- WebSocket connections are long-lived: configure idle timeouts on the load balancer to match, and send a periodic keep-alive ping.
- Sticky sessions are needed only if you keep per-connection state; a shared pub/sub backend removes that requirement.
- Authenticate the handshake, then check authorization per event: a user who lost access must stop receiving updates.
- Emit a bounded payload. Sending an entire entity on every change is a bandwidth problem at scale - send the id and let the client refetch if it needs details.
- Measure the number of active subscriptions and the publish-to-delivery latency; a growing subscriber set is a capacity signal.
// a graceful shutdown that closes sockets properly
process.on("SIGTERM", async () => {
await server.stop(); // stop accepting new connections
await pubsub.close(); // release the redis subscribers
await db.destroy();
process.exit(0);
});FAQ
Subscriptions or polling?
Polling is simpler, stateless and easy to cache; it is often good enough for anything that changes every few seconds. Choose subscriptions when the update rate is high, the latency requirement is tight, or the traffic cost of polling every client is real.
How do I test a subscription?
Subscribe with a test client, run the mutation that should trigger it, and assert on the received payload with a timeout. Also test that unsubscribing stops delivery and that a token expiring closes the connection.
Related
Authentication and field-level authorization Federation, caching and next steps
Last refreshed 2026-09-18.