Since 1.22.0

Configure GraphQL Subscription Support

Enable clients to receive real-time updates


For self-hosted routers, subscription support is an Enterprise feature.Subscription support is also available for cloud routers with a GraphOS Serverless or Dedicated plan. For cloud router subscription information, refer to the cloud-specific docs.

GraphOS routers provides support for GraphQL subscription operations:

GraphQL
1subscription OnStockPricesChanged {
2  stockPricesChanged {
3    symbol
4    price
5  }
6}

With subscription support enabled, you can add Subscription fields to the schema of any subgraph that supports common WebSocket protocols for subscription communication:

GraphQL
stocks.graphql
1type Subscription {
2  stockPricesChanged: [Stock!]!
3}

What are subscriptions for?

GraphQL subscriptions enable clients to receive continual, real-time updates whenever new data becomes available. Unlike queries and mutations, subscriptions are long-lasting. That means a client can receive multiple updates from a single subscription:

Subscriptions are best suited to apps that rely on frequently changing, time-sensitive data such as stock prices, IoT sensor readings, live chat, or sports scores.

How subscriptions work

  1. A client executes a GraphQL subscription operation against your router over HTTP:

    GraphQL
    Example subscription
    1subscription OnStockPricesChanged {
    2  stockPricesChanged {
    3    symbol
    4    price
    5  }
    6}
    • The client doesn't use a WebSocket protocol. Instead, it receives updates via multipart HTTP responses.

    • By using HTTP for subscriptions, clients can execute all GraphQL operation types over HTTP instead of using two different protocols.

    • Apollo Client, Apollo Kotlin, and Apollo iOS all support GraphQL subscriptions over HTTP with minimal configuration. See each library's documentation for details. Apollo Client also provides network adapters for the Relay and urql libraries.

  2. When your router receives a subscription, it executes that same subscription against whichever subgraph defines the requested field—stockPricesChanged in the code snippet above.

    • This communication usually does use a WebSocket subprotocol, for compatibility with most subgraph libraries.

    • With a self-hosted router, you can also configure an HTTP-callback-based protocol.

  3. The subgraph periodically sends new data to your router. Whenever it does, the router returns that data to the client in an additional HTTP response part.

    • A subscription can include federated entity fields that are defined in other subgraphs. If it does, the router first fetches those fields by querying the corresponding subgraphs, such as the Portfolios subgraph in the diagram above. These queries use HTTP as usual.

Special considerations

Whenever your router updates its supergraph schema at runtime, it terminates all active subscriptions. Clients can detect this special-case termination via an error code and execute a new subscription. See Termination on schema update.

Prerequisites

Before you add Subscription fields to your subgraphs, do all of the following in the order shown to prevent schema composition errors:

  1. Update your router instances to version 1.22.0 or later. Download the latest version.

    • Previous versions of the router don't support subscription operations.

  2. Make sure your router is connected to a GraphOS Enterprise organization.

    • Subscription support is an Enterprise feature of self-hosted routers.

  3. If you compose your router's supergraph schema with GraphOS (instead of with the Rover CLI), update your build pipeline to use Apollo Federation 2.4 or later.

    • Previous versions of Apollo Federation don't support subscription operations.

  4. Modify your subgraph schemas to use Apollo Federation 2.4 or later:

    GraphQL
    stocks.graphql
    1extend schema
    2@link(url: "https://specs.apollo.dev/federation/v2.4",
    3      import: ["@key", "@shareable"])
    4
    5type Subscription {
    6  stockPricesChanged: [Stock!]!
    7}
    • You can skip modifying subgraph schemas that don't define any Subscription fields.

  5. If you're using Apollo Server to implement subgraphs, update your Apollo Server instances to version 4 or later.

After you complete these prerequisites, you can safely configure your router for subscriptions.

Router setup

After completing all prerequisites, in your router's YAML config file, you configure how the router communicates with each of your subgraphs when executing GraphQL subscriptions.

The router supports two popular WebSocket protocols for subscriptions, and it also provides support for an HTTP-callback-based protocol. Your router must use whichever protocol is expected by each subgraph.

WebSocket setup

Here's an example router configuration snippet that sets up subgraph subscriptions over WebSocket:

YAML
router.yaml
1subscription:
2  enabled: true
3  mode:
4    passthrough:
5      all: # The router uses these subscription settings UNLESS overridden per-subgraph
6        path: /subscriptions # The absolute URL path to use for subgraph subscription endpoints (Default: /ws)
7      subgraphs: # Overrides subscription settings for individual subgraphs
8        reviews: # Overrides settings for the 'reviews' subgraph
9          path: /ws # Absolute path that overrides the preceding '/subscriptions' path for 'all'
10          protocol: graphql_ws # The WebSocket-based subprotocol to use for subscription communication (Default: graphql_ws)
11          heartbeat_interval: 10s # Optional and 'disable' by default, also supports 'enable' (set 5s interval) and custom values for intervals, e.g. '100ms', '10s', '1m'.

This example enables subscriptions in passthrough mode, which uses long-lived WebSocket connections.

note
  • Each path must be set as an absolute path. For example, given http://localhost:8080/foo/bar/graphql/ws, set the path configuration as path: "/foo/bar/graphql/ws".
  • Subgraph path configurations override the path configuration for all subgraphs.
  • If your subgraph implementation (e.g. DGS) can close idle connections, set heartbeat_interval to keep the connection alive.

The router supports the following WebSocket subprotocols, specified via the protocol option:

  • graphql_ws

    • Used by the graphql-ws library

    • This subprotocol is the default value and is recommended for GraphQL server libraries implementing WebSocket-based subscriptions.

  • graphql_transport_ws

By default, the router uses the graphql_ws protocol option for all subgraphs. You can change this global default and/or override it for individual subgraphs by setting the protocol key as shown above.

Your router creates a separate WebSocket connection for each client subscription, unless it can perform subscription deduplication.

HTTP callback setup

note
  • Your router must use whichever subprotocol is expected by each of your subgraphs.
  • To disambiguate between graph-ws and graph_ws:
    • graph-ws (with a hyphen -) is the name of the library that uses the recommended graphql_ws (with un underscore _) WebSocket subprotocol.
  • Each path must be set as an absolute path. For example, given http://localhost:8080/foo/bar/graphql/ws, set the absolute path as path: "/foo/bar/graphql/ws".
  • The public_url must include the configured path on the router. For example, given a server URL of http://localhost:8080 and the router's path = /my_callback, then your public_url must append the path to the server: http://localhost:8080/my_callback.
  • If you have a proxy in front of the router that redirects queries to the path configured in the router, you can specify another path for the public_url, for example http://localhost:8080/external_path.
  • Given a public_url, the router appends a subscription id to the public_url to get http://localhost:8080/external_access/{subscription_id} then passes it directly to your subgraphs.
  • If you don't specify the path, its default value is /callback, so you'll have to specify it in public_url.

The router provides support for receiving subgraph subscription events via HTTP callbacks, instead of over a persistent WebSocket connection. This callback mode provides the following advantages over WebSocket-based subscriptions:

  • The router doesn't need to maintain a persistent connection for each distinct subscription.

  • You can publish events directly to the router from a pubsub system, instead of routing those events through the subgraph.

Callback mode requires your subgraph library to support the router's HTTP callback protocol.

note
Currently, Apollo Server 4.10 and Spring GraphQL 4.3.0 support this protocol. If you're implementing support in a subgraph library, please create a GitHub discussion.

Here's an example configuration that sets up subgraph subscriptions in callback mode:

YAML
router.yaml
1subscription:
2  enabled: true
3  mode:
4    callback:
5      public_url: https://example.com:4000/callback # The router's public URL, which your subgraphs access, must include the path configured on the router
6      listen: 127.0.0.1:4000 # The IP address and port the router will listen on for subscription callbacks, for security reasons it might be better to expose on another port that is only available from your internal network
7      path: /callback # The path of the router's callback endpoint
8      heartbeat_interval: 5s # Optional (default: 5secs)
9      subgraphs: # The list of subgraphs that use the HTTP callback protocol
10        - accounts

You can disable the heartbeat by setting heartbeat_interval: disabled. This is useful for example if you're running in callback mode in an infrastructure based on lambda functions, where you prefer neither to send heartbeats nor to keep a lambda awake just to send heartbeats to subscriptions.

caution
Once the heartbeat is disabled, you must manage how a subscription is closed from the server side.If something crashes on your side for a specific subscription on specific events, but you don't manage closing the subscription, you can end up with a subscription still opened on the client but without any events to notify you on the client side that the subscription has crashed.Also, when handling subscriptions with heartbeats disabled, make sure to store a subscription's request payload (including extensions data with callback URL and verifier) to be able to send the right events on the right callback URL when you start your lambda function triggered by an event.

Using a combination of modes

If some of your subgraphs require passthrough mode and others require callback mode for subscriptions, you can apply different modes to different subgraphs in your configuration:

YAML
router.yaml
1subscription:
2  enabled: true
3  mode:
4    passthrough:
5      subgraphs:
6        reviews:
7          path: /ws
8          protocol: graphql_ws
9    callback:
10      public_url: http://public_url_of_my_router_instance:4000/callback # This must include the path configured on the router
11      listen: 127.0.0.1:4000
12      path: /callback
13      subgraphs:
14        - accounts

In this example, the reviews subgraph uses WebSocket and the accounts subgraph uses HTTP-based callbacks.

caution
If you configure both passthrough mode and callback mode for a particular subgraph, the router uses the passthrough mode configuration.If any subgraphs require callback mode, do not set the passthrough.all key. If you do, the router uses the passthrough mode configuration for all subgraphs.

Example execution

Let's say our supergraph includes the following subgraphs and partial schemas:

GraphQL
Products subgraph
1type Product @key(fields: "id") {
2  id: ID!
3  name: String!
4  price: Int!
5}
6
7type Subscription {
8  productPriceChanged: Product!
9}
GraphQL
Reviews subgraph
1type Product @key(fields: "id") {
2  id: ID!
3  reviews: [Review!]!
4}
5
6type Review {
7  score: Int!
8}

Now, let's say a client executes the following subscription against our router (over HTTP):

GraphQL
1subscription OnProductPriceChanged {
2  productPriceChanged {
3    # Defined in Products subgraph
4    name
5    price
6    reviews {
7      # Defined in Reviews subgraph
8      score
9    }
10  }
11}

When our router receives this operation, it executes a corresponding subscription operation against the Products subgraph (over a new WebSocket connection):

GraphQL
1subscription {
2  productPriceChanged {
3    id # Added for entity fetching
4    name
5    price
6    # Reviews fields removed
7  }
8}
note
  • This operation adds the Product.id field. The router needs @key fields of the Product entity to merge entity fields from across subgraphs.
  • This operation removes all fields defined in the Reviews subgraph, because the Products subgraph can't resolve them.

At any point after the subscription is initiated, the Products subgraph might send updated data to our router. Whenever this happens, the router does not immediately return this data to the client, because it's missing requested fields from the Reviews subgraph.

Instead, our router executes a standard GraphQL query against the Reviews subgraph to fetch the missing entity fields:

GraphQL
1query {
2  entities(representations: [...]) {
3    ... on Product {
4      reviews {
5        score
6      }
7    }
8  }
9}

After receiving this query result from the Reviews subgraph, our router combines it with the data from Products and returns the combination to the subscribing client.

Trying subscriptions with curl

To quickly try out the GraphOS router's HTTP-based subscriptions without setting up an Apollo Client library, you can execute a curl command against your router with the following format:

Bash
1 curl 'http://localhost:4000/' -v \
2  -H 'accept: multipart/mixed;subscriptionSpec=1.0, application/json' \
3  -H 'content-type: application/json' \
4  --data-raw '{"query":"subscription OnProductPriceChanged { productPriceChanged { name price reviews { score } } }","operationName":"OnProductPriceChanged"}'

This command creates an HTTP multipart request and keeps an open connection that receives new subscription data in response "chunks":

Text
1--graphql
2content-type: application/json
3
4{}
5--graphql
6content-type: application/json
7
8{"payload":{"data":{"productPriceChanged":{"name":"Croissant","price":400,"reviews":[{"score":5}]}}}}
9--graphql
10content-type: application/json
11
12{"payload":{"data":{"productPriceChanged":{"name":"Croissant","price":375,"reviews":[{"score":5}]}}}}
13--graphql
14content-type: application/json
15
16{"payload":{"data":{"productPriceChanged":{"name":"Croissant","price":425,"reviews":[{"score":5}]}}}}
17--graphql--

This example subscription only emits three events and then directly closes the connection.

For more information on this multipart HTTP subscription protocol, see this article.

Subscription deduplication

By default, the router deduplicates identical subscriptions. This can dramatically reduce load on both your router and your subgraphs, because the router doesn't need to open a new connection if an existing connection is already handling the exact same subscription.

For example, if thousands of clients all subscribe to real-time score updates for the same sports game, your router only needs to maintain one connection to your sportsgames subgraph to receive events for all of those subscriptions.

The router considers subscription operations identical if all of the following are true:

  • The operations sent to the subgraph have identical GraphQL selection sets (i.e., requested fields).

  • The operations provide identical values for all headers that the router sends to the subgraph.

Disabling deduplication

You can disable subscription deduplication by adding the following to your router's YAML config file under the subscription key:

YAML
router.yaml
1subscription:
2  enabled: true
3  enable_deduplication: false # default: true

Note that this is a global setting (not per-subgraph or per-operation).

Why disable deduplication?

Disabling deduplication is useful if you need to create a separate connection to your subgraph for each client-initiated subscription. For example:

  • Your subgraph needs to trigger an important event every time a new client subscribes to its data.

    • This event doesn't trigger whenever the router reuses an existing connection.

  • Your subscription needs to start by receiving the first value in a particular sequence, instead of the most recent value.

    • If a subscription reuses an existing connection, it starts by receiving the next value for that connection.

    • As a basic example, let's say a subscription should always fire events returning the integers 0 through 1000, in order. If a new subscription reuses an existing subgraph connection, it starts by receiving whichever value is next for the original connection, which is almost definitely not 0.

Advanced configuration

Termination on schema update

Whenever your router's supergraph schema is updated, the router terminates all active subscriptions.

Your router's supergraph schema is updated in the following cases:

  • Your router regularly polls GraphOS for its supergraph schema, and an updated schema becomes available.

  • Your router obtains its supergraph schema from a local file, which it watches for updates if the --hot-reload option is set.

When the router terminates subscriptions this way, it sends the following as a final response payload to all active subscribing clients:

JSON
1{
2  "errors": [
3    {
4      "message": "subscription has been closed due to a schema reload",
5      "extensions": {
6        "code": "SUBSCRIPTION_SCHEMA_RELOAD"
7      }
8    }
9  ]
10}

A client that receives this SUBSCRIPTION_SCHEMA_RELOAD error code can reconnect by executing a new subscription operation.

WebSocket auth support

By default, if you've configured your router to propagate HTTP Authorization headers to your subgraph, then the router automatically sets corresponding connectionParams when initiating a WebSocket connection to that subgraph.

For example, when your router sends the connection_init message to a subgraph, it includes the value of the Authorization header via the following payload:

JSON
1{
2  "connectionParams": {
3    "token": "CONTENTS_OF_AUTHORIZATION_HEADER"
4  }
5}

To specify a custom payload for theconnection_init message, you can write a Rhai script and use the context directly:

Rhai
1fn subgraph_service(service, subgraph) {
2  let params = Router.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS;
3  let f = |request| {
4    request.context[params] = #{
5			my_token: "here is my token"
6		};
7  };
8
9  service.map_request(f);
10}
note
If you specify both a context entry and an Authorization header, the context entry takes precedence.

Expanding event queue capacity

If your router receives a high volume of events for a particular subscription, it might accumulate a backlog of those events to send to clients. To handle this backlog, the router maintains an in-memory queue of unsent events.

The router maintains a separate event queue for each of its active subscription connections to subgraphs.

You can configure the size of each event queue in your router's YAML config file, like so:

YAML
router.yaml
1subscription:
2  enabled: true
3  queue_capacity: 100000 # Default: 128

The value of queue_capacity corresponds to the maximum number of subscription events for each queue, not the total size of those events.

Whenever your router receives a subscription event when its queue is full, it discards the oldest unsent event in the queue and enqueues the newly received event. The discarded event is not sent to subscribing clients.

If it's absolutely necessary for clients to receive every subscription event, increase the size of your event queue as needed.

Limiting the number of client connections

Client subscriptions are long-lived HTTP connections, which means they might remain open indefinitely. You can limit the number of simultaneous client subscription connections in your router's YAML config file, like so:

YAML
router.yaml
1subscription:
2  enabled: true
3  max_opened_subscriptions: 150 # Only 150 simultaneous connections allowed

If a client attempts to execute a subscription on your router when it's already at max_open_subscriptions, the router rejects the client's request with an error.

Feedback

Edit on GitHub

Forums