Skip to content
NestJS ns graphql 4 min read

GraphQL Subscriptions

Subscriptions are GraphQL’s mechanism for pushing data to clients over a long-lived connection instead of answering a one-off request. In NestJS you declare them with the @Subscription decorator and back them with a PubSub engine that broadcasts events to interested clients. This is how you build live dashboards, chat, notifications, and any feature where the server needs to tell the client something changed — without the client polling for it.

How subscriptions work in NestJS

A subscription resolver returns an AsyncIterator rather than a value. The PubSub engine produces that iterator from a named topic; whenever something publishes to the topic, every subscribed client receives the payload. Mutations (or any service code) are the producers, and subscriptions are the consumers. Under the hood the transport is a WebSocket using the graphql-ws protocol, so subscriptions need that layer enabled in the GraphQL module.

Install the in-memory PubSub package to get started. It is perfect for a single instance; for multiple instances you swap it for a Redis-backed implementation (covered below).

npm install graphql-subscriptions graphql-ws

Enable the subscription transport in your GraphQLModule configuration. With the Apollo driver, set subscriptions to register the graphql-ws protocol on a path.

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'schema.gql',
      subscriptions: {
        'graphql-ws': true,
      },
    }),
  ],
})
export class AppModule {}

Providing a PubSub instance

Expose the PubSub engine as a Nest provider so it can be injected anywhere — both into the resolver that subscribes and into the service that publishes. Registering it once guarantees every consumer shares the same event bus.

import { Module } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
import { CommentsResolver } from './comments.resolver';
import { CommentsService } from './comments.service';

export const PUB_SUB = 'PUB_SUB';

@Module({
  providers: [
    CommentsResolver,
    CommentsService,
    { provide: PUB_SUB, useValue: new PubSub() },
  ],
})
export class CommentsModule {}

Declaring a @Subscription resolver

A @Subscription method returns pubSub.asyncIterator(topic). The decorator’s type function describes the payload shape, exactly like @Query. The field name (here commentAdded) must match the key used in the published payload object, unless you supply a resolve function to reshape it.

import { Resolver, Subscription, Mutation, Args } from '@nestjs/graphql';
import { Inject } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';
import { Comment } from './models/comment.model';
import { AddCommentInput } from './dto/add-comment.input';
import { CommentsService } from './comments.service';
import { PUB_SUB } from './comments.module';

@Resolver(() => Comment)
export class CommentsResolver {
  constructor(
    private readonly commentsService: CommentsService,
    @Inject(PUB_SUB) private readonly pubSub: PubSub,
  ) {}

  @Mutation(() => Comment)
  async addComment(@Args('input') input: AddCommentInput): Promise<Comment> {
    const comment = await this.commentsService.add(input);
    await this.pubSub.publish('commentAdded', { commentAdded: comment });
    return comment;
  }

  @Subscription(() => Comment)
  commentAdded() {
    return this.pubSub.asyncIterator('commentAdded');
  }
}

A client subscribes over the WebSocket connection and receives a message each time addComment publishes.

wscat -c ws://localhost:3000/graphql -s graphql-transport-ws \
  -x '{"type":"connection_init"}' \
  -x '{"id":"1","type":"subscribe","payload":{"query":"subscription { commentAdded { id text } }"}}'

Output:

{"type":"next","id":"1","payload":{"data":{"commentAdded":{"id":"42","text":"Nice work!"}}}}

Filtering and resolving payloads

Often a client should only receive a subset of events — for example, comments on the post it is viewing. The filter function runs per published event and returns a boolean; the resolve function transforms the stored payload into the value sent to the client. Both receive the subscription’s own @Args, so you can compare arguments against the payload.

@Subscription(() => Comment, {
  filter: (payload, variables) =>
    payload.commentAdded.postId === variables.postId,
  resolve: (payload) => payload.commentAdded,
})
commentAdded(@Args('postId') postId: string) {
  return this.pubSub.asyncIterator('commentAdded');
}

Keep filtering on the server. Returning every event and filtering in the browser leaks data and wastes bandwidth — filter evaluates before the payload ever leaves the process.

Options reference

OptionPurpose
filterPredicate (payload, variables, context) deciding if a client receives the event
resolveMaps the published payload to the field’s return value
nameOverride the schema field name when it differs from the method name
nullableAllow the emitted value to be null

Scaling with Redis

The default PubSub keeps subscriptions in process memory, so events published on one instance never reach clients connected to another. Behind a load balancer you need a shared broker. Swap in graphql-redis-subscriptions, which fans events out through Redis pub/sub.

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

const options = { host: process.env.REDIS_HOST, port: 6379 };

{
  provide: PUB_SUB,
  useValue: new RedisPubSub({
    publisher: new Redis(options),
    subscriber: new Redis(options),
  }),
}

Because the resolver depends only on the PUB_SUB token, no resolver or service code changes — you have replaced the engine, not the contract.

Best Practices

  • Register a single PubSub instance as a provider behind a token and inject it everywhere, so publishers and subscribers share one bus.
  • Publish from services or mutations the moment state changes, and keep the published payload small and serializable.
  • Always filter events server-side with filter rather than trusting clients to discard irrelevant messages.
  • Use resolve to decouple the shape you store on a topic from the shape clients consume, keeping topics reusable across subscriptions.
  • Move to a Redis-backed PubSub before you run more than one instance — in-memory PubSub silently drops cross-instance events.
  • Authenticate the WebSocket handshake (via subscriptions.onConnect) and guard subscription fields just as you would queries and mutations.
Last updated June 14, 2026
Was this helpful?