GraphQL Subscriptions: Real-Time Data Made Simple

Build live updates, chat apps, and real-time features with GraphQL subscriptions

Published: January 2025 • 14 min read

GraphQL subscriptions enable real-time communication between your server and clients. While queries fetch data once and mutations modify it, subscriptions create a continuous connection that pushes updates to clients as they happen.

This guide covers everything from basic subscription setup to production-ready implementations with WebSockets. Use our GraphQL formatter to beautify your subscription queries and schema validator to check your schemas.

What Are GraphQL Subscriptions?

Subscriptions are the third operation type in GraphQL (alongside queries and mutations). They allow clients to:

  • Receive real-time updates when data changes on the server
  • Maintain a persistent connection via WebSockets
  • Get notified of events like new messages, updates, or deletions
  • Build reactive UIs that update automatically without polling

Query

Request-Response
One-time fetch

Mutation

Request-Response
Modify data once

Subscription

Persistent Connection
Continuous updates

How Subscriptions Work

The Flow

1
Client subscribes - Opens a WebSocket connection and sends a subscription query
2
Connection stays open - Unlike HTTP requests, the connection persists
3
Events trigger updates - Server pushes data when relevant events occur
4
Client receives updates - UI updates automatically with new data

Transport Protocol: WebSockets

Subscriptions typically use WebSockets for bi-directional, full-duplex communication:

// Traditional HTTP
Client → Server: GET /api/messages
Server → Client: [messages]
// Connection closes

// WebSocket for Subscriptions
Client ↔ Server: Persistent connection established
Server → Client: New message arrives (pushed automatically)
Server → Client: Another message arrives
Server → Client: Message updated
// Connection stays open until closed

Your First Subscription

Schema Definition

type Subscription {
  messageAdded(chatId: ID!): Message!
}

type Message {
  id: ID!
  text: String!
  author: User!
  createdAt: String!
}

type User {
  id: ID!
  name: String!
  avatar: String
}

Client Subscription Query

subscription OnMessageAdded($chatId: ID!) {
  messageAdded(chatId: $chatId) {
    id
    text
    author {
      name
      avatar
    }
    createdAt
  }
}

# Variables
{
  "chatId": "chat-123"
}

This subscription will receive updates whenever a message is added to chat-123

Server Implementation

Apollo Server Example

const { ApolloServer, gql, PubSub } = require('apollo-server');

const pubsub = new PubSub();
const MESSAGE_ADDED = 'MESSAGE_ADDED';

// Schema
const typeDefs = gql`
  type Subscription {
    messageAdded(chatId: ID!): Message!
  }

  type Message {
    id: ID!
    text: String!
    chatId: ID!
    author: User!
    createdAt: String!
  }

  type Mutation {
    sendMessage(chatId: ID!, text: String!): Message!
  }
`;

// Resolvers
const resolvers = {
  Mutation: {
    sendMessage: (parent, { chatId, text }, context) => {
      const message = {
        id: String(Date.now()),
        text,
        chatId,
        author: context.user,
        createdAt: new Date().toISOString()
      };

      // Publish the message to subscribers
      pubsub.publish(MESSAGE_ADDED, { 
        messageAdded: message 
      });

      return message;
    }
  },

  Subscription: {
    messageAdded: {
      subscribe: (parent, { chatId }) => {
        // Return an async iterator that filters by chatId
        return pubsub.asyncIterator([MESSAGE_ADDED]);
      },
      resolve: (payload, { chatId }) => {
        // Only send if message is for this chat
        if (payload.messageAdded.chatId === chatId) {
          return payload.messageAdded;
        }
        return null;
      }
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  subscriptions: {
    path: '/subscriptions',
  }
});

server.listen().then(({ url, subscriptionsUrl }) => {
  console.log(`Server ready at ${url}`);
  console.log(`Subscriptions ready at ${subscriptionsUrl}`);
});

Key Point: The PubSub pattern decouples event publishers (mutations) from subscribers. When data changes, publish an event, and all active subscriptions receive it.

Client Implementation

Apollo Client Setup

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

// HTTP connection for queries and mutations
const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql'
});

// WebSocket connection for subscriptions
const wsLink = new GraphQLWsLink(createClient({
  url: 'ws://localhost:4000/subscriptions'
}));

// Split traffic based on operation type
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,      // Use WebSocket for subscriptions
  httpLink,    // Use HTTP for queries and mutations
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
});

React Component Example

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

const MESSAGE_SUBSCRIPTION = gql`
  subscription OnMessageAdded($chatId: ID!) {
    messageAdded(chatId: $chatId) {
      id
      text
      author {
        name
        avatar
      }
      createdAt
    }
  }
`;

function ChatRoom({ chatId }) {
  const [messages, setMessages] = useState([]);

  const { data, loading, error } = useSubscription(
    MESSAGE_SUBSCRIPTION,
    { variables: { chatId } }
  );

  useEffect(() => {
    if (data) {
      // Add new message to the list
      setMessages(prev => [...prev, data.messageAdded]);
    }
  }, [data]);

  if (loading) return <p>Connecting...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          <strong>{message.author.name}:</strong> {message.text}
        </div>
      ))}
    </div>
  );
}

Common Subscription Use Cases

1. Chat Applications

Real-time messaging with typing indicators and read receipts:

type Subscription {
  messageAdded(chatId: ID!): Message!
  userTyping(chatId: ID!): User!
  messageRead(chatId: ID!): ReadReceipt!
}

2. Live Dashboards

Analytics, metrics, and monitoring updates:

type Subscription {
  metricsUpdated: Metrics!
  alertTriggered: Alert!
  systemStatus: Status!
}

3. Collaborative Editing

Google Docs-style real-time collaboration:

type Subscription {
  documentUpdated(docId: ID!): Document!
  cursorMoved(docId: ID!): CursorPosition!
  userJoined(docId: ID!): User!
  userLeft(docId: ID!): User!
}

4. Social Media Feeds

New posts, likes, and comments in real-time:

type Subscription {
  postAdded: Post!
  postLiked(postId: ID!): Like!
  commentAdded(postId: ID!): Comment!
}

5. Gaming & Multiplayer

Game state, player moves, and events:

type Subscription {
  gameStateChanged(gameId: ID!): GameState!
  playerMoved(gameId: ID!): PlayerPosition!
  gameEvent(gameId: ID!): GameEvent!
}

6. E-commerce & Orders

Order status, inventory, and pricing updates:

type Subscription {
  orderStatusChanged(userId: ID!): Order!
  productPriceChanged(productId: ID!): Product!
  stockUpdated(productId: ID!): Stock!
}

Subscription Best Practices

✅ Use Filters

Filter events server-side to reduce unnecessary data sent to clients.

✅ Implement Authentication

Verify user permissions before establishing subscription connections.

✅ Handle Reconnections

Implement reconnection logic for when WebSocket connections drop.

✅ Rate Limit

Protect servers from subscription abuse with rate limiting.

✅ Use Redis PubSub

For scaled deployments, use Redis instead of in-memory PubSub.

✅ Monitor Connections

Track active subscriptions and connection health.

❌ Don't Over-Subscribe

Too many subscriptions can overwhelm clients and servers.

❌ Avoid Heavy Payloads

Keep subscription data lightweight for better performance.

Scaling Subscriptions in Production

Redis PubSub for Multi-Server Setup

When running multiple server instances, use Redis to share subscription events:

const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');

const options = {
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
  retryStrategy: times => Math.min(times * 50, 2000)
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options)
});

// Now all server instances share the same PubSub

Connection Limits

Set reasonable limits to protect your infrastructure:

  • • Max connections per user: 5-10
  • • Max subscriptions per connection: 10-20
  • • Idle connection timeout: 5-10 minutes
  • • Heartbeat/ping interval: 30 seconds

GraphQL Development Tools

Related GraphQL Guides

External Resources

Official Documentation

Libraries & Tools