DocumentationSubscriptions

Subscriptions

Subscriptions allow a GraphQL server to push updates to clients in real time. Unlike queries and mutations, which use a request/response model, subscriptions maintain a long-lived connection between the client and server to stream data as events occur. This is useful for building features like chat apps, live dashboards, or collaborative tools that require real-time updates.

This guide covers how to implement subscriptions in GraphQL.js, when to use them, and what to consider in production environments.

What is a subscription?

A subscription is a GraphQL operation that delivers ongoing results to the client when a specific event happens. Unlike queries or mutations, which return a single response, a subscription delivers data over time through a persistent connection.

GraphQL.js implements the subscription execution algorithm, but it’s up to you to connect it to your event system and transport layer. Common transport options include WebSockets and Server-Sent Events (SSE). For more context on the advantages of each approach, consider a general comparison of WebSockets and SSE as well as GraphQL Yoga’s breakdown with respect to GraphQL.

How execution works

The core of subscription execution in GraphQL.js is the subscribe function. It starts the subscription and, when successful, returns an async iterable of execution results instead of a single response:

import { subscribe, parse } from 'graphql';
import schema from './schema.js';
 
const document = parse(`
  subscription {
    messageSent
  }
`);
 
const result = await subscribe({ schema, document });
 
if (isAsyncIterableObject(result)) {
  for await (const payload of result) {
    console.log(payload);
  }
} else {
  console.error(result.errors);
}
 
function isAsyncIterableObject(value) {
  return value != null && typeof value[Symbol.asyncIterator] === 'function';
}

Each time your application publishes a new messageSent event, the iterator emits a new result. It is up to your transport layer to manage the connection and forward these updates to the client.

Why subscribe() is separate from execute()

GraphQL.js keeps execute() and subscribe() as separate entry points for historical and behavioral reasons. execute() is the older API for running one execution and producing a single result. Subscriptions were added as a separate GraphQL algorithm because starting a subscription has to resolve a source event stream before executing the selection set for each event.

The return type reflects that split. execute() returns one ExecutionResult, or a promise for one. subscribe() returns an ExecutionResult when the subscription cannot start, or an async stream of ExecutionResult values when it can. In GraphQL.js v17, that return is a PromiseOrValue. It contains either an ExecutionResult or an AsyncGenerator<ExecutionResult, void, void>, and await subscribe(args) still works whether the implementation completes synchronously or asynchronously.

When to use subscriptions

Subscriptions are helpful when your application needs to respond to real-time events. For example:

  • Receiving new messages in a chat
  • Updating a live feed or activity stream
  • Displaying presence indicators (e.g., “user is online”)
  • Reflecting real-time price changes

If real-time delivery isn’t essential, consider using polling with queries instead. Subscriptions require more infrastructure and introduce additional complexity, especially around scaling and connection management.

Implementing subscriptions in GraphQL.js

GraphQL.js supports subscription execution, but you’re responsible for setting up the transport and event system. At a minimum, you’ll need:

  • A Subscription root type in your schema
  • A subscribe resolver that returns an AsyncIterable
  • An event-publishing mechanism
  • A transport layer to maintain the connection

The following examples use a small in-memory pub/sub helper to set up an event system for local development.

Install dependencies

Start by installing the necessary packages:

npm install graphql

To serve subscriptions over a network, you’ll also need a transport implementation. Two common options are graphql-ws, a community-maintained WebSocket library, and graphql-sse, a community-maintained Server-Sent Events library. This guide focuses on schema-level implementation.

Set up a pub/sub instance

In production, subscriptions are typically backed by a robust pub/sub system that can span processes and servers. For this demonstration, you can use an in-memory pub/sub system of your choice, or an implementation such as the following:

import { EventEmitter, on } from 'node:events';
 
export class InMemoryPubSub<T extends Record<string, unknown>> {
  private emitter = new EventEmitter();
 
  publish<K extends keyof T & string>(topic: K, value: T[K]): boolean {
    return this.emitter.emit(topic, value);
  }
 
  async *subscribe<K extends keyof T & string>(
    topic: K,
    signal?: AbortSignal,
  ): AsyncGenerator<T[K], void, unknown> {
    for await (const [value] of on(this.emitter, topic, { signal })) {
      yield value as T[K];
    }
  }
}

Then create a typed pub/sub instance for the subscription events in your schema:

type PubSubEvents = {
  MESSAGE_SENT: { messageSent: string };
};
 
const pubsub = new InMemoryPubSub<PubSubEvents>();

This pubsub object provides publish and subscribe methods, allowing you to broadcast and listen for events.

Define a subscription type

Next, define your Subscription root type:

import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
 
const QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    noop: { type: GraphQLString },
  },
});
 
const SubscriptionType = new GraphQLObjectType({
  name: 'Subscription',
  fields: {
    messageSent: {
      type: GraphQLString,
      subscribe: () => pubsub.subscribe('MESSAGE_SENT'),
    },
  },
});
 
const schema = new GraphQLSchema({
  query: QueryType,
  subscription: SubscriptionType,
});

This schema defines a messageSent field that listens for the MESSAGE_SENT event and returns a string.

Publish events

You can trigger a subscription event from any part of your application using the publish method:

pubsub.publish('MESSAGE_SENT', { messageSent: 'Hello world!' });

Clients subscribed to the messageSent field will receive this message.

Use your schema

A client can then initiate a subscription like this:

subscription {
  messageSent
}

Whenever your server publishes a MESSAGE_SENT event, clients subscribed to messageSent will receive the updated value over their active connection.

Planning for production

The in-memory pub/sub helper used in this example is suitable for development only. It does not support multiple server instances or distributed environments.

For production, consider using a more robust event system such as:

  • Redis Pub/Sub
  • Message brokers like Kafka or NATS
  • Custom implementations with persistent queues or durable event storage

Subscriptions also require careful handling of connection limits, authentication, rate limiting, and network reliability. These responsibilities fall to your transport layer and infrastructure.