GraphQL Subscriptions for Real-Time Data
Build real-time features with GraphQL subscriptions using WebSockets, including setup with Apollo Server and client-side integration.
What you'll learn
- ✓How GraphQL subscriptions work under the hood with WebSockets
- ✓How to set up subscriptions with Apollo Server and graphql-ws
- ✓How to consume subscriptions on the client with automatic reconnection
Prerequisites
- •Basic GraphQL knowledge
- •Familiarity with Node.js and Express
What Are GraphQL Subscriptions?
Queries and mutations follow a request-response pattern: the client asks, the server answers. Subscriptions break this pattern. A subscription establishes a persistent connection between client and server, allowing the server to push data to the client whenever a specific event occurs.
Common use cases include chat messages, live notifications, real-time dashboards, collaborative editing, and order status updates.
type Subscription {
messageSent(channelId: ID!): Message!
}
type Message {
id: ID!
text: String!
sender: User!
createdAt: String!
}
The Transport Layer: WebSockets
GraphQL subscriptions typically use WebSockets for the persistent connection. The modern standard is the graphql-ws protocol, which replaced the older subscriptions-transport-ws library. Always use graphql-ws for new projects as the older library is unmaintained.
Server Setup with Apollo Server
Install the required packages:
npm install @apollo/server graphql-ws ws graphql
Set up an Express server with both HTTP and WebSocket support:
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';
const app = express();
const httpServer = createServer(app);
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Create WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
// Set up graphql-ws
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));
httpServer.listen(4000, () => {
console.log('Server running on http://localhost:4000/graphql');
console.log('WebSocket running on ws://localhost:4000/graphql');
});
The PubSub System
Subscriptions need a publish-subscribe mechanism. The server publishes events, and active subscriptions receive them. For development, use an in-memory PubSub. For production, use Redis or another external broker.
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const MESSAGE_SENT = 'MESSAGE_SENT';
Writing Subscription Resolvers
Subscription resolvers differ from query and mutation resolvers. They return an AsyncIterator using the subscribe function:
const resolvers = {
Query: {
messages: (_, { channelId }) => db.messages.findByChannel(channelId),
},
Mutation: {
sendMessage: async (_, { channelId, text }, context) => {
const message = await db.messages.create({
channelId,
text,
senderId: context.userId,
});
// Publish the event
await pubsub.publish(MESSAGE_SENT, {
messageSent: message,
channelId,
});
return message;
},
},
Subscription: {
messageSent: {
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([MESSAGE_SENT]);
},
// Optional: filter events to only relevant ones
resolve: (payload) => payload.messageSent,
},
},
};
Filtering Subscriptions
Most subscriptions need filtering. A user subscribing to messages in channel “general” should not receive messages from channel “random”. Use withFilter for this:
import { withFilter } from 'graphql-subscriptions';
const resolvers = {
Subscription: {
messageSent: {
subscribe: withFilter(
() => pubsub.asyncIterator([MESSAGE_SENT]),
(payload, variables) => {
// Only send to subscribers of this specific channel
return payload.channelId === variables.channelId;
}
),
resolve: (payload) => payload.messageSent,
},
},
};
The filter function receives the published payload and the subscription variables. Return true to send the event to this subscriber, false to skip it.
Production PubSub with Redis
The in-memory PubSub only works for single-server deployments. With multiple server instances, you need an external broker. Redis is the most common choice:
npm install graphql-redis-subscriptions ioredis
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const pubsub = new RedisPubSub({
publisher: new Redis({ host: 'localhost', port: 6379 }),
subscriber: new Redis({ host: 'localhost', port: 6379 }),
});
This is a drop-in replacement. The API is identical to the in-memory PubSub, but events are broadcast across all server instances through Redis.
Client-Side Integration
On the client, configure Apollo Client with a split link that routes subscriptions through WebSockets and everything else through HTTP:
npm install @apollo/client graphql-ws
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql',
connectionParams: {
authToken: localStorage.getItem('token'),
},
})
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
Using Subscriptions in React
Use the useSubscription hook to listen for events:
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageSent($channelId: ID!) {
messageSent(channelId: $channelId) {
id
text
sender {
name
}
createdAt
}
}
`;
function ChatMessages({ channelId }) {
const { data, loading, error } = useSubscription(MESSAGE_SUBSCRIPTION, {
variables: { channelId },
});
if (loading) return <p>Listening for messages...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<p>New message: {data.messageSent.text}</p>
</div>
);
}
To combine initial data from a query with live updates from a subscription, use subscribeToMore:
import { useQuery, gql } from '@apollo/client';
function ChatRoom({ channelId }) {
const { data, subscribeToMore } = useQuery(GET_MESSAGES, {
variables: { channelId },
});
useEffect(() => {
const unsubscribe = subscribeToMore({
document: MESSAGE_SUBSCRIPTION,
variables: { channelId },
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const newMessage = subscriptionData.data.messageSent;
return {
messages: [...prev.messages, newMessage],
};
},
});
return () => unsubscribe();
}, [channelId, subscribeToMore]);
return (
<ul>
{data?.messages.map(msg => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
);
}
Authentication in Subscriptions
WebSocket connections authenticate differently from HTTP requests. Pass authentication data through connectionParams:
// Server side: validate connection
const serverCleanup = useServer(
{
schema,
context: async (ctx) => {
const token = ctx.connectionParams?.authToken;
if (!token) throw new Error('Missing auth token');
const user = await verifyToken(token);
return { userId: user.id };
},
onConnect: async (ctx) => {
const token = ctx.connectionParams?.authToken;
if (!token) return false; // reject connection
},
},
wsServer
);
When to Use Subscriptions vs. Polling
Subscriptions are ideal when data changes are infrequent but must appear instantly, such as chat messages or notifications. For data that changes frequently and slight delays are acceptable, polling with useQuery’s pollInterval option is simpler and more scalable. Subscriptions maintain persistent connections which consume server resources, so use them deliberately.
Summary
GraphQL subscriptions enable real-time features by maintaining a persistent WebSocket connection between client and server. Use graphql-ws as the transport protocol, implement PubSub for event distribution (Redis for production), filter events to relevant subscribers, and split your Apollo Client links to route subscriptions through WebSockets. Start with simple use cases like notifications before building more complex real-time features.
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 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.
- Django Django Channels: Real-Time WebSockets
Add real-time WebSocket support to Django with Channels — consumers, routing, channel layers, groups, and building a live chat.
- FastAPI WebSocket Endpoints in FastAPI for Real-Time Features
Build real-time features with FastAPI WebSockets including chat rooms, live notifications, and connection management patterns.