GraphQL Subscriptions with Redis
How to build GraphQL subscriptions that survive multiple server instances by using Redis as the pub/sub backbone. Covers setup, scaling, and the failure modes nobody warns you about.
What you'll learn
- ✓How GraphQL subscriptions work over WebSockets
- ✓Why in-memory pub/sub breaks at scale
- ✓How Redis pub/sub fixes the multi-instance problem
- ✓Patterns for filtering and authorization
- ✓Operational gotchas in production
Prerequisites
- •Basic GraphQL
- •Familiar with WebSockets
GraphQL subscriptions are how you push live data to clients. They look simple in a single-process demo and quietly fall apart the moment you deploy more than one server. Redis pub/sub is the standard fix. This post walks through how it fits in and what to watch out for.
What and Why
A subscription is a long-lived GraphQL operation. Instead of returning a single response, the server sends a stream of events whenever something matches. Under the hood it usually rides on a WebSocket using the graphql-ws protocol.
The reason single-instance code breaks is that pub/sub events live in memory. If a user is connected to server A and the mutation that should fire an event hits server B, server A never hears about it. Redis sits between the servers as a shared event bus so every instance sees every event.
Mental Model
Picture a message bus running outside your application. Every server instance subscribes to channels on Redis. When any instance wants to publish an event, it pushes a message into Redis. Redis fans it out to all subscribers. Each server then walks its list of connected clients and forwards the event to anyone who has matched the subscription’s filter.
The GraphQL layer does not see Redis directly. It sees an async iterator, which is the same abstraction you would use with an in-memory pub/sub. Swapping backends is just a matter of plugging in a different implementation.
Hands-on Example
Set up the publisher with graphql-redis-subscriptions and Redis.
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const options = { host: 'redis', port: 6379 };
const pubsub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options),
});
const resolvers = {
Mutation: {
postMessage: async (_, { room, text }, { user }) => {
const msg = await db.messages.insert({ room, text, userId: user.id });
pubsub.publish(`ROOM_${room}`, { messagePosted: msg });
return msg;
},
},
Subscription: {
messagePosted: {
subscribe: (_, { room }) => pubsub.asyncIterator(`ROOM_${room}`),
},
},
};
Two server instances now share events through Redis. A client connected to instance A receives messages published by instance B.
client A ----ws---- server 1 ---\
\
v
[Redis pub/sub]
^
/
client B ----ws---- server 2 ---/
mutation on server 2 -> publish to Redis -> fanout
-> server 1 forwards to client A
-> server 2 forwards to client B For high-volume topics where most events are filtered out per subscriber, use withFilter from graphql-subscriptions so that the filtering happens after the iterator but before the network send.
Common Pitfalls
The first pitfall is using a single Redis client for both publishing and subscribing. Redis blocks the connection in subscribe mode, so you need two connections. Most libraries enforce this, but custom code often does not.
The second is unbounded channels. Naming a channel ROOM_${roomId} works until you have a million rooms and Redis is tracking that many subscriptions per server. Aggregate where you can and filter on the consumer.
The third is silent disconnects. WebSockets die without notice, especially behind load balancers with short idle timeouts. Configure keep-alive pings on both client and server, and treat reconnection as a first-class flow.
Practical Tips
Authorize on subscription start, not on every event. The subscribe resolver is the right place to check that the user is allowed to listen on a topic. Doing it per event is wasteful and easy to get wrong.
Add a sequence number or timestamp to every event. Clients can detect gaps after a reconnect and ask for the missing window from a normal query.
Monitor Redis memory and the number of active pub/sub channels. Both are early indicators that your subscription design is leaking.
Wrap-up
Redis turns GraphQL subscriptions from a single-process toy into something you can actually scale horizontally. Treat the WebSocket layer, the pub/sub layer, and the data layer as separate concerns, and the system stays comprehensible even as it grows.
Related articles
- GraphQL GraphQL Subscriptions Tutorial
Add real-time data to your GraphQL API with subscriptions. Learn the transport, pubsub patterns, and a working Apollo example.
- GraphQL GraphQL Caching with Apollo Client
How Apollo Client's normalized cache works, why entity IDs matter, and the patterns for cache updates, refetches, and consistent UI after mutations.
- GraphQL GraphQL Error Handling Best Practices
Compare the errors array, union result types, and partial responses to design predictable, typed error handling for your GraphQL APIs and clients.
- GraphQL GraphQL Federation: A Practical Overview
Understand Apollo Federation: subgraphs, the gateway, entity references, and when to choose federation over a monolithic GraphQL schema.