Learn GraphQL - Real-Time Data with Subscriptions
Episode 16 of 51

Learn GraphQL - Real-Time Data with Subscriptions

Episode 16 builds real-time features with subscriptions: the subscription versus polling concept, WebSocket transport with graphql-ws, setting up subscriptions in Apollo Server 4, the PubSub pattern with Redis, per-connection subscription security, and client integration with useSubscription.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

So far every GraphQL operation has been request-response: the client asks, the server answers, done. Episode 16 introduces a third mode: subscriptions, where the server "pushes" data to the client when an event happens — without the client having to ask repeatedly. We'll cover the subscription concept, WebSocket transport with the graphql-ws library, setting up subscriptions in Apollo Server 4, the PubSub pattern including a Redis implementation for production, per-connection security, and client integration with Apollo Client.

Subscription Fundamentals

What Are Subscriptions

A subscription is a GraphQL operation that maintains an active connection. Every time a watched event happens, the server sends the latest payload. Classic use cases: real-time chat, notifications, and live dashboard updates.

Comparison with the alternatives:

  • Polling: the client asks periodically — simple but wasteful and delayed.
  • Webhooks: the server sends to a specific endpoint — one-way, needs a public endpoint.
  • Subscription: a permanent two-way connection — right for frequent live updates.

Transport Protocols

WebSocket and graphql-ws

Subscriptions generally run over WebSocket. The modern standard library is graphql-ws, which replaces the deprecated subscriptions-transport-ws. graphql-ws supports more robust connections, including ping/pong handling and error recovery; install it with npm install graphql-ws ws.

There's also the Server-Sent Events (SSE) alternative, which is one-way (server to client) and runs over plain HTTP — simpler for one-way notifications, no WebSocket needed. For chat and two-way collaboration, WebSocket remains the primary choice.

Server Setup for Subscriptions

Subscriptions in Apollo Server 4

Apollo Server 4 handles subscriptions in two ways: via the ApolloServerPluginSubscriptionCallback plugin (HTTP callbacks) or via a standalone WebSocket with graphql-ws. The WebSocket approach:

JSWebSocket server for subscriptions
import { useServer } from "graphql-ws/lib/use/ws";
import { WebSocketServer } from "ws";
import { createServer } from "http";
 
const httpServer = createServer();
const wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" });
 
useServer(
  { schema: server.schema, context: async (ctx) => ({ user: authenticate(ctx) }) },
  wsServer
);
 
httpServer.listen(4001, () => console.log("WS di port 4001"));

Subscriptions are exposed on a separate WebSocket endpoint (/graphql), while queries and mutations still run over HTTP. The schema:

Subscription schema
type Subscription {
  messageAdded(roomId: ID!): Message!
  notificationReceived: Notification!
}

Subscription Resolvers

A subscription resolver has two parts: subscribe (returns an event iterator) and resolve (shapes the payload):

JSSubscription resolver
Subscription: {
  messageAdded: {
    subscribe: (_, args, ctx) =>
      ctx.pubsub.asyncIterator(["MESSAGE_ADDED", args.roomId].join(":")),
    resolve: (payload) => payload.message,
  },
},

The PubSub Pattern

Publishing and Responding

PubSub (publish-subscribe) separates event publishers from subscribers. Mutations publish events, subscriptions catch them:

JSPublish from a mutation
import { PubSub } from "graphql-subscriptions";
 
const pubsub = new PubSub();
 
async function addMessage(_, args, ctx) {
  const message = await ctx.db.messages.create(args.input);
  await pubsub.publish("MESSAGE_ADDED:general", { message });
  return message;
}

In-memory PubSub is fine for development, but doesn't work across server instances. If you run many instances (episode 34), an event published on instance A won't reach clients connected to instance B. The solution is a PubSub backed by a shared broker.

Redis PubSub for Production

Redis PubSub is the standard for distributed subscriptions; install it with npm install graphql-redis-subscriptions ioredis:

JSRedis PubSub
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";
 
const pubsub = new RedisPubSub({
  publisher: new Redis(process.env.REDIS_URL),
  subscriber: new Redis(process.env.REDIS_URL),
});

All server instances listen to the same Redis channel, so events are distributed to every client across every instance. Redis PubSub will be revisited when we scale WebSockets in episode 34.

Subscription Security

Per-Connection Authentication

WebSocket connections are long-lived, so identity validation happens once when the connection is established — via the connectionParams the client sends:

JSAuthentication on the WS connection
useServer(
  {
    schema,
    onConnect: (ctx) => {
      const token = ctx.connectionParams?.token;
      if (!token) throw new Error("Autentikasi gagal");
      ctx.user = verifyToken(token);
    },
    context: (ctx) => ({ user: ctx.user }),
  },
  wsServer
);

Beyond authentication, also apply authorization to subscription data (episode 14): make sure users only receive events they're allowed to see, for example by filtering topics based on room membership.

Client Integration

useSubscription in Apollo Client

On the client side, Apollo Client provides the useSubscription hook:

JSThe useSubscription hook
import { useSubscription, gql } from "@apollo/client";
 
const MESSAGE_ADDED = gql`
  subscription OnMessageAdded($roomId: ID!) {
    messageAdded(roomId: $roomId) {
      id
      content
      author { username }
    }
  }
`;
 
function ChatRoom({ roomId }) {
  const { data, loading, error } = useSubscription(MESSAGE_ADDED, {
    variables: { roomId },
  });
  if (loading) return <p>Menghubungkan...</p>;
  return <p>{data?.messageAdded.content}</p>;
}

This hook handles the whole lifecycle: establishing the connection, sending variables, receiving payloads, and recovering when the connection drops. The client WebSocket setup will be covered fully in episode 25.

Conclusion

Key takeaways:

  • Subscriptions push real-time data; polling and webhooks each have their limitations.
  • graphql-ws over WebSocket is the modern transport for subscriptions.
  • A subscription resolver consists of subscribe (an event iterator) and resolve (the payload).
  • In-memory PubSub is only for development; Redis PubSub for multi-instance production.
  • Validate the token in onConnect for per-connection security.
  • Apollo Client's useSubscription handles the connection lifecycle automatically.

In the next episode, episode 17, you'll learn about file upload handling — the GraphQL Upload specification with multipart requests, implementing graphql-upload, stream processing with type and size validation, integrating storage like AWS S3 and Cloudinary, and file upload security. User media will be able to flow into your API!

Learn GraphQL - Real-Time Data with Subscriptions | Learn GraphQL