← Back to DevBytes

Implementing GraphQL Subscriptions: From Theory to Practice

Understanding GraphQL Subscriptions

GraphQL subscriptions represent the third fundamental operation type in the GraphQL specification, sitting alongside queries and mutations. While queries fetch data and mutations modify data, subscriptions are designed for long-lived, real-time data delivery. They allow a client to open a persistent connection to the server and receive automatic updates whenever specific events occur that match the subscription's criteria.

At their core, subscriptions implement the publish-subscribe pattern. A client "subscribes" to a particular topic or event on the server. When the server publishes data to that topic, all subscribed clients receive the update in real time. This is fundamentally different from polling or repeatedly executing queries — the server pushes data to the client only when there is something new to deliver.

The Three Core GraphQL Operations

To understand where subscriptions fit, let's quickly contrast the three operation types:

Subscriptions are defined in your GraphQL schema using the Subscription type, and they must return a single root field (unlike queries which can fetch multiple root fields in one operation). The server uses a PubSub (Publish-Subscribe) engine to manage event channels, and each subscription resolver returns an AsyncIterator that yields values over time.

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 capabilities. Users expect to see changes instantly — whether it's a new message in a chat, a stock price update, or a collaborative document edit. GraphQL subscriptions solve this elegantly by extending the same type-safe, schema-driven approach you already use for queries and mutations into the real-time domain. This means:

Real-World Use Cases

The Transport Layer: WebSockets and SSE

GraphQL subscriptions require a persistent, bidirectional (or at least server-to-client streaming) communication channel. The two primary transport mechanisms are:

The ecosystem has converged on the graphql-ws protocol (maintained by the GraphQL community) as the standard for WebSocket-based GraphQL subscriptions. It replaced the older subscriptions-transport-ws protocol (which was used by Apollo Server v3 and earlier). The new protocol is cleaner, more secure, and handles connection management more robustly. Throughout this tutorial, we'll use graphql-ws.

Server-Side Implementation: A Complete Walkthrough

We'll build a complete subscription-enabled GraphQL server using Apollo Server v4, Express, and the graphql-ws WebSocket transport. The example models a simple real-time chat where clients can post messages and subscribe to new messages as they arrive.

Step 1: Define the Schema with Subscription Types

Your GraphQL schema must include a Subscription type. Each field on this type represents an event that clients can subscribe to. Here's the schema for our chat example:


type Message {
  id: ID!
  content: String!
  author: String!
  createdAt: String!
}

type Query {
  messages: [Message!]!
}

type Mutation {
  postMessage(author: String!, content: String!): Message!
}

type Subscription {
  messageAdded: Message!
}

Notice that the Subscription type defines messageAdded which returns a single Message!. When a mutation creates a new message, the subscription will push that new message to all listening clients.

Step 2: Set Up the PubSub System

A PubSub engine is the event bus that decouples your mutation logic from subscription delivery. The graphql-subscriptions package provides an in-memory PubSub implementation perfect for development and single-server setups:

const { PubSub } = require('graphql-subscriptions');

const pubsub = new PubSub();

// Define event channel names as constants to avoid typos
const MESSAGE_ADDED = 'MESSAGE_ADDED';
const USER_TYPING = 'USER_TYPING';

The PubSub class offers two critical methods:

Step 3: Write Subscription Resolvers

Subscription resolvers differ from query and mutation resolvers. Instead of returning data directly, they return an async iterator (or an object with a subscribe method that returns one). The server then iterates over this source, sending each yielded value to the client:

let messages = [];
let idCounter = 1;

const resolvers = {
  Query: {
    messages: () => messages,
  },

  Mutation: {
    postMessage: (_, { author, content }) => {
      const message = {
        id: String(idCounter++),
        content,
        author,
        createdAt: new Date().toISOString(),
      };
      messages.push(message);

      // Publish to the PubSub channel — this triggers the subscription
      pubsub.publish(MESSAGE_ADDED, { messageAdded: message });

      return message;
    },
  },

  Subscription: {
    messageAdded: {
      // The subscribe function returns an AsyncIterator
      subscribe: () => pubsub.asyncIterator([MESSAGE_ADDED]),
    },
  },
};

Key points about the subscription resolver:

Step 4: Wire Up Apollo Server with WebSocket Transport

Apollo Server v4 does not handle WebSocket connections internally. Instead, you create an HTTP server, attach Apollo as middleware for HTTP requests, and set up a separate WebSocket server on the same HTTP server instance for subscription traffic. Here is the complete server implementation:

const express = require('express');
const { createServer } = require('http');
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { ApolloServerPluginDrainHttpServer } = require('@apollo/server/plugin/drainHttpServer');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { PubSub } = require('graphql-subscriptions');
const cors = require('cors');
const bodyParser = require('body-parser');

// ---------- PubSub and Data ----------
const pubsub = new PubSub();
const MESSAGE_ADDED = 'MESSAGE_ADDED';

let messages = [];
let idCounter = 1;

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

  type Query {
    messages: [Message!]!
  }

  type Mutation {
    postMessage(author: String!, content: String!): Message!
  }

  type Subscription {
    messageAdded: Message!
  }
`;

// ---------- Resolvers ----------
const resolvers = {
  Query: {
    messages: () => messages,
  },

  Mutation: {
    postMessage: (_, { author, content }) => {
      const message = {
        id: String(idCounter++),
        content,
        author,
        createdAt: new Date().toISOString(),
      };
      messages.push(message);
      pubsub.publish(MESSAGE_ADDED, { messageAdded: message });
      return message;
    },
  },

  Subscription: {
    messageAdded: {
      subscribe: () => pubsub.asyncIterator([MESSAGE_ADDED]),
    },
  },
};

// ---------- Main Server Setup ----------
async function main() {
  const app = express();
  const httpServer = createServer(app);

  // Create the WebSocket server for subscriptions
  const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql',  // same path as the HTTP endpoint
  });

  // Build the executable schema
  const schema = makeExecutableSchema({ typeDefs, resolvers });

  // Pass the schema to the WebSocket server handler
  const serverCleanup = useServer({ schema }, wsServer);

  //

🚀 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