Live · Sun, Jul 5, 2026 · 14:11 UTC Block 843,917 Fees 14 sat/vB Fear & Greed 72 · Greed
Newsletter Pro Terminal Sign in
ITop Field News.
Subscribe →
Live · 14:11 UTC Block 843,917 F&G 72
Software development Software development desk

GraphQL subscriptions: when and how to use them

GraphQL subscriptions give applications a live data feed over a persistent connection, but they are not the right tool for every real-time problem. Here is a practical guide to when they make sense and how to implement them without creating operational headaches.

a blue background with lines and dots

Photo by Conny Schneider on Unsplash

GraphQL subscriptions are the part of the specification that most teams skip on a first implementation and then scramble to add later when users start complaining that their dashboards only refresh on page load. Unlike queries and mutations, which follow a request-response cycle, subscriptions open a persistent channel between client and server, pushing data whenever a defined event occurs. That sounds simple. In practice, the operational complexity is real, and choosing subscriptions over a lighter alternative is a decision worth making deliberately.

What GraphQL subscriptions actually are

The GraphQL specification defines three operation types: query, mutation, and subscription. A subscription is a long-lived operation that asks the server to emit results whenever a particular event fires. The client sends the subscription document once, and the server responds with a stream of payloads for as long as the connection stays open. Most implementations use WebSocket as the transport layer, though HTTP streaming and server-sent events are also viable depending on your stack.

The subscription payload follows the same typed schema as any other GraphQL response. That type safety is one of the reasons teams reach for subscriptions over a raw WebSocket or a hand-rolled polling loop: the contract between client and server is explicit, versioned, and introspectable. If you are already thinking carefully about WebSockets, REST, and GraphQL as distinct API patterns, subscriptions sit at the intersection of GraphQL's schema model and the persistent-connection model that WebSockets provide.

When subscriptions are the right choice

Subscriptions are not a universal real-time tool. They introduce server-side state, connection management overhead, and new failure modes. The cases where they genuinely earn their keep are:

  • Collaborative editing and live presence. When multiple users are working on the same document or board simultaneously, a subscription lets each client see changes from others within milliseconds. Polling at even one-second intervals creates visible lag and unnecessary load.
  • Operational dashboards. Monitoring views where a metric changing by five per cent matters to the person watching them. Pushing changes as they happen is more honest than a timestamp showing "last refreshed 47 seconds ago".
  • Chat and messaging. The canonical use case. A subscription per conversation thread is a clean fit for the data model.
  • Notification feeds. When a background job completes, a payment clears, or an alert fires, a subscription lets the UI respond instantly without requiring the user to refresh.
  • Live auction or trading data. Price data that changes in sub-second windows, where stale data has a real financial cost.

By contrast, subscriptions are overkill for data that changes infrequently, for server-to-client pushes where you control both ends (a simple webhook or server-sent event is lighter), or for any scenario where the client only needs the latest state rather than a stream of changes.

Transport and protocol choices

Most production GraphQL subscription implementations use one of two WebSocket sub-protocols: graphql-ws (the current standard, maintained by the graphql-ws library) or the older subscriptions-transport-ws (now largely unmaintained). If you are starting a new project, use graphql-ws. Migrating later is painful because clients and servers must agree on the sub-protocol.

For scenarios where WebSocket connections are blocked by corporate firewalls or restrictive proxies, server-sent events (SSE) offer a useful fallback. SSE uses a standard HTTP connection and works through most intermediaries, but is unidirectional: the client cannot send further messages over the same connection. That is fine for notification-style subscriptions but awkward for anything requiring client-to-server acknowledgement over the live channel.

Apollo and Relay both have first-class subscription support. On the server side, popular options include Apollo Server's subscription handling, Yoga, and Pothos with a PubSub layer. The PubSub layer is where things get interesting at scale.

The PubSub problem at scale

Every subscription implementation needs a mechanism to route events from the part of your system where they originate to the specific subscription connections that care about them. In a single-server setup, an in-memory PubSub (such as EventEmitter in Node.js) works fine. In any horizontally scaled deployment, it falls apart immediately: a mutation handled by server instance A cannot notify a subscription connection held by server instance B.

The standard solution is an external PubSub broker. Redis Pub/Sub is the most common choice and integrates cleanly with most GraphQL server libraries. Apache Kafka is worth considering if you already have it in your stack for event streaming, though the operational overhead is higher. NATS is a lighter option with excellent performance characteristics for high-throughput notification workloads.

Whichever broker you use, think carefully about event filtering. A naive implementation publishes every event to every subscriber and filters on the client side. At any meaningful scale, this becomes a bandwidth and CPU problem. Push filtering logic into the subscription resolver so that each connection only receives events it actually needs. This is a point where observability over your subscription layer pays dividends: without metrics on connection counts, event fan-out ratios, and dropped events, you are flying blind.

Authentication and authorisation

Authentication for subscriptions is trickier than for queries. HTTP headers are not available after the WebSocket handshake in the same way they are for a standard request, so tokens must either be passed in the WebSocket connection params at handshake time or re-validated in the onSubscribe hook of your server library.

Do not skip re-authorisation. A token that was valid when the subscription opened may expire or be revoked during the connection's lifetime. Implement a server-side mechanism to close subscriptions when the associated credential is invalidated. For Australian teams handling personal or health data, this is not optional: the Office of the Australian Information Commissioner expects that data access controls apply continuously, not just at connection time.

Practical implementation steps

A reliable subscription implementation follows a predictable sequence:

  1. Define the subscription type in your schema. Be specific about what events trigger a payload and what shape that payload takes. Resist the temptation to publish the entire root entity on every change; send only the fields the subscriber cares about.
  2. Choose and configure your transport. For most browser clients, graphql-ws over a WebSocket is the right starting point. Add an SSE fallback if your users are behind restrictive firewalls.
  3. Implement a PubSub layer with the right scope. In-memory for development and single-server deployments; Redis or another broker for anything horizontally scaled.
  4. Add authentication in the connection handshake. Validate the token before upgrading the connection, then re-validate on a schedule or on token events.
  5. Instrument everything. Track active connection counts, event throughput, and connection churn rates as first-class metrics. Subscriptions that silently drop events are worse than polling.
  6. Plan for reconnection on the client side. Networks drop. Clients should reconnect with exponential backoff and re-request missed data using a query after reconnection, not rely on the subscription stream having buffered it.

Common mistakes to avoid

Teams new to subscriptions tend to make a handful of repeatable errors. Over-subscribing is the most common: opening a subscription for every entity on a page rather than one subscription for a relevant context. Each WebSocket connection consumes server memory and file descriptors, so the cost compounds quickly with user count.

Another frequent mistake is conflating "subscription" with "event sourcing". A GraphQL subscription is a delivery mechanism, not a record of truth. If a client disconnects and reconnects, it has missed events; it should fetch current state with a query before re-opening the subscription, not attempt to reconstruct state from a buffered event stream.

Finally, do not expose subscriptions publicly without rate limiting. A client that opens thousands of connections in a short period can exhaust server resources as effectively as a DDoS. Apply connection limits per authenticated identity, not just per IP address, and add circuit-breaker logic in your PubSub layer to handle event storms from upstream systems.

When to choose something simpler

Before committing to a full subscription architecture, ask whether the problem can be solved with polling or server-sent events instead. For data that updates every few minutes, a five-second polling loop on a cached query endpoint is simpler to operate, easier to debug, and perfectly adequate. The right architecture is the one that matches the actual latency requirement of the feature, not the one that sounds most sophisticated in a design review.

GraphQL subscriptions are a mature, well-specified tool. They deserve a place in any team's API toolkit, applied to the problems they actually solve.

→ The Confirmations · Daily newsletter

One email at 06:00 UTC. Six minutes. The only digest written for desks, not for retail.