GraphQL Subscriptions are the most surprising way to add real-time data to your applications, not because they’re new, but because they elegantly sidestep the typical complexities of websockets.

Imagine a live stock ticker, a chat application, or a collaborative document editor. These all rely on pushing data from the server to the client as it changes, without the client constantly polling. Traditionally, this meant managing websockets directly, a protocol that, while powerful, can be a bear to scale and integrate with existing HTTP-based APIs. GraphQL Subscriptions offer a way to achieve this real-time behavior using the familiar GraphQL query language, but for events rather than data fetching.

Let’s see it in action with a simple example. We’ll set up a basic GraphQL server that can broadcast messages.

First, the server-side setup. We’ll use Apollo Server, a popular choice.

// server.js
import { ApolloServer, gql } from 'apollo-server';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const typeDefs = gql`
  type Message {
    id: ID!
    text: String!
  }

  type Query {
    hello: String!
  }

  type Mutation {
    sendMessage(text: String!): Message!
  }

  type Subscription {
    messageAdded: Message!
  }
`;

const resolvers = {
  Query: {
    hello: () => 'Hello world!',
  },
  Mutation: {
    sendMessage: (_, { text }) => {
      const newMessage = { id: Date.now().toString(), text };
      pubsub.publish('MESSAGE_ADDED', newMessage); // Publish the event
      return newMessage;
    },
  },
  Subscription: {
    messageAdded: {
      subscribe: () => pubsub.asyncIterator('MESSAGE_ADDED'), // Subscribe to the event
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

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

Here’s the magic: pubsub.publish('MESSAGE_ADDED', newMessage) fires an event, and pubsub.asyncIterator('MESSAGE_ADDED') on the subscription side listens for it. Apollo Server, behind the scenes, handles the underlying transport (often websockets, but can be others) so you don’t have to.

Now, on the client side, using Apollo Client:

// client.js
import { ApolloClient, InMemoryCache, gql, split, HttpLink } from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql', // Your GraphQL HTTP endpoint
});

const wsLink = new WebSocketLink({
  uri: `ws://localhost:4000/graphql`, // Your GraphQL WebSocket endpoint
  options: {
    reconnect: true,
  },
});

// Use split to route queries/mutations to HTTP and subscriptions to WebSocket
const link = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

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

// Subscription query
const MESSAGE_ADDED_SUBSCRIPTION = gql`
  subscription OnMessageAdded {
    messageAdded {
      id
      text
    }
  }
`;

client.subscribe({
  query: MESSAGE_ADDED_SUBSCRIPTION,
}).subscribe({
  next(data) {
    console.log('Received message:', data.data.messageAdded);
  },
  error(err) { console.error('Subscription error:', err); },
});

// Example of sending a message (mutation)
const SEND_MESSAGE_MUTATION = gql`
  mutation SendMessage($text: String!) {
    sendMessage(text: $text) {
      id
      text
    }
  }
`;

client.mutate({
  mutation: SEND_MESSAGE_MUTATION,
  variables: { text: 'Hello from client!' },
}).then(result => console.log('Message sent:', result.data.sendMessage));

The split function is key here. It inspects each operation and directs it to either the httpLink (for queries and mutations) or the wsLink (for subscriptions). This allows you to use a single GraphQL endpoint that intelligently handles different operation types.

The problem GraphQL Subscriptions solve is the need for bidirectional communication without the overhead of managing raw websockets. Instead of building a separate websocket server, you define your real-time events within your GraphQL schema. The server broadcasts these events, and clients subscribe to them using familiar GraphQL syntax. The PubSub mechanism, provided by libraries like graphql-subscriptions, is the core of this. It acts as an in-memory event bus on the server. When a mutation publishes an event, PubSub routes it to all active subscriptions listening for that event. The underlying transport layer (which you configure, often via graphql-ws or similar libraries) handles the actual delivery over websockets or other protocols.

What most developers miss is how seamlessly subscriptions integrate with your existing GraphQL schema and tooling. You don’t need a separate API for real-time updates; it’s just another type of operation in your GraphQL schema. This means your schema acts as a single source of truth for both data fetching and real-time eventing. The asyncIterator pattern is the elegant way the server exposes these events, allowing subscription resolvers to simply await new event payloads as they are published, rather than complex connection management.

The next hurdle you’ll likely encounter is handling subscription connection management and scaling, especially in distributed environments.

Want structured learning?

Take the full Graphql-tools course →