Skip to content

GraphQL Subscriptions Deep Dive — Real-Time Data with WebSockets

DodaTech Updated 2026-06-28 6 min read

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
â„šī¸ Info

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

  1. What transport protocol do GraphQL subscriptions use?
  2. Why is Redis Pub/Sub needed for production subscriptions?
  3. How do you authenticate WebSocket connections?
  4. What does withFilter do?
  5. How do clients handle subscription reconnection?

Answers:

  1. WebSocket (ws:// or wss://) over a persistent TCP connection. Apollo Server also supports HTTP long-polling as a fallback.
  2. 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.
  3. Pass the auth token in WebSocket connection parameters and validate it in the onConnect callback. Attach the verified user to the context for use in resolvers.
  4. withFilter is a utility that wraps asyncIterator with a filter function. It receives the published payload and the subscription arguments, returning true to include the event or false to skip it.
  5. Set reconnect: true in 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
âŦ… Directives Guide
➡ Nested Resolvers

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro