Skip to main content

GraphQL Subscriptions: WebSocket & Server-Sent Events Guide

GraphQL subscriptions enable real-time data push from server to client. This guide covers when to use subscriptions, how to implement WebSocket and SSE transports, and how to scale across multiple server instances using Redis pub/sub.

โœ“ graphql-ws WebSocket protocol โœ“ SSE with graphql-sse โœ“ Apollo Server setup โœ“ Redis pub/sub at scale

Subscription Schema & Resolver

Define subscriptions in the SDL and implement resolvers using AsyncIterator. The PubSub instance manages publish/subscribe internally:

import { PubSub } from 'graphql-subscriptions';
import { gql } from 'graphql-tag';

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

const typeDefs = gql`
  type Message {
    id: ID!
    content: String!
    author: String!
    createdAt: String!
  }

  type Query { messages(channelId: ID!): [Message!]! }

  type Mutation {
    sendMessage(channelId: ID!, content: String!, author: String!): Message!
  }

  type Subscription {
    messageAdded(channelId: ID!): Message!
  }
`;

const resolvers = {
  Mutation: {
    sendMessage: async (_: unknown, args: any) => {
      const message = await db.messages.create(args);
      // Publish to all subscribers of this channel
      await pubsub.publish(`${MESSAGE_ADDED}.${args.channelId}`, {
        messageAdded: message,
      });
      return message;
    },
  },
  Subscription: {
    messageAdded: {
      // AsyncIterator yields events when published
      subscribe: (_: unknown, { channelId }: { channelId: string }) =>
        pubsub.asyncIterator(`${MESSAGE_ADDED}.${channelId}`),
    },
  },
};

Apollo Server with graphql-ws (WebSocket)

Apollo Server 4 requires a separate WebSocket server for subscriptions. Use graphql-ws to handle the WebSocket handshake alongside your HTTP server:

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { useServer } from 'graphql-ws/lib/use/ws';
import { WebSocketServer } from 'ws';
import express from 'express';
import http from 'http';

const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();
const httpServer = http.createServer(app);

// WebSocket server โ€” handles subscriptions
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

const serverCleanup = useServer({
  schema,
  onConnect: async (ctx) => {
    // Authenticate during WS handshake
    const token = ctx.connectionParams?.authToken as string;
    if (!token) throw new Error('Missing auth token');
    return { user: await validateToken(token) };
  },
}, wsServer);

// HTTP server โ€” handles queries & mutations
const apolloServer = new ApolloServer({
  schema,
  plugins: [
    { async serverWillStart() {
      return { async drainServer() { await serverCleanup.dispose(); } };
    }},
  ],
});

await apolloServer.start();
app.use('/graphql', expressMiddleware(apolloServer));
httpServer.listen(4000);

Scaling with Redis Pub/Sub

Replace the in-memory PubSub with graphql-redis-subscriptions to broadcast events across all server instances:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

// Two Redis clients: one for publish, one for subscribe
const pubsub = new RedisPubSub({
  publisher: new Redis({ host: 'redis', port: 6379 }),
  subscriber: new Redis({ host: 'redis', port: 6379 }),
});

// Drop-in replacement for the in-memory PubSub
// All server instances share the same Redis channel
// Instance A publishes โ†’ Redis broadcasts โ†’ Instance B/C push to their WS clients

// Use pattern subscriptions for dynamic channels
const asyncIterator = pubsub.asyncIterator(
  `MESSAGE_ADDED.${channelId}`  // per-channel topic
);

// With graphql-subscriptions-filter for server-side filtering:
import { withFilter } from 'graphql-subscriptions';

const resolvers = {
  Subscription: {
    messageAdded: {
      subscribe: withFilter(
        (_: any, { channelId }: any) => pubsub.asyncIterator('MESSAGE_ADDED'),
        (payload: any, variables: any) => payload.messageAdded.channelId === variables.channelId,
      ),
    },
  },
};

WebSocket vs SSE vs Polling for Subscriptions

Feature WebSocket Server-Sent Events Polling
Direction Full-duplex Server โ†’ Client only Client-initiated
Protocol ws:// / wss:// HTTP (text/event-stream) HTTP GET/POST
Proxy friendly Sometimes tricky Works everywhere Works everywhere
Auto-reconnect Manual Built-in browser N/A
Latency Real-time Real-time Interval-based
Best for Chat, collaborative Notifications, feeds Low-frequency updates
Share this tool: X / Twitter LinkedIn
โœ“ 60,000+ developers use DevPlaybook โ˜…โ˜…โ˜…โ˜…โ˜… 4.8 / 5 on Gumroad โœ“ MIT licensed ยท Instant download โœ“ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • โšก AI Code Review, Doc Generator & SQL Builder
  • โšก All premium templates & early access
  • โšก Member discounts on Gumroad products
Start Free Trial โ†’

$9/mo after trial ยท Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products โ€” grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro โ€” $79 โ†’

MIT licensed ยท Instant download ยท No subscription

See what's included โ†’ Browse all deals โ†’

Frequently Asked Questions

When should I use GraphQL subscriptions vs polling?▼

Use subscriptions when you need real-time push updates โ€” chat messages, live notifications, collaborative editing cursors, or order status changes. Use polling when the update frequency is low (every 30+ seconds), when real-time is not critical, or when infrastructure complexity is a concern. Polling is stateless and simpler to scale; subscriptions require persistent connections and stateful infrastructure (load balancers with sticky sessions or a pub/sub broker like Redis).

What is the graphql-ws protocol?▼

graphql-ws is the modern WebSocket subprotocol for GraphQL subscriptions (protocol identifier: graphql-transport-ws). It replaced the older subscriptions-transport-ws package which is no longer maintained. It handles connection initialization, ping/pong keepalives, subscription multiplexing over a single WebSocket, and graceful error handling. Apollo Server 4+ uses graphql-ws by default.

What are Server-Sent Events (SSE) and when should I use them over WebSockets?▼

Server-Sent Events (SSE) is an HTTP-based protocol for one-way server-to-client streaming. Unlike WebSockets (full-duplex), SSE is unidirectional โ€” only the server sends events. SSE works over standard HTTP/1.1 and HTTP/2, passes through proxies and firewalls more easily, and auto-reconnects natively in browsers. Use SSE for GraphQL subscriptions when you only need server-push (no client-initiated messages) and want simpler infrastructure. The graphql-sse package implements this.

How do I scale GraphQL subscriptions across multiple server instances?▼

Each server instance maintains its own set of WebSocket connections. When an event is published, you need all instances to be notified so they can push to their connected clients. This requires a pub/sub broker. The most common approach is Redis pub/sub: the publishing instance publishes to a Redis channel, all server instances subscribe to that channel and forward messages to their connected clients. graphql-redis-subscriptions implements this pattern with Apollo Server's PubSub interface.

Can I use subscriptions with Apollo Client?▼

Yes. Apollo Client has built-in subscription support via the useSubscription hook and subscribeToMore function on queries. You configure it with a WebSocketLink (using graphql-ws) in addition to your HttpLink for queries and mutations. A common pattern is to split the link: send subscription operations to WebSocketLink and everything else to HttpLink using Apollo's split() utility.

What are the security considerations for subscriptions?▼

Authenticate WebSocket connections during the connection_init message (the initial handshake), not at the HTTP upgrade step. Send the auth token in the connectionParams field and validate it in your onConnect server handler. Also implement authorization per-subscription in the resolver to ensure users only receive events for resources they have access to. Rate-limit the number of active subscriptions per user to prevent resource exhaustion.