GraphQL Subscriptions: Real-Time Data with WebSockets
GraphQL subscriptions provide real-time communication between the server and client. Unlike queries (one-time fetch) and mutations (write), subscriptions maintain a persistent connection that pushes updates to the client whenever relevant events occur. This makes them ideal for chat applications, live dashboards, notifications, collaborative editing, and any feature requiring real-time data flow.
How Subscriptions Work
Subscriptions use a publish-subscribe pattern over WebSockets. The client sends a subscription request to the server, specifying the events it wants to listen to. The server keeps the connection open and pushes data when the subscribed events fire.
Client Server
| |
|--- subscription { |
| postAdded { |
| title |
| author |
| } |
|----------------------->|
| |
|<---- data: { |
| postAdded: { |
| "A new post" |
| } |
| } |
| (when event fires) |GraphQL Subscription Schema
Define subscriptions in your schema using the Subscription type, just like Query and Mutation:
type Subscription {
postAdded(channelId: ID!): Post
postUpdated(postId: ID!): Post
notificationReceived(userId: ID!): Notification
---Implementation with Apollo Server
Apollo Server provides built-in subscription support through WebSockets.
Server Setup
const { ApolloServer } = require('apollo-server');
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const POST_ADDED = 'POST_ADDED';
const typeDefs = gql`
type Subscription {
postAdded(channelId: ID!): Post
}
`;
const resolvers = {
Subscription: {
postAdded: {
subscribe: (_, { channelId }) =>
pubsub.asyncIterator(`${POST_ADDED}_${channelId}`)
}
}
---;
const server = new ApolloServer({
typeDefs,
resolvers,
subscriptions: {
path: '/subscriptions'
}
---);Publishing Events
When a mutation creates a post, it publishes to the subscription channel:
const resolvers = {
Mutation: {
addPost: (_, { channelId, title, content }) => {
const post = { id: generateId(), title, content };
pubsub.publish(`${POST_ADDED}_${channelId}`, {
postAdded: post
});
return post;
}
}
---;Client Setup (Apollo Client)
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: { reconnect: true }
---);
const client = new ApolloClient({
link: wsLink,
cache: new InMemoryCache()
---);
const SUBSCRIPTION = gql`
subscription OnPostAdded($channelId: ID!) {
postAdded(channelId: $channelId) {
id
title
content
}
}
`;
const observable = client.subscribe({
query: SUBSCRIPTION,
variables: { channelId: '123' }
---);
observable.subscribe({
next: ({ data }) => console.log('New post:', data.postAdded),
error: (err) => console.error('Subscription error:', err)
---);Event Filtering
Subscriptions often need to filter events. In Apollo, you use withFilter:
const { withFilter } = require('graphql-subscriptions');
const resolvers = {
Subscription: {
postUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(POST_UPDATED),
(payload, variables) =>
payload.postUpdated.id === variables.postId
)
}
}
---;Using Subscriptions with React
Apollo Client’s React hooks make subscriptions straightforward to integrate into React components. The useSubscription hook automatically manages the subscription lifecycle, including reconnection and cleanup when the component unmounts:
import { useSubscription, gql } from '@apollo/client';
const POST_ADDED_SUBSCRIPTION = gql`
subscription OnPostAdded($channelId: ID!) {
postAdded(channelId: $channelId) {
id
title
content
author {
name
}
}
}
`;
function PostFeed({ channelId }) {
const { data, loading, error } = useSubscription(
POST_ADDED_SUBSCRIPTION,
{ variables: { channelId } }
);
if (loading) return <p>Connecting to live feed...</p>;
if (error) return <p>Connection error: {error.message}</p>;
return <div>{data?.postAdded?.title}</div>;
---The useSubscription hook integrates seamlessly with the Apollo Client cache — when subscription data arrives, it updates the normalized cache automatically, ensuring that any query reading the same data reflects the change. For more granular control, the hook provides onData and onError callbacks, and you can combine subscriptions with queries using the updateQuery option to merge subscription results into existing query data.
Subscription Security
WebSocket connections introduce unique security challenges because they persist beyond a single request-response cycle. Validate the Origin header during the WebSocket handshake to prevent cross-site WebSocket hijacking attacks. Implement token expiration checking on every reconnection — a stolen token should not grant indefinite subscription access. For sensitive data, use per-event authorization checks in the subscription resolver rather than only checking authentication during the initial connection. Consider rate limiting subscription events per client to prevent abusive consumers from flooding all subscribers with high-frequency updates. When using the onConnect hook to authenticate, verify that the connection token is still valid and has not been revoked since it was issued, rather than trusting a token presented during the initial handshake indefinitely.
Advanced PubSub Implementations
The default PubSub from graphql-subscriptions works for single-server deployments but does not scale horizontally. For production systems with multiple server instances, use a shared backend.
Redis PubSub
const { RedisPubSub } = require('graphql-redis-subscriptions');
const pubsub = new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL),
subscriber: new Redis(process.env.REDIS_URL)
---);MQTT, Kafka, RabbitMQ
For high-throughput or durable messaging systems, any message broker that supports pub/sub can be integrated as a backend. These provide message persistence, replay capabilities, and robust delivery guarantees.
Authentication and Authorization
Subscriptions need special handling for authentication since the connection is persistent.
const server = new ApolloServer({
subscriptions: {
onConnect: (connectionParams) => {
if (!connectionParams.authToken) {
throw new Error('Authentication required');
}
const user = verifyToken(connectionParams.authToken);
return { user };
}
},
context: ({ connection }) => {
if (connection) {
return { user: connection.context.user };
}
return {};
}
---);Error Handling
Subscription errors are different from query errors. A failed subscription should not crash the server — it should log the error and allow reconnection.
observable.subscribe({
next: handleData,
error: (err) => {
console.error('Subscription error:', err);
// Attempt to resubscribe after delay
setTimeout(initSubscription, 5000);
},
complete: () => {
console.log('Subscription ended');
// Server closed the connection, decide whether to reconnect
}
---);Performance Considerations
Connection Management
Each subscription maintains a WebSocket connection. For applications with millions of users, connection management becomes critical. Consider connection pooling, load balancers with WebSocket support, and dedicated subscription servers.
Payload Size
Subscription payloads should be minimal. Push only the data the client needs — avoid sending entire objects when IDs are sufficient. The client can then query for full details if needed.
Backpressure
When the server publishes events faster than the client can consume them, you need backpressure handling. Implement rate limiting at the publisher level or buffer management at the client level.
Subscription Lifecycle
Track subscriptions and clean up when clients disconnect. Unnotified disconnections can leak resources. Use the onDisconnect hook to release any per-connection state, unsubscribe from Redis channels, and update metrics.
const server = new ApolloServer({
subscriptions: {
onDisconnect: () => {
console.log('Client disconnected, cleaning up');
// Release associated resources
}
}
---);Subscription Batching
When the same event triggers multiple related subscriptions, consider batching the event delivery. Instead of publishing one event per affected client, publish a single event that contains all updated IDs and let clients filter on the client side. This reduces Redis channel saturation and WebSocket message overhead for high-frequency events like stock price updates or live sports scores. For applications with thousands of subscribers per channel, implement fan-out at the server level — a single database poll or event source feeds an in-memory distributor that broadcasts to all local subscriptions without each subscriber polling independently.
When to Use Subscriptions
Subscriptions are not always the right choice. Evaluate alternatives:
- Polling: Simple, works over regular HTTP, easy to cache. Use for data that changes infrequently.
- Subscriptions: Real-time, low latency. Use for live updates, notifications, collaborative features.
- Live queries: Automatic refetching based on data dependencies. More complex but more declarative.
FAQ
What is the difference between subscriptions and polling? Subscriptions maintain a persistent connection and push updates immediately when events occur. Polling sends periodic HTTP requests to check for new data. Subscriptions have lower latency but require more server resources. Polling is simpler and works through standard HTTP infrastructure.
How do I handle WebSocket reconnection? Apollo Client supports automatic reconnection with the reconnect: true option. Implement exponential backoff for reconnection attempts (1s, 2s, 4s, 8s, max 30s). Track subscription state in your application to show connection status to users.
Can I use subscriptions with serverless functions? WebSocket connections are stateful and long-lived, making them incompatible with stateless serverless functions. Consider using third-party services like Pusher, Ably, or AWS AppSync for real-time features in serverless architectures.
How do I test GraphQL subscriptions? Use the Apollo Server testing utilities with a WebSocket client. Write integration tests that open a subscription, trigger the corresponding mutation, and verify the subscription receives the expected data. Mock the pub/sub backend for unit tests.
What is the best way to scale subscriptions? Use Redis PubSub to share subscription state across server instances. Use a dedicated WebSocket server or process for subscriptions, separate from your REST API. Consider using a managed WebSocket service like AWS API Gateway WebSockets for very large deployments.
How do I handle subscription backpressure? Implement flow control using the WebSocket writable stream state — pause publishing when the client buffer is full and resume when it drains. For server-side backpressure, use a message broker that supports consumer groups and prefetch limits (RabbitMQ, Kafka). At the application level, skip intermediate events when the client is overwhelmed by sampling event streams or using debouncing at the publisher level.
Can subscriptions work with GraphQL federation? Yes. Apollo Federation supports subscriptions across subgraphs using the @shareable directive for shared event types. Each subgraph can define its own subscription resolvers, and the Apollo Router (or gateway) aggregates them into a unified subscription endpoint. Subscriptions traverse the federation graph, so a subscription on a federated Order type resolves through the appropriate subgraph’s subscription resolver. Ensure your subgraph’s pub/sub backend is shared across all instances of that subgraph for consistent event delivery.
Conclusion
GraphQL subscriptions bring real-time capabilities to your API with the same type-safe, declarative approach as queries and mutations. Start with a simple PubSub implementation for development, then migrate to Redis or Kafka for production scale. Pay attention to authentication, error handling, and connection management. When implemented correctly, subscriptions provide a seamless real-time experience that integrates naturally with your existing GraphQL schema.
For a comprehensive overview, read our article on Api Authentication Guide.
For a comprehensive overview, read our article on Api Caching Strategies.