Node.js GraphQL Subscriptions in 2026: Realtime at Scale
product-development12 min readintermediate

Node.js GraphQL Subscriptions in 2026: Realtime at Scale

Vivek Singh
Founder & CEO at Witarist · June 10, 2026

Realtime is no longer a premium feature. Live dashboards, collaborative editors, trading tickers, delivery tracking, AI token streaming — users in 2026 expect data to update the instant it changes, not on the next refresh. For teams already invested in GraphQL, subscriptions are the native answer: the same schema and type system you use for queries and mutations, extended to a continuous stream of events.

But GraphQL subscriptions have a reputation for being fiddly in production. The original WebSocket library is unmaintained, transports behave differently behind load balancers, and an in-memory event bus silently breaks the moment you run more than one Node.js process. This guide walks through how subscriptions actually work in 2026 — the modern graphql-ws and graphql-sse transports, building a server on Apollo Server 4, and scaling horizontally with a Redis PubSub backend so no subscriber is ever left behind.

Why Realtime GraphQL Matters in 2026

Subscriptions vs. queries and mutations

A GraphQL operation comes in three flavours. Queries read data, mutations change it, and subscriptions open a long-lived channel that pushes new results to the client whenever a relevant event fires on the server. Unlike a query, a subscription does not resolve once and close — it stays open, emitting a payload each time the underlying source publishes. That makes it ideal for anything event-driven: chat messages, notification feeds, presence indicators, live order status, or streaming model output.

Where subscriptions beat polling

The naive alternative is polling: have the client re-run a query every few seconds. It works, but it scales badly. A thousand clients polling once per second is a thousand requests per second whether or not anything changed, and the freshest data is still up to a poll-interval stale. Subscriptions invert that — the server speaks only when there is genuinely something new, cutting both latency and wasted load. The throughput gap widens fast as connection counts grow, which the benchmark below makes concrete.

Comparison table of Node.js GraphQL subscription transports: graphql-ws WebSocket, graphql-sse SSE, and long-polling across protocol, bidirectionality, and reconnect behaviour
Figure 1 — How the three realtime transports compare across the dimensions that matter in production

graphql-ws vs graphql-sse: Choosing a Transport

graphql-ws and the modern WebSocket protocol

If you have read older tutorials you will have seen subscriptions-transport-ws. Do not use it — it has been unmaintained since 2021 and is incompatible with Apollo Server 4, causing connection instability and memory leaks. The current standard is graphql-ws, which implements the newer GraphQL over WebSocket protocol with a clean handshake, connection-init payloads for auth, and proper ping/pong keep-alives. WebSocket gives you a true bidirectional channel, which is what you want for chat, live cursors, and anything where the client also pushes frequent updates.

graphql-sse and Server-Sent Events

Not every realtime feature needs a duplex socket. For one-directional flows — notification feeds, live counters, log tailing — Server-Sent Events over plain HTTP are simpler to operate. The graphql-sse library implements the GraphQL over SSE spec and is the default transport for GraphQL Yoga. Because SSE is just HTTP, it sails through proxies and load balancers that need special configuration for WebSocket upgrades, and it reconnects automatically using the Last-Event-ID header. The trade-off is that the channel is server-to-client only.

A practical decision rule

Reach for graphql-ws when you need bidirectional, low-latency messaging or are already running a WebSocket gateway; reach for graphql-sse when the data flows one way and you value HTTP simplicity. Many production systems run both. If you are weighing this against a non-GraphQL approach entirely, our companion guides on building backend APIs and choosing a GraphQL stack go deeper on the trade-offs.

Figure 2 — Sustained message throughput by transport as concurrent subscribers grow

Building a Subscription Server with Apollo Server 4

Wiring graphql-ws into an Express + Apollo app

Apollo Server 4 dropped built-in subscription support, so you compose it yourself with the graphql-ws WebSocket server and the ws package. The pattern is to share one executable schema between the HTTP handler and the WebSocket handler, then register a drain plugin so in-flight sockets close cleanly on shutdown. The example below is a complete, runnable starting point.

server.js
import { createServer } from "node:http";
import express from "express";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();

const typeDefs = `#graphql
  type Message { id: ID!, room: ID!, body: String! }
  type Query { ping: String }
  type Mutation { send(room: ID!, body: String!): Message! }
  type Subscription { messageAdded(room: ID!): Message! }
`;

const resolvers = {
  Query: { ping: () => "pong" },
  Mutation: {
    send: (_p, { room, body }) => {
      const msg = { id: crypto.randomUUID(), room, body };
      pubsub.publish(`MSG_${room}`, { messageAdded: msg });
      return msg;
    },
  },
  Subscription: {
    messageAdded: {
      subscribe: (_p, { room }) => pubsub.asyncIterator(`MSG_${room}`),
    },
  },
};

const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();
const httpServer = createServer(app);

const wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" });
const wsCleanup = useServer({ schema }, wsServer);

const apollo = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    { async serverWillStart() { return { async drainServer() { await wsCleanup.dispose(); } }; } },
  ],
});

await apollo.start();
app.use("/graphql", express.json(), expressMiddleware(apollo));
httpServer.listen(4000, () => console.log("ready on :4000/graphql"));

The shape to notice is the resolver pair: a mutation publishes to a named topic, and the subscription resolver returns an async iterator over that same topic. Everything else — the WebSocket upgrade, keep-alives, and graceful drain — is wiring you set up once.

Architecture diagram showing clients connecting through a load balancer to three Node.js Apollo Server instances, all sharing events through a Redis PubSub bus
Figure 3 — The reference topology: a Redis PubSub bus fans every event to all Node.js instances

Scaling Subscriptions Across Instances with Redis

Why the in-memory PubSub breaks

The graphql-subscriptions PubSub shown above keeps its event bus in process memory. That is fine for a single instance and local development, but the moment you run two or more Node.js processes behind a load balancer it falls apart: a mutation handled by instance A publishes to instance A's memory only, so subscribers connected to instance B never receive the event. Restarts wipe all subscription state. In any horizontally scaled deployment you need a shared bus.

Ready to build your team?

Hire Pre-Vetted Node.js Developers

Skip the months-long search. Our exclusive talent network has senior Node.js experts ready to join your team in 48 hours.

Swapping in a Redis-backed PubSub

The fix is a drop-in replacement: graphql-redis-subscriptions publishes events to a Redis channel and every instance subscribes, so an event raised anywhere fans out to all connected clients regardless of which process holds the socket. The API matches the in-memory PubSub, so your resolvers do not change.

pubsub.js
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";

const options = {
  host: process.env.REDIS_HOST ?? "127.0.0.1",
  port: Number(process.env.REDIS_PORT ?? 6379),
  retryStrategy: (times) => Math.min(times * 50, 2000),
};

// Separate connections for pub and sub — Redis requires it.
export const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

// Resolvers stay identical:
//   pubsub.publish(`MSG_${room}`, { messageAdded: msg })
//   pubsub.asyncIterator(`MSG_${room}`)

The latency picture is the real payoff. With an in-memory bus a single overloaded node's tail latency explodes as subscribers pile on; with Redis fan-out across three instances, p95 delivery stays flat well past 100k concurrent subscribers, as the next chart shows.

Figure 4 — p95 delivery latency: in-memory single node versus Redis PubSub across three instances

Authentication, Authorization and Backpressure

Authenticating the connection

With graphql-ws, clients send credentials in the connection_init payload during the handshake. Your onConnect callback validates the token once and stores the resulting user on the connection context, which every subscription resolver can then read. Reject early — returning false from onConnect closes the socket before any subscription starts, so unauthenticated clients never consume server resources.

⚠️Warning
Never trust the subscription arguments alone for authorization. A client can subscribe to messageAdded(room: "any-id"). Always check inside the resolver that the authenticated user is actually a member of that room before returning the async iterator — otherwise subscriptions become a data-leak vector.

Backpressure and slow consumers

A subscriber on a flaky mobile connection can receive events more slowly than you produce them. Without a guard, buffered messages pile up in memory. Cap per-connection buffers, drop or coalesce events for slow clients where the domain allows it, and treat subscriptions as best-effort rather than guaranteed delivery. If a client needs a perfect, gap-free history, pair the live stream with a catch-up query keyed on a cursor or timestamp when it reconnects.

🚀Pro Tip
Send a heartbeat every 25–30 seconds. Most load balancers and proxies idle-timeout connections after 60 seconds, silently dropping otherwise-healthy subscriptions. graphql-ws supports keep-alive pings out of the box — enable them and set the interval below your proxy's timeout.

Production Checklist: Observability and Reliability

What to monitor

Subscriptions are long-lived, so the metrics that matter differ from request/response APIs. Track open connection count, subscriptions per connection, event publish rate, delivery latency, and reconnect frequency. Export these to your metrics backend and alert on sudden connection drops, which usually signal a deploy that did not drain sockets cleanly or a proxy timeout regression. Teams running this at scale often lean on a DevOps engineer to own the load-balancer and Redis configuration that subscriptions depend on.

Graceful shutdown on deploy

Every deploy closes thousands of sockets at once. Drain them: stop accepting new connections, send a close frame so clients reconnect to a healthy instance with backoff, and only then exit. The drain plugin in the server example does exactly this. Clients using graphql-ws reconnect automatically; just make sure your reconnect logic uses exponential backoff so a rolling deploy does not trigger a thundering herd.

💡Tip
Run a synthetic subscriber in production that connects, expects a known heartbeat event, and pages you if it stops receiving updates. It catches broken realtime paths that health checks on the HTTP endpoint will happily miss.

Realtime systems are where Node.js architecture skill really shows — the difference between a demo that works on one laptop and a fleet that holds 100k live connections is exactly the Redis fan-out, backpressure, and graceful-shutdown work above. If you are building one and need experienced engineers, HireNodeJS connects you with pre-vetted senior Node.js developers available within 48 hours — without the recruiter overhead. You can also see how the process works before you commit.

Hire Expert Node.js Developers — Ready in 48 Hours

Building the right realtime system is only half the battle — you need the right engineers to build it. HireNodeJS.com specialises exclusively in Node.js talent: every developer is pre-vetted on real-world projects, API design, event-driven architecture, and production deployments.

Unlike generalist platforms, our curated pool means you speak only to engineers who live and breathe Node.js — including specialists in GraphQL and Node.js realtime systems. Most clients have their first developer working within 48 hours of getting in touch. Engagements start as short-term contracts and can convert to full-time hires with zero placement fee.

💡Tip
🚀 Ready to scale your Node.js team? HireNodeJS.com connects you with pre-vetted engineers who can join within 48 hours — no lengthy screening, no recruiter fees. Browse developers at hirenodejs.com/hire

Conclusion: Realtime Done Right

GraphQL subscriptions in 2026 are production-ready when you make three decisions deliberately: pick the transport that matches your data flow (graphql-ws for bidirectional, graphql-sse for one-way), build on Apollo Server 4 with a cleanly drained WebSocket server, and back the event bus with Redis the instant you run more than one process. Get those right and you get a realtime layer that shares your existing schema, scales horizontally, and stays flat under load.

Layer on authentication at the handshake, authorization in every resolver, backpressure handling for slow clients, and observability tuned for long-lived connections, and you have a system that holds up well beyond the demo. The patterns are well understood now — the remaining work is disciplined engineering, which is exactly what separates a realtime feature that ships from one that pages you at 3am.

Topics
#Node.js#GraphQL#Subscriptions#Realtime#WebSocket#Redis#Apollo Server

Frequently Asked Questions

What is the difference between graphql-ws and graphql-sse?

graphql-ws runs GraphQL subscriptions over WebSocket and supports a bidirectional channel, ideal for chat or live cursors. graphql-sse runs them over Server-Sent Events on plain HTTP, which is simpler behind proxies and great for one-directional feeds and notifications.

Why do my GraphQL subscriptions stop working when I add a second server?

The default in-memory PubSub only lives inside one Node.js process, so events published on one instance never reach subscribers on another. Switch to a shared bus like graphql-redis-subscriptions so every instance receives every event.

Is subscriptions-transport-ws still safe to use?

No. It has been unmaintained since 2021 and is incompatible with Apollo Server 4, causing connection instability and memory leaks. Use graphql-ws for all new projects.

Does Apollo Server 4 support subscriptions out of the box?

Not built in. You add the graphql-ws WebSocket server yourself, share one executable schema between HTTP and WebSocket handlers, and register a drain plugin for graceful shutdown.

How do I authenticate a GraphQL subscription?

Clients send credentials in the connection_init payload during the WebSocket handshake. Validate the token in the onConnect callback, store the user on the connection context, and authorize again inside each subscription resolver.

How many concurrent subscribers can a Node.js server handle?

A single tuned instance can hold tens of thousands of connections, but for hundreds of thousands you scale horizontally across instances with a Redis PubSub backend and a load balancer, keeping p95 delivery latency flat.

About the Author
Vivek Singh
Founder & CEO at Witarist

Vivek Singh is the founder of Witarist and HireNodeJS.com — a platform connecting companies with pre-vetted Node.js developers. With years of experience scaling engineering teams, Vivek shares insights on hiring, tech talent, and building with Node.js.

Developers available now

Looking for a Node.js + GraphQL expert?

HireNodeJS connects you with pre-vetted senior Node.js engineers who have shipped realtime GraphQL systems at scale — available within 48 hours. No recruiter fees, no lengthy screening.