GraphQL Subscriptions Deep Dive â Real-Time Data with WebSockets
In this tutorial, you will learn about GraphQL Subscriptions Deep Dive. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL subscriptions provide real-time data push from server to client over Websocket connections, enabling live updates for dashboards, notifications, and collaborative features.
What You'll Learn
You will learn how to implement GraphQL subscriptions with Apollo Server, use Redis Pub/Sub for multi-instance scaling, filter subscription events, authenticate WebSocket connections, and handle reconnection gracefully.
Why Subscriptions Matter
Polling for changes wastes bandwidth and adds latency. With subscriptions, the server pushes updates the instant an event occurs. DodaTech's Durga Antivirus Pro dashboard uses subscriptions to display live threat alerts â when a new zero-day is detected, every connected dashboard updates within milliseconds, enabling security analysts to respond instantly.
sequenceDiagram
participant Client
participant Gateway
participant PubSub
participant DB
Client->>Gateway: WebSocket Connect (auth token)
Gateway-->>Client: Connection established
Client->>Gateway: Subscribe: threatDetected(severity: CRITICAL)
Gateway->>PubSub: AsyncIterator("THREAT_DETECTED")
DB-->>Gateway: New threat written
Gateway->>PubSub: Publish("THREAT_DETECTED", threat)
PubSub-->>Gateway: Event received
Gateway-->>Client: Push: { threatDetected: { name, severity } }
Note over Client,Gateway: Persistent connection
Prerequisites: GraphQL queries and mutations. Apollo Server experience. WebSocket concepts.
Setting Up Subscriptions with Apollo Server
const { ApolloServer, gql, PubSub } = require('apollo-server');
const pubsub = new PubSub();
const THREAT_DETECTED = 'THREAT_DETECTED';
const typeDefs = gql`
type Threat {
id: ID!
name: String!
severity: String!
detectedAt: String!
deviceId: ID!
}
type Query {
threats: [Threat!]!
}
type Mutation {
reportThreat(name: String!, severity: String!, deviceId: ID!): Threat!
}
type Subscription {
threatDetected(deviceId: ID): Threat!
}
`;
const resolvers = {
Query: { threats: () => threats },
Mutation: {
reportThreat: (_, args) => {
const threat = {
id: String(threats.length + 1),
name: args.name,
severity: args.severity,
deviceId: args.deviceId,
detectedAt: new Date().toISOString(),
};
threats.push(threat);
pubsub.publish(THREAT_DETECTED, { threatDetected: threat });
return threat;
},
},
Subscription: {
threatDetected: {
subscribe: (_, { deviceId }) => {
const iterator = pubsub.asyncIterator([THREAT_DETECTED]);
// Filter by deviceId if provided
if (deviceId) {
return {
[Symbol.asyncIterator]() { return this; },
async next() {
while (true) {
const { value, done } = await iterator.next();
if (done) return { done: true };
if (value.threatDetected.deviceId === deviceId) {
return { value, done: false };
}
}
},
};
}
return iterator;
},
},
},
};
Client-Side Subscription
import { ApolloClient, InMemoryCache, gql, split } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
const wsLink = new WebSocketLink({
uri: 'ws://localhost:4000/subscriptions',
options: {
reconnect: true,
connectionParams: { authToken: 'eyJhbGciOiJIUzI1NiIs...' },
},
});
const client = new ApolloClient({ link: wsLink, cache: new InMemoryCache() });
const subscription = client.subscribe({
query: gql`
subscription OnThreatDetected($deviceId: ID) {
threatDetected(deviceId: $deviceId) {
id
name
severity
detectedAt
}
}
`,
variables: { deviceId: 'dev-001' },
}).subscribe({
next({ data }) {
console.log('New threat:', data.threatDetected);
showAlert(data.threatDetected);
},
error(err) {
console.error('Subscription error:', err);
},
});
// Cleanup on unmount
setTimeout(() => subscription.unsubscribe(), 3600000);
Expected console output:
New threat: {
id: "42",
name: "Novel Ransomware Variant",
severity: "critical",
detectedAt: "2026-06-28T14:30:00Z"
}
Redis Pub/Sub for Multi-Instance Scaling
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
const pubsub = new RedisPubSub({
publisher: new Redis({ host: 'redis-cluster', port: 6379 }),
subscriber: new Redis({ host: 'redis-cluster', port: 6379 }),
});
// Works across multiple server instances
// Instance 1 receives a mutation â publishes to Redis
// All instances receive the event â push to their connected clients
Authentication in Subscriptions
const server = new ApolloServer({
typeDefs,
resolvers,
subscriptions: {
path: '/subscriptions',
onConnect: (connectionParams, webSocket) => {
const token = connectionParams.authToken;
if (!token) throw new Error('Authentication required');
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
return { user }; // Attached to context
} catch {
throw new Error('Invalid authentication token');
}
},
onDisconnect: (webSocket, context) => {
console.log('Client disconnected');
},
},
});
withFilter for Declarative Filtering
const { withFilter } = require('graphql-subscriptions');
const resolvers = {
Subscription: {
threatDetected: {
subscribe: withFilter(
() => pubsub.asyncIterator([THREAT_DETECTED]),
(payload, variables) => {
// Return true to include the event, false to filter it out
if (variables.deviceId && variables.severity) {
return payload.threatDetected.deviceId === variables.deviceId
&& payload.threatDetected.severity === variables.severity;
}
if (variables.deviceId) {
return payload.threatDetected.deviceId === variables.deviceId;
}
return true; // No filter â include all
}
),
},
},
};
Common Mistakes
1. Using In-Memory PubSub in Multi-Instance Deployments
In-memory PubSub only works within a single Process. With multiple server instances, a subscription on instance 1 never receives mutations sent to instance 2. Use Redis PubSub in production.
2. Not Handling Reconnection
WebSocket connections drop. Without reconnect: true and reconnection logic, clients silently lose all subscription data after a network interruption.
3. Sending Large Payloads in Subscription Events
Subscription events fire frequently. Sending full object payloads wastes bandwidth. Send minimal data (IDs) and let clients fetch details via queries.
4. Ignoring onDisconnect Cleanup
Accumulated subscriptions from disconnected clients leak memory. The onDisconnect callback is the place to clean up per-client resources.
5. Not Using withFilter for Scalable Filtering
Implementing filter logic in the async Iterator works but is verbose and error-prone. Use withFilter for declarative, testable subscription filters.
Practice Questions
- What transport protocol do GraphQL subscriptions use?
- Why is Redis Pub/Sub needed for production subscriptions?
- How do you authenticate WebSocket connections?
- What does withFilter do?
- How do clients handle subscription reconnection?
Answers:
- WebSocket (ws:// or wss://) over a persistent TCP connection. Apollo Server also supports HTTP long-polling as a fallback.
- With multiple server instances, in-memory PubSub cannot broadcast events across instances. Redis Pub/Sub ensures that when one instance publishes an event, all instances receive it and push to their connected clients.
- Pass the auth token in WebSocket connection parameters and validate it in the
onConnectcallback. Attach the verified user to the context for use in resolvers. withFilteris a utility that wrapsasyncIteratorwith a filter function. It receives the published payload and the subscription arguments, returning true to include the event or false to skip it.- Set
reconnect: truein the WebSocket connection options. On reconnect, re-establish subscriptions. Apollo Client automatically resubscribes after reconnection.
Challenge: Build a real-time threat monitoring system for DodaTech using subscriptions. Include multiple event types (threatDetected, scanCompleted, deviceStatusChanged), per-device filtering, admin-only critical alerts, Redis Pub/Sub, JWT authentication, and a React dashboard that displays live alerts with sound notifications.
Mini Project
Create a complete subscription-based real-time dashboard for Durga Antivirus Pro. Include subscriptions for threat alerts, scan progress, device status changes, and system health metrics. Implement Redis Pub/Sub, JWT auth, reconnection with exponential backoff, and rate-limited event publishing.
What's Next
| Topic | Description |
|---|---|
| Nested Resolvers | Resolver chains and data fetching |
| DataLoader (N+1 Problem) | Batching database queries |
| Error Handling | Error patterns in GraphQL |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro