← Back to DevBytes

GraphQL Subscriptions: A Complete Reference Guide

What Are GraphQL Subscriptions?

GraphQL subscriptions provide a way to establish real-time, persistent connections between a client and a GraphQL server. While queries are used to fetch data on demand and mutations are used to modify data, subscriptions allow the server to push updates to clients as soon as specific events occur. They are the GraphQL specification's answer to real-time data delivery.

At their core, subscriptions leverage a persistent transport layer — typically WebSockets — to maintain an open connection. Once a client subscribes to a particular event stream, the server can deliver payloads over that connection whenever the underlying data changes. Unlike polling, where the client repeatedly asks "has anything changed?", subscriptions invert the flow: the server says "something changed, here's the update."

A subscription operation looks structurally similar to a query or mutation in the GraphQL document:

subscription OnNewMessage {
  messageCreated(roomId: "lobby") {
    id
    content
    sender {
      username
      avatarUrl
    }
    createdAt
  }
}

What makes subscriptions unique is the execution model. Rather than returning a single response, the server returns an asynchronous iterator or a stream that yields multiple results over time — one for each event that matches the subscription's selection set.

Why GraphQL Subscriptions Matter

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Modern applications increasingly demand real-time user experiences. Whether it's a chat application, a collaborative document editor, a live dashboard, a sports score tracker, or a financial trading platform, users expect to see changes as they happen, without manual refreshes. GraphQL subscriptions address this need in a way that integrates cleanly with the rest of your GraphQL schema.

Here are the key reasons subscriptions are indispensable:

How GraphQL Subscriptions Work Under the Hood

Understanding the lifecycle of a subscription is crucial for debugging and optimization. Here's the step-by-step flow:

  1. Client initiates a subscription: The client sends a GraphQL document containing a subscription operation over a WebSocket connection (or, in newer transports like graphql-ws, over HTTP with a protocol upgrade).
  2. Server validates and resolves: The server's subscription resolver is invoked. Instead of returning a value directly, it returns an AsyncIterator or a similar reactive stream source.
  3. Server sets up a source event listener: Behind the scenes, the resolver typically subscribes to an internal pub/sub system, an event emitter, a database change stream (like PostgreSQL NOTIFY, MongoDB change streams, or Redis pub/sub), or a message broker like Kafka.
  4. Data flows through the iterator: When an event occurs in the source system, the iterator yields a value. The GraphQL engine applies the subscription's selection set to that value, serializes the result, and writes it to the client's WebSocket.
  5. Client receives updates: The client-side GraphQL library deserializes the payload, merges it into the local cache (if applicable), and notifies the UI layer to re-render affected components.
  6. Termination: The client can unsubscribe by closing the WebSocket connection or sending a specific "complete" message. The server then cleans up the source listener to prevent memory leaks.

The transport protocol matters significantly. The legacy protocol (subscriptions-transport-ws) used by early Apollo Server versions has been superseded by the graphql-ws protocol, which is now the standard. The newer protocol fixes several design flaws, supports bidirectional acknowledgment of subscription starts and stops, and is more resilient to connection drops.

Setting Up a GraphQL Subscription Server

We'll walk through a complete, production-ready setup using Apollo Server 4 with the graphql-ws WebSocket implementation and a Redis-backed pub/sub layer for horizontal scalability. The example models a simple chat application.

Step 1: Define the Schema

First, define your GraphQL schema using the subscription keyword. The subscription field returns a type — typically the same type you'd fetch via a query, which allows clients to reuse fragments seamlessly.

# schema.graphql
type Message {
  id: ID!
  content: String!
  sender: User!
  roomId: String!
  createdAt: String!
}

type User {
  id: ID!
  username: String!
  avatarUrl: String
}

type Query {
  messages(roomId: String!): [Message!]!
  me: User
}

type Mutation {
  sendMessage(roomId: String!, content: String!): Message!
  joinRoom(roomId: String!): Boolean!
  leaveRoom(roomId: String!): Boolean!
}

type Subscription {
  messageCreated(roomId: String!): Message!
  userJoined(roomId: String!): User!
  userLeft(roomId: String!): User!
}

Step 2: Implement a Pub/Sub Abstraction

For a single-server setup, an in-memory EventEmitter works. For production, Redis pub/sub allows multiple server instances to broadcast events across processes. We'll build a generic pub/sub interface so the resolver layer stays decoupled from the transport.

// pubsub.ts
import { Redis } from 'ioredis';

interface PubSubConfig {
  redisUrl?: string;
}

export class PubSub {
  private redisPublisher: Redis | null = null;
  private redisSubscriber: Redis | null = null;
  private localListeners: Map void>> = new Map();

  constructor(config: PubSubConfig = {}) {
    if (config.redisUrl) {
      this.redisPublisher = new Redis(config.redisUrl);
      this.redisSubscriber = new Redis(config.redisUrl);
      this.redisSubscriber.on('message', (channel, message) => {
        const listeners = this.localListeners.get(channel);
        if (listeners) {
          const parsed = JSON.parse(message);
          listeners.forEach(cb => cb(parsed));
        }
      });
    }
  }

  publish(channel: string, payload: any): void {
    const stringPayload = JSON.stringify(payload);
    
    // Deliver locally
    const listeners = this.localListeners.get(channel);
    if (listeners) {
      listeners.forEach(cb => cb(payload));
    }
    
    // Publish to Redis for other server instances
    if (this.redisPublisher) {
      this.redisPublisher.publish(channel, stringPayload);
    }
  }

  subscribe(channel: string, callback: (payload: any) => void): () => void {
    if (!this.localListeners.has(channel)) {
      this.localListeners.set(channel, new Set());
      
      // Subscribe to Redis channel if using Redis
      if (this.redisSubscriber) {
        this.redisSubscriber.subscribe(channel);
      }
    }
    
    this.localListeners.get(channel)!.add(callback);
    
    // Return unsubscribe function
    return () => {
      const listeners = this.localListeners.get(channel);
      if (listeners) {
        listeners.delete(callback);
        if (listeners.size === 0) {
          this.localListeners.delete(channel);
          if (this.redisSubscriber) {
            this.redisSubscriber.unsubscribe(channel);
          }
        }
      }
    };
  }

  asyncIterable(channel: string): AsyncIterator {
    let pushQueue: T[] = [];
    let resolveWait: ((value: IteratorResult) => void) | null = null;
    let completed = false;
    
    const unsubscribe = this.subscribe(channel, (payload: T) => {
      if (resolveWait) {
        resolveWait({ value: payload, done: false });
        resolveWait = null;
      } else {
        pushQueue.push(payload);
      }
    });

    return {
      next: async (): Promise> => {
        if (completed) return { value: undefined, done: true };
        
        if (pushQueue.length > 0) {
          const value = pushQueue.shift()!;
          return { value, done: false };
        }
        
        return new Promise(resolve => {
          resolveWait = resolve;
        });
      },
      return: async (): Promise> => {
        completed = true;
        unsubscribe();
        if (resolveWait) {
          resolveWait({ value: undefined, done: true });
          resolveWait = null;
        }
        return { value: undefined, done: true };
      },
      throw: async (error: any): Promise> => {
        completed = true;
        unsubscribe();
        throw error;
      },
      [Symbol.asyncIterator]() {
        return this;
      }
    };
  }
}

Step 3: Write the Resolvers

Subscription resolvers return an AsyncIterator. The subscribe field on the resolver object is where you set up the event source. The resolve field (optional) can transform the payload before it's sent to the client.

// resolvers.ts
import { PubSub } from './pubsub';

const pubsub = new PubSub({ redisUrl: process.env.REDIS_URL });

const CHAT_ROOM_MESSAGE = 'CHAT_ROOM_MESSAGE';
const CHAT_ROOM_USER_JOINED = 'CHAT_ROOM_USER_JOINED';
const CHAT_ROOM_USER_LEFT = 'CHAT_ROOM_USER_LEFT';

// In-memory store for demonstration
const messagesByRoom: Record = {};
const usersByRoom: Record> = {};

interface Message {
  id: string;
  content: string;
  senderId: string;
  roomId: string;
  createdAt: string;
}

interface User {
  id: string;
  username: string;
  avatarUrl: string | null;
}

const resolvers = {
  Query: {
    messages: (_parent: any, args: { roomId: string }) => {
      return messagesByRoom[args.roomId] || [];
    },
    me: (_parent: any, _args: any, context: { userId: string }) => {
      return getUserById(context.userId);
    }
  },
  
  Mutation: {
    sendMessage: (_parent: any, args: { roomId: string; content: string }, context: { userId: string }) => {
      const message: Message = {
        id: generateId(),
        content: args.content,
        senderId: context.userId,
        roomId: args.roomId,
        createdAt: new Date().toISOString()
      };
      
      if (!messagesByRoom[args.roomId]) {
        messagesByRoom[args.roomId] = [];
      }
      messagesByRoom[args.roomId].push(message);
      
      // Publish event — this triggers subscription deliveries
      pubsub.publish(`${CHAT_ROOM_MESSAGE}:${args.roomId}`, message);
      
      return message;
    },
    
    joinRoom: (_parent: any, args: { roomId: string }, context: { userId: string }) => {
      if (!usersByRoom[args.roomId]) {
        usersByRoom[args.roomId] = new Set();
      }
      const user = getUserById(context.userId);
      usersByRoom[args.roomId].add(context.userId);
      pubsub.publish(`${CHAT_ROOM_USER_JOINED}:${args.roomId}`, user);
      return true;
    },
    
    leaveRoom: (_parent: any, args: { roomId: string }, context: { userId: string }) => {
      if (usersByRoom[args.roomId]) {
        usersByRoom[args.roomId].delete(context.userId);
      }
      const user = getUserById(context.userId);
      pubsub.publish(`${CHAT_ROOM_USER_LEFT}:${args.roomId}`, user);
      return true;
    }
  },

  Message: {
    sender: (parent: Message) => getUserById(parent.senderId)
  },

  Subscription: {
    messageCreated: {
      subscribe: (_parent: any, args: { roomId: string }) => {
        return pubsub.asyncIterable(`${CHAT_ROOM_MESSAGE}:${args.roomId}`);
      },
      resolve: (payload: Message) => payload
    },
    userJoined: {
      subscribe: (_parent: any, args: { roomId: string }) => {
        return pubsub.asyncIterable(`${CHAT_ROOM_USER_JOINED}:${args.roomId}`);
      },
      resolve: (payload: User) => payload
    },
    userLeft: {
      subscribe: (_parent: any, args: { roomId: string }) => {
        return pubsub.asyncIterable(`${CHAT_ROOM_USER_LEFT}:${args.roomId}`);
      },
      resolve: (payload: User) => payload
    }
  }
};

// Helper functions
let idCounter = 0;
function generateId(): string {
  return `msg_${Date.now()}_${++idCounter}`;
}

function getUserById(id: string): User {
  // Mock user lookup
  return { id, username: `user_${id}`, avatarUrl: null };
}

Step 4: Wire Up the Server with WebSocket Support

Apollo Server 4 uses the @apollo/server package. To enable subscriptions, you need to create a WebSocket server using the ws package and the graphql-ws library, then integrate it with your HTTP server.

// server.ts
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import express from 'express';
import http from 'http';
import cors from 'cors';
import { readFileSync } from 'fs';
import { resolvers } from './resolvers';

const typeDefs = readFileSync('./schema.graphql', 'utf-8');

// Create the executable schema once, shared between HTTP and WS servers
const schema = makeExecutableSchema({ typeDefs, resolvers });

const app = express();
const httpServer = http.createServer(app);

// Set up the WebSocket server for subscriptions
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

// Use graphql-ws to handle the subscription protocol
const serverCleanup = useServer({ 
  schema,
  context: async (ctx, _msg, _args) => {
    // Extract auth token from connection params
    const token = ctx.connectionParams?.authorization as string || '';
    const userId = verifyTokenAndGetUserId(token);
    return { userId };
  }
}, wsServer);

// Create Apollo Server instance
const apolloServer = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    {
      async serverWillStart() {
        return {
          async drainServer() {
            serverCleanup.dispose();
          }
        };
      }
    }
  ],
});

await apolloServer.start();

app.use(
  '/graphql',
  cors({
    origin: ['https://your-app-domain.com'],
    credentials: true,
  }),
  express.json(),
  expressMiddleware(apolloServer, {
    context: async ({ req }) => {
      const token = req.headers.authorization || '';
      const userId = verifyTokenAndGetUserId(token.replace('Bearer ', ''));
      return { userId };
    }
  })
);

const PORT = parseInt(process.env.PORT || '4000', 10);
httpServer.listen(PORT, () => {
  console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
  console.log(`🚀 WebSocket subscriptions ready at ws://localhost:${PORT}/graphql`);
});

function verifyTokenAndGetUserId(token: string): string {
  // Verify JWT or session token here
  // Return the authenticated user ID or throw an error
  if (!token) return 'anonymous';
  // ... JWT verification logic ...
  return 'user_123'; // Mock
}

This setup ensures that both HTTP queries/mutations and WebSocket subscriptions are handled on the same path (/graphql), which simplifies client configuration and proxy setup.

Client-Side Subscription Handling

On the client, consuming subscriptions requires a WebSocket-capable GraphQL client. Apollo Client provides first-class support via a split link that routes subscription operations to a WebSocket transport while keeping queries and mutations on HTTP.

Setting Up Apollo Client with Subscriptions

// apollo-client-setup.ts
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

// HTTP link for queries and mutations
const httpLink = new HttpLink({
  uri: 'https://api.example.com/graphql',
  headers: {
    authorization: `Bearer ${getToken()}`,
  },
});

// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
  createClient({
    url: 'wss://api.example.com/graphql',
    connectionParams: () => ({
      authorization: `Bearer ${getToken()}`,
    }),
    retryAttempts: Infinity,
    retryWait: (attempt) => new Promise(resolve => 
      setTimeout(resolve, Math.min(1000 * 2 ** attempt, 30000))
    ),
    on: {
      connected: (socket) => {
        console.log('WebSocket connected');
      },
      closed: () => {
        console.log('WebSocket closed, will retry');
      },
      error: (error) => {
        console.error('WebSocket error:', error);
      }
    }
  })
);

// Split link routes operations based on type
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(),
});

function getToken(): string {
  return localStorage.getItem('auth_token') || '';
}

Using the useSubscription Hook in React

Apollo Client's React integration provides a useSubscription hook that manages the subscription lifecycle automatically. It subscribes when the component mounts and unsubscribes on unmount.

// ChatRoom.tsx
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_CREATED_SUBSCRIPTION = gql`
  subscription OnNewMessage($roomId: String!) {
    messageCreated(roomId: $roomId) {
      id
      content
      sender {
        username
        avatarUrl
      }
      createdAt
    }
  }
`;

const MESSAGE_FRAGMENT = gql`
  fragment MessageFields on Message {
    id
    content
    sender {
      username
      avatarUrl
    }
    createdAt
  }
`;

function ChatRoom({ roomId }: { roomId: string }) {
  const { data, loading, error } = useSubscription(
    MESSAGE_CREATED_SUBSCRIPTION,
    { variables: { roomId } }
  );

  const [messages, setMessages] = useState([]);

  useEffect(() => {
    if (data?.messageCreated) {
      setMessages(prev => [...prev, data.messageCreated]);
    }
  }, [data]);

  if (error) {
    return 
Subscription error: {error.message}
; } return (

Chat Room: {roomId}

{messages.map(msg => (
{msg.sender.username}

{msg.content}

))}
{loading && !data &&

Waiting for first message...

}
); }

Handling Subscriptions in Vanilla JavaScript

If you're not using a framework, you can use the graphql-ws client directly. This gives you full control over connection management.

// vanilla-subscription.ts
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'wss://api.example.com/graphql',
  connectionParams: {
    authorization: `Bearer ${token}`,
  },
});

// Subscribe to a specific stream
const subscription = client.iterate({
  query: `
    subscription OnNewMessage($roomId: String!) {
      messageCreated(roomId: $roomId) {
        id
        content
        sender { username }
        createdAt
      }
    }
  `,
  variables: { roomId: 'lobby' },
});

// Async iterator consumption
(async () => {
  for await (const event of subscription) {
    if (event.data?.messageCreated) {
      console.log('New message:', event.data.messageCreated);
      appendMessageToUI(event.data.messageCreated);
    }
  }
})();

// To unsubscribe later:
// subscription.return();

Authentication and Authorization for Subscriptions

Securing subscriptions requires special attention because WebSocket connections don't carry HTTP headers automatically. The graphql-ws protocol supports passing authentication data via connectionParams, which are sent during the WebSocket handshake.

On the server side, the useServer callback receives these connection parameters. You should validate tokens and attach user identity to the context, just as you would for HTTP requests:

// Server-side auth context for WebSocket
const wsServerCleanup = useServer({
  schema,
  context: async (ctx) => {
    const token = ctx.connectionParams?.authorization as string;
    
    if (!token) {
      // Reject anonymous connections for authenticated subscriptions
      throw new Error('Authentication required');
    }
    
    try {
      const user = await verifyJwt(token.replace('Bearer ', ''));
      return { userId: user.id, roles: user.roles };
    } catch {
      throw new Error('Invalid token');
    }
  },
  // Optional: filter which subscriptions are available
  onSubscribe: async (ctx, msg) => {
    const { userId } = ctx.context as any;
    
    // Check if user is allowed to subscribe to this room
    const roomId = msg.variables?.roomId as string;
    if (roomId && !await isUserMemberOfRoom(userId, roomId)) {
      throw new Error('Not a member of this room');
    }
    
    return ctx;
  },
}, wsServer);

For additional security, consider these measures:

Advanced Patterns and Best Practices

Pattern 1: Subscription Filters and Dynamic Topics

Rather than creating a separate subscription field for every permutation of arguments, design your pub/sub topics to encode filtering criteria. The resolver's subscribe function then creates a channel name that incorporates the arguments:

// Dynamic topic naming
Subscription: {
  documentUpdated: {
    subscribe: (_parent: any, args: { documentId: string; sections?: string[] }) => {
      const channel = `DOCUMENT_UPDATED:${args.documentId}`;
      const iterator = pubsub.asyncIterable(channel);
      
      // If sections filter is provided, wrap the iterator
      if (args.sections?.length) {
        return filterIterator(iterator, (payload: any) => 
          args.sections!.includes(payload.section)
        );
      }
      
      return iterator;
    }
  }
}

// Generic async iterator filter utility
function filterIterator(
  source: AsyncIterator,
  predicate: (value: T) => boolean
): AsyncIterator {
  let filteredQueue: T[] = [];
  let resolveWait: ((result: IteratorResult) => void) | null = null;
  let sourceDone = false;

  const processSource = async () => {
    while (!sourceDone) {
      try {
        const result = await source.next();
        if (result.done) {
          sourceDone = true;
          if (resolveWait) {
            resolveWait({ value: undefined, done: true });
            resolveWait = null;
          }
          break;
        }
        if (predicate(result.value)) {
          if (resolveWait) {
            resolveWait({ value: result.value, done: false });
            resolveWait = null;
          } else {
            filteredQueue.push(result.value);
          }
        }
      } catch (error) {
        if (resolveWait) {
          resolveWait({ value: undefined, done: true });
          resolveWait = null;
        }
        sourceDone = true;
        break;
      }
    }
  };

  processSource();

  return {
    next: async () => {
      if (sourceDone && filteredQueue.length === 0) {
        return { value: undefined, done: true };
      }
      if (filteredQueue.length > 0) {
        return { value: filteredQueue.shift()!, done: false };
      }
      return new Promise(resolve => {
        resolveWait = resolve;
      });
    },
    return: async () => {
      sourceDone = true;
      filteredQueue = [];
      if (resolveWait) {
        resolveWait({ value: undefined, done: true });
        resolveWait = null;
      }
      await source.return?.();
      return { value: undefined, done: true };
    },
    throw: async (error: any) => {
      sourceDone = true;
      await source.throw?.(error);
      throw error;
    },
    [Symbol.asyncIterator]() { return this; }
  };
}

Pattern 2: Live Queries as an Alternative

Some systems implement "live queries" where the server re-executes a query whenever its underlying data changes, streaming incremental results to the client. This is not part of the official GraphQL spec but is gaining traction. Libraries like @graphql-tools/live-query or custom solutions using database change streams can simulate this. However, subscriptions remain the spec-compliant, well-supported approach for most use cases.

Pattern 3: Batching Subscription Updates

If events arrive at high frequency (e.g., cursor movements in a collaborative editor), sending each event individually can overwhelm the network. Implement a debounce or throttle on the server side before publishing:

// Debounced publisher for high-frequency events
class DebouncedPublisher {
  private buffers: Map = new Map();
  private timers: Map = new Map();
  private debounceMs: number;
  private pubsub: PubSub;

  constructor(pubsub: PubSub, debounceMs: number = 50) {
    this.pubsub = pubsub;
    this.debounceMs = debounceMs;
  }

  enqueue(channel: string, payload: any): void {
    if (!this.buffers.has(channel)) {
      this.buffers.set(channel, []);
    }
    this.buffers.get(channel)!.push(payload);

    if (this.timers.has(channel)) {
      clearTimeout(this.timers.get(channel)!);
    }

    this.timers.set(channel, setTimeout(() => {
      const batch = this.buffers.get(channel) || [];
      this.buffers.delete(channel);
      this.timers.delete(channel);
      // Publish the batch as a single event
      this.pubsub.publish(channel, { batch, count: batch.length });
    }, this.debounceMs));
  }
}

Best Practices Summary

Error Handling and Reconnection Strategies

Robust subscription systems must gracefully handle errors at multiple levels: transport errors, subscription-specific errors, and data resolution errors.

Server-Side Error Handling

When an error occurs during subscription execution, the server should send an error payload to the client without necessarily terminating the entire subscription (unless the error is fatal). In graphql-ws, you can emit errors on the iterator:

// Enhanced async iterator with error emission
function resilientAsyncIterator(
  channel: string,
  pubsub: PubSub
): AsyncIterator {
  let errored = false;
  let errorValue: Error | null = null;
  
  const rawIterator = pubsub.asyncIterable(channel);
  
  return {
    next: async () => {
      if (errored) throw errorValue;
      try {
        return await rawIterator.next();
      } catch (err) {
        errored = true;
        errorValue = err instanceof Error ? err : new Error(String(err));
        throw errorValue;
      }
    },
    return: async () => {
      return rawIterator.return!();
    },
    throw: async (error: any) => {
      return rawIterator.throw!(error);
    },
    [Symbol.asyncIterator]() { return this; }
  };
}

Client-Side Reconnection

The graphql-ws client library has built-in retry logic. Configure it to handle transient network failures gracefully. When the connection drops, the client will attempt to reconnect with exponential backoff. Once reconnected, it automatically re-subscribes to all active subscriptions.

// Resilient client configuration
const wsClient = createClient({
  url: 'wss://api.example.com/graphql',
  connectionParams: { authorization: `Bearer ${getToken()}` },
  retryAttempts: Infinity,
  retryWait: (attempt) => {
    // Exponential backoff with jitter
    const base = Math.min(1000 * 2 ** attempt, 30000);
    const jitter = Math.random() * 1000;
    return new Promise(resolve => setTimeout(resolve, base + jitter));
  },
  isFatalConnectionProblem: (error) => {
    // Only treat 4xx auth errors as fatal (don't retry)
    if (error instanceof Error && error.message.includes('401')) {
      return true;
    }
    return false;
  },
  on: {
    connected: () => {
      console.log('Reconnected, subscriptions resumed');
    },
    connecting: () => {
      console.log('Attempting to reconnect...');
    },
    closed: () => {
      console.log('Connection closed');
    },
  },
});

On the application level, you should also handle the case where a subscription fails due to authorization changes (e.g., user logs out). Listen for the error callback on useSubscription and present appropriate UI feedback:

const { data, error } = useSubscription(SUBSCRIPTION_DOC, { variables });
if (error) {
  if (error.message.includes('Not authorized')) {
    return 

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles