HomeCurriculumGraphQLReal-Time GraphQL Subscriptions & Federation
Real-Time GraphQL Subscriptions & Federation

Real-Time GraphQL Subscriptions & Federation

Deploy enterprise Supergraphs with Apollo Federation @key directives and manage real-time WebSocket subscriptions over distributed PubSub brokers.

DT
Authored by DevThesis Systems Editorial Team
Reviewed for Technical Accuracy & Code Determinism

As organizations scale their engineering operations and product complexity, GraphQL architectures must evolve along two critical dimensions: real-time bi-directional interactivity and distributed microservice decomposition.

On the client interaction side, standard request-response HTTP queries fall short when applications demand instantaneous data updates—such as collaborative document editing, real-time financial trading tickers, multi-user gaming leaderboards, and live chat notifications. GraphQL addresses this through the third root operation type: Subscriptions.

On the organizational architecture side, managing a monolithic GraphQL schema across dozens of autonomous engineering teams creates severe deployment friction, schema conflicts, and operational bottlenecks. To resolve this, the industry adopted Apollo Federation—a declarative architecture that decomposes a unified, global "supergraph" into independent, domain-driven "subgraphs" owned and deployed autonomously by separate microservice teams.

This module delivers an exhaustive, university-grade architectural breakdown of real-time GraphQL subscriptions over the graphql-ws protocol, distributed Redis pub/sub event backbones, and Apollo Federation v2 schema composition and query planning algorithms.


The Real-Time Paradigm: GraphQL Subscriptions#

While query and mutation operations operate over standard stateless HTTP request-response cycles, a subscription establishes a persistent, stateful connection between the client and the server.

text
+-----------------------------------------------------------------------------------+
| GraphQL Subscriptions Network Architecture                                        |
|                                                                                   |
|  [ Client Application: React / Apollo Client ]                                    |
|                         |                                                         |
|                         | 1. Persistent Bi-directional WebSocket / SSE Handshake |
|                         |    Protocol: 'graphql-transport-ws' (RFC compliant)     |
|                         v                                                         |
|  +-----------------------------------------------------------------------------+  |
|  | GraphQL Subscription Gateway / WebSocket Server                             |  |
|  | - Handles connection initialization (connection_init -> connection_ack)     |  |
|  | - Maintains active client socket registry (Socket ID -> Subscriptions Map)  |  |
|  | - Dispatches heartbeat PING/PONG frames to detect stale half-open sockets   |  |
|  +-------------------------------------+---------------------------------------+  |
|                                        |                                          |
|                                        | 2. Subscribes to event topics            |
|                                        v                                          |
|  +-----------------------------------------------------------------------------+  |
|  | Distributed Event Bus: Redis Pub/Sub / Kafka / RabbitMQ                     |  |
|  | - Topic: "CHAT_MESSAGE_CREATED:channel_99"                                   |  |
|  +-------------------------------------+---------------------------------------+  |
|                                        ^                                          |
|                                        | 3. Mutation publishes new event          |
|  +-------------------------------------+---------------------------------------+  |
|  | Mutating Backend Server (HTTP POST /graphql)                                |  |
|  | - mutation { postMessage(...) } -> Saves to DB -> pubsub.publish(...)       |  |
|  +-----------------------------------------------------------------------------+  |
|                                                                                   |
|  Data Flow (Steps 3 -> 2 -> 1):                                                   |
|  Mutation publishes payload to Redis -> Redis distributes to all Gateway nodes -> |
|  Gateway evaluates subscription filter -> Pushes serialized GraphQL JSON to client|
+-----------------------------------------------------------------------------------+

The Modern `graphql-ws` Transport Protocol#

Historically, subscriptions relied on the legacy subscriptions-transport-ws library. In modern production environments, that library is deprecated and replaced by the RFC-compliant `graphql-ws` protocol.

Lifecycle Protocol Frame Exchange:#

  1. Connection Initialization:

* Client sends: {"type": "connection_init", "payload": { "authToken": "..." }}

* Server validates authentication token inside connection callback.

* Server responds: {"type": "connection_ack"}

  1. Subscription Registration:

* Client sends: {"id": "sub_1", "type": "subscribe", "payload": { "query": "subscription { messageAdded(channelId: \"9\") { id text } }" }}

* Server executes the subscription AST, invokes the subscribe resolver, and registers an AsyncIterator.

  1. Data Streaming:

* Whenever an event occurs on the pub/sub topic, the server invokes the resolve resolver and pushes:

{"id": "sub_1", "type": "next", "payload": { "data": { "messageAdded": { "id": "msg_42", "text": "Hello World" } } }}

  1. Subscription Termination:

* Client sends: {"id": "sub_1", "type": "complete"}

* Server unbinds the listener, cleans up memory, and closes the AsyncIterator.


Pub/Sub Architecture: In-Memory vs Distributed Redis#

A subscription resolver consists of two distinct functions: subscribe and resolve.

typescript
const resolvers = {
  Subscription: {
    messageAdded: {
      // 1. Subscribes to the underlying event stream (AsyncIterator)
      subscribe: (_parent, args, context) => {
        return context.pubsub.asyncIterator([`CHANNEL_{args.channelId}`]);
      },
      // 2. Maps the raw event payload into the GraphQL response shape
      resolve: (payload) => {
        return payload.messageCreated;
      }
    }
  }
};

The Multi-Server Cluster Dilemma#

In local development, developers often use Node's in-memory EventEmitter (e.g., const pubsub = new PubSub()).

In production, applications run behind load balancers on multi-container clusters (Kubernetes Pods).

text
  [ Client A (WebSocket) ]                  [ Client B (HTTP Client) ]
             |                                          |
             v                                          v
      +--------------+                           +--------------+
      | Pod 1 (Node) |                           | Pod 2 (Node) |
      | Socket A     |                           | Mutation     |
      +-------+------+                           +-------+------+
              |                                          |
              |                                          | In-Memory PubSub
              |                                          | Emits to Pod 2 ONLY!
              x <========================================+
                 Pod 1 NEVER receives the event!
                 Client A NEVER receives the update!
  • Client A connects via WebSocket to Pod 1.
  • Client B issues a mutation via HTTP POST to Pod 2.
  • If Pod 2 emits to an in-memory PubSub, the event exists only in Pod 2's RAM. Pod 1 never sees it, and Client A's screen never updates.

Architectural Requirement: Enterprise GraphQL subscriptions must use a centralized distributed message broker—such as Redis Pub/Sub or Redis Streams—via libraries like graphql-redis-subscriptions. When Pod 2 publishes to Redis, Redis broadcasts the message to all subscriber pods, ensuring Pod 1 picks up the event and dispatches it down Client A's WebSocket.


Schema Federation: Decomposing the Monolith#

In large enterprises with hundreds of engineers (e.g., Netflix, Expedia, PayPal), maintaining a single monolithic GraphQL server repository is unsustainable. Teams experience merge conflicts in schema files, deployment coordination deadlocks, and cross-domain code pollution.

text
+-----------------------------------------------------------------------------------+
| Monolithic GraphQL vs Apollo Federation v2 Architecture                           |
|                                                                                   |
|  Monolithic Schema Repository:                                                    |
|  - 1 Huge Server (50,000 lines of SDL)                                            |
|  - 15 Teams committing to 1 repo                                                  |
|  - If Team A introduces a bug, Team B's service deploy is blocked                 |
|                                                                                   |
|  -------------------------------------------------------------------------------  |
|                                                                                   |
|  Apollo Federation v2 Supergraph Architecture:                                    |
|                                                                                   |
|                             [ Client Query Request ]                              |
|                                        |                                          |
|                                        v                                          |
|                     +--------------------------------------+                      |
|                     | Apollo Router / Gateway (Rust-based) |                      |
|                     | - Exposes unified Supergraph Schema  |                      |
|                     | - Query Planner: Generates DAG plan  |                      |
|                     | - Orchestrates downstream subgraphs  |                      |
|                     +------------------+-------------------+                      |
|                                        |                                          |
|             +--------------------------+--------------------------+               |
|             | HTTP POST (Sub-query)    | HTTP POST (Sub-query)    | HTTP POST     |
|             v                          v                          v               |
|    +------------------+       +------------------+       +------------------+     |
|    | Users Subgraph   |       | Products Subgraph|       | Reviews Subgraph |     |
|    | (Auth Team)      |       | (Inventory Team) |       | (Community Team) |     |
|    | Owns: User entity|       | Owns: Product    |       | Extends: User    |     |
|    +------------------+       +------------------+       +------------------+     |
+-----------------------------------------------------------------------------------+

Core Concepts of Apollo Federation v2#

1. The Supergraph#

The unified, comprehensive schema exposed to client applications. Clients are completely unaware that Federation exists; to the frontend, it appears as a single, coherent GraphQL API.

2. Subgraphs#

Independent, autonomous GraphQL services owned by separate microservice teams. Each subgraph defines only the types, queries, and mutations relevant to its business domain.

3. Entities#

An Entity is an object type that can be referenced, extended, and resolved across multiple subgraphs. An entity is declared using the @key directive:

graphql
# users-subgraph.graphql
type User @key(fields: "id") {
  id: ID!
  email: String!
  displayName: String!
}

4. Entity Extensions & Entity Representations#

Other subgraphs can extend an entity to add domain-specific fields without modifying the original service.

For example, the Reviews Team can add a reviews field to the User entity:

graphql
# reviews-subgraph.graphql
type Review {
  id: ID!
  rating: Int!
  comment: String!
}

# Extend User entity owned by Users Subgraph
type User @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

5. Entity Reference Resolvers (`__resolveReference`)#

When a client queries:

graphql
query {
  user(id: "42") {
    displayName
    reviews { rating comment }
  }
}
  1. The Apollo Router queries the Users Subgraph for displayName.
  2. The Router extracts { __typename: "User", id: "42" } (the Entity Representation).
  3. The Router dispatches an internal query to the Reviews Subgraph:

query { _entities(representations: [{ __typename: "User", id: "42" }]) { ... on User { reviews { rating comment } } } }

  1. The Reviews Subgraph implements a dedicated __resolveReference resolver to rehydrate the user:
typescript
const resolvers = {
  User: {
    __resolveReference: async (reference: { id: string }, context: GraphQLContext) => {
      // Rehydrate user reviews using reference.id
      return { id: reference.id };
    },
    reviews: async (parent: { id: string }, _args: unknown, context: GraphQLContext) => {
      return context.db.query('SELECT * FROM reviews WHERE author_id = 1', [parent.id]);
    }
  }
};

Federation Directive Taxonomy#

Apollo Federation v2 relies on standardized directives to coordinate cross-service schema composition:

DirectivePurposeExample
@key(fields: "...")Designates primary key field(s) for an Entitytype Product @key(fields: "sku")
@shareableAllows multiple subgraphs to resolve the exact same fieldname: String! @shareable
@externalMarks a field as defined and owned by another subgraphpriceCents: Int! @external
@requires(fields: "...")Declares that resolving a field requires another external field@requires(fields: "priceCents")
@provides(fields: "...")Informs router that this subgraph can optimize by providing external dataauthor: User! @provides(fields: "name")
@inaccessibleOmits a field from the Supergraph API while keeping it in subgraphsinternalHash: String! @inaccessible

The Federation Query Planner#

The core intelligence of Apollo Federation resides in the Query Planner (implemented in Rust inside the Apollo Router). When a client query arrives, the Query Planner constructs an execution directed acyclic graph:

text
Query Plan for:
{
  product(id: "p_101") {
    name                # From Products Subgraph
    shippingCost        # From Shipping Subgraph (Requires: weightGrams from Product)
    reviews { rating }  # From Reviews Subgraph
  }
}

Step 1: Fetch from Products Subgraph
  Query: { product(id: "p_101") { name weightGrams } }

Step 2: Concurrently execute:
  - Step 2a: Query Shipping Subgraph
      Pass Entity: { __typename: "Product", id: "p_101", weightGrams: 540 }
      Fetch: shippingCost
  - Step 2b: Query Reviews Subgraph
      Pass Entity: { __typename: "Product", id: "p_101" }
      Fetch: reviews { rating }

Step 3: Flatten and merge JSON responses; strip 'weightGrams' (unrequested by client); return to user.

Checkpoint Practice Problems#

Test your architectural depth in distributed GraphQL, WebSocket socket management, and federated schema design with these real-world engineering problems.

Problem 1: WebSocket Memory Footprint & Connection Capacity Sizing#

An enterprise SaaS collaboration tool provides live document synchronization using GraphQL Subscriptions over WebSockets (graphql-ws).

The infrastructure team provisions a cluster of application server nodes to terminate client connections.

System parameters:

  • Each server node is a Kubernetes pod provisioned with M = 8 GB of RAM (8,192 MB) and 4 vCPU cores.
  • The Linux operating system and Node.js V8 runtime baseline memory overhead consumes B = 800 MB.
  • Each active WebSocket connection maintains:

* A TCP socket buffer pair (rmem / wmem kernel allocation): S_{tcp} = 16 KB.

* A V8 JavaScript socket instance, session state, and subscription AST metadata: S_{v8} = 48 KB.

* Total memory footprint per connection = S_{conn} = 16 KB + 48 KB = 64 KB.

  • In addition, 15\% of active users execute a document collaboration subscription that caches an active document context averaging S_{doc} = 120 KB per subscriber.

Calculate:

  1. The average memory footprint per active subscriber connection (S_avg).
  2. The theoretical maximum concurrent WebSocket connections (N_{max}) that a single 8 GB pod can sustain before triggering an Out-Of-Memory (OOM) killer eviction (assuming an 80% safe memory ceiling to prevent GC spikes).
  3. The number of pods required to support a global peak concurrent user base of 500,000 active subscribers.

Solution:

Step 1: Calculate Average Memory Footprint per Connection (S_avg):

Base connection memory = 64 KB.

15% of connections hold additional document context:

Additional Context = 0.15 × 120 KB = 18 KBAverage memory footprint per connection:

S_avg = 64 KB + 18 KB = 82 KB = 82 × 1024 bytes = 83,968 bytes ≈ 0.080 MBStep 2: Maximum Concurrent Connections per 8 GB Pod:

Total RAM = 8,192 MB.

Applying an 80% safe threshold:

Safe RAM Ceiling = 8,192 MB × 0.80 = 6,553.6 MBSubtracting baseline OS and runtime overhead (800 MB):

Available RAM for Connections = 6,553.6 MB - 800 MB = 5,753.6 MBMaximum connections per pod:

N_{max} = (5,753.6 MB) / (0.080 MB/conn) ≈ 71,920 concurrent connectionsOS File Descriptor Check:

Each socket requires an open file descriptor (fd).

The Linux system limit (nofile) must be tuned:

ulimit -n 100000 to prevent EMFILE: too many open files errors.

Step 3: Cluster Sizing for 500,000 Concurrent Users:

Pods Required = (500,000 users) / (71,920 conn/pod) ≈ 6.95 → 7 podsProvisioning with an N+1 or N+2 high-availability redundancy margin:

Production Cluster Size = 7 + 2 = 9 podsA cluster of 9 pods (each 8 GB RAM / 4 vCPU) comfortably sustains 500,000 concurrent real-time subscriptions with full fault tolerance.


Problem 2: Federation Query Plan Execution Time & Critical Path Analysis#

A client application queries an Apollo Federation v2 supergraph:

graphql
query ProductDetails(id: ID!) {
  product(id: id) {
    name                # Subgraph A: Products Service
    inventoryLevel      # Subgraph B: Warehouse Service (Key: sku)
    estimatedDelivery   # Subgraph C: Logistics Service (Requires: warehouseZip, weightGrams)
    reviews(limit: 5) { # Subgraph D: Reviews Service (Key: id)
      rating
    }
  }
}

The Router analyzes the schema dependencies:

  1. Products Service resolves name, sku, and weightGrams. Execution latency: T_A = 12 ms.
  2. Warehouse Service uses sku (from Subgraph A) to resolve inventoryLevel and warehouseZip. Latency: T_B = 18 ms.
  3. Logistics Service requires warehouseZip (from Subgraph B) and weightGrams (from Subgraph A) to resolve estimatedDelivery. Latency: T_C = 25 ms.
  4. Reviews Service uses id (from Subgraph A) to resolve reviews. Latency: T_D = 15 ms.
  5. Router internal JSON parsing, network serialization, and merging introduces an overhead of T_{router} = 2.0 ms per network hop.

Determine:

  1. The dependency DAG (Directed Acyclic Graph) of the query execution plan.
  2. The Critical Path Latency of the distributed query.
  3. Contrast this with a naive sequential execution across all 4 subgraphs.

Solution:

Step 1: Construct Execution Dependency DAG:

  • Hop 1: Router must query Subgraph A (Products Service) first because sku, weightGrams, and id are required downstream.
  • Hop 2 (Parallel Fan-out):

* As soon as Subgraph A returns:

* Subgraph D (Reviews Service) can execute immediately (depends only on Subgraph A's id).

* Subgraph B (Warehouse Service) can execute immediately (depends on Subgraph A's sku).

  • Hop 3 (Dependent Hop):

Subgraph C (`Logistics Service`) cannot execute until both* Subgraph A (weightGrams) and Subgraph B (warehouseZip) have resolved!

* Therefore, Subgraph C must wait for Subgraph B to complete.

text
       [ Router Ingress ]
               |
               v
     [ Subgraph A: 12 ms ]
               |
        +------+------+
        |             |
        v             v
  [ Subgraph D: 15ms ] [ Subgraph B: 18 ms ]
                      |
                      v
                [ Subgraph C: 25 ms ]

Step 2: Calculate Critical Path Latency:

The total execution time is governed by the longest path through the execution graph:

  • Path 1: Router → Subgraph A → Subgraph D → Merge

Latency_1 = T_{router} + T_A + T_{router} + T_D = 2 + 12 + 2 + 15 = 31 ms* Path 2: Router → Subgraph A → Subgraph B → Subgraph C → Merge

Latency_2 = T_{router} + T_A + T_{router} + T_B + T_{router} + T_C + T_{router}Latency_2 = 2 + 12 + 2 + 18 + 2 + 25 + 2 = 63 msCritical Path Latency = max(31 ms, 63 ms) = 63 ms.

Step 3: Comparison with Naive Sequential Execution:

If a naive gateway executed all 4 subgraphs sequentially:

Sequential Latency = T_A + T_B + T_C + T_D + (4 × T_{router}) = 12 + 18 + 25 + 15 + 8 = 78 msParallel execution of Subgraph D concurrently with the B → C pipeline saves 15 ms (19.2\% latency reduction).


Problem 3: Production Distributed Subscription Engine with Redis Pub/Sub#

Implement an enterprise-grade GraphQL subscription server using express, ws, graphql-ws, and graphql-redis-subscriptions that handles user authentication during WebSocket connection initialization and binds to a Redis cluster.

Solution:

typescript
// subscription-server.ts
import http from 'node:http';
import express from 'express';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
import { schema } from './schema';

const app = express();
const server = http.createServer(app);

// Configure Redis Pub/Sub connections
const redisOptions = {
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379', 10),
  retryStrategy: (times: number) => Math.min(times * 50, 2000)
};

export const pubsub = new RedisPubSub({
  publisher: new Redis(redisOptions),
  subscriber: new Redis(redisOptions)
});

// Create WebSocket server for subscriptions
const wsServer = new WebSocketServer({
  server,
  path: '/graphql'
});

// Configure graphql-ws server lifecycle
useServer(
  {
    schema,
    // 1. Connection Initialization & Authentication Guard
    onConnect: async (ctx) => {
      const authHeader = ctx.connectionParams?.authToken as string;
      if (!authHeader) {
        // Reject unauthenticated WebSocket connections
        return false;
      }

      try {
        const user = await validateAuthToken(authHeader);
        // Bind user to context
        ctx.extra = { currentUser: user };
        return true;
      } catch {
        return false;
      }
    },

    // 2. Build GraphQL Context for each subscription operation
    context: (ctx) => {
      return {
        pubsub,
        currentUser: (ctx.extra as any).currentUser
      };
    },

    // 3. Error and disconnect logging
    onDisconnect: (_ctx, code, reason) => {
      console.log(`[Subscription WS] Client disconnected ({code}): {reason}`);
    }
  },
  wsServer
);

async function validateAuthToken(token: string) {
  // Token verification logic
  return { id: 'u_102', role: 'USER' };
}

server.listen(4000, () => {
  console.log('[Server] GraphQL Subscription Server running on ws://localhost:4000/graphql');
});

Through this architecture, incoming WebSocket connections are securely authenticated during the connection_init handshake frame. State updates published to Redis are broadcast across all instances, guaranteeing real-time synchronization across load-balanced pods.


Problem 4: Multi-Subgraph Apollo Federation Architecture Walkthrough#

Design the complete schema and resolver implementations for two Apollo Federation v2 subgraphs:

  1. Accounts Subgraph: Owns the User entity (id, username, email).
  2. Reviews Subgraph: Extends the User entity to provide reviews: [Review!]!, and implements a query topReviews.

Solution:

Subgraph 1: Accounts Service (`accounts-subgraph`)#

graphql
# accounts.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])

type User @key(fields: "id") {
  id: ID!
  username: String!
  email: String!
}

type Query {
  user(id: ID!): User
}
typescript
// accounts-resolvers.ts
const usersDb = new Map([
  ['usr_1', { id: 'usr_1', username: 'alice', email: 'alice@devthesis.com' }]
]);

export const accountsResolvers = {
  Query: {
    user: (_p: any, args: { id: string }) => usersDb.get(args.id) || null
  },
  User: {
    // Entity Reference Resolver
    __resolveReference: (reference: { id: string }) => {
      return usersDb.get(reference.id) || null;
    }
  }
};

Subgraph 2: Reviews Service (`reviews-subgraph`)#

graphql
# reviews.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])

type Review {
  id: ID!
  body: String!
  rating: Int!
  authorId: ID!
}

# Extend User entity owned by Accounts Subgraph
type User @key(fields: "id") {
  id: ID!
  reviews: [Review!]!
}

type Query {
  topReviews(limit: Int = 10): [Review!]!
}
typescript
// reviews-resolvers.ts
const reviewsDb = [
  { id: 'rev_1', authorId: 'usr_1', rating: 5, body: 'Outstanding deep architectural guide!' },
  { id: 'rev_2', authorId: 'usr_1', rating: 4, body: 'Very thorough mathematical proofs.' }
];

export const reviewsResolvers = {
  Query: {
    topReviews: (_p: any, args: { limit: number }) => {
      return reviewsDb.slice(0, args.limit);
    }
  },
  User: {
    // Entity Reference Resolver for extended User entity
    __resolveReference: (reference: { id: string }) => {
      // Rehydrate minimal reference
      return { id: reference.id };
    },
    reviews: (parent: { id: string }) => {
      return reviewsDb.filter((rev) => rev.authorId === parent.id);
    }
  }
};

When composed by the Apollo Supergraph Router, the frontend can query user(id: "usr_1") { username reviews { rating body } }. The router dispatches username to Accounts, forwards { id: "usr_1" } to Reviews, and stitches the final response seamlessly without either microservice holding direct database dependencies on the other.

Key Architectural Takeaways

  • Apollo Federation composes autonomous subgraphs into a unified Supergraph via declarative directives (@key, @requires).
  • The Federation Gateway generates distributed Query Plans to stitch entity fragments via the _entities interface.
  • GraphQL Subscriptions leverage WebSockets or SSE backed by Redis Pub/Sub to push continuous state updates to clients.

Hands-on Code Playground Available

Execute, edit, and experiment with code for this topic in the live sandbox.

Practice Check • Quick Quiz

Test Your Understanding

In Apollo Federation, what is the purpose of the @key(fields: 'id') directive declared on an entity type?

A
It encrypts the ID field using AES-256.
B
It designates the type as an entity that can be referenced, extended, and resolved across multiple subgraphs.
C
It specifies that the field must be cached in browser localStorage.
D
It prevents client queries from requesting the ID field.