Configure GraphQL Subscription Support
Enable clients to receive real-time updates
GraphOS routers provides support for GraphQL subscription operations:
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:
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
A client executes a GraphQL subscription operation against your router over HTTP:
GraphQLExample subscription1subscription 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.
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.
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:
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.
Make sure your router is connected to a GraphOS Enterprise organization.
Subscription support is an Enterprise feature of self-hosted routers.
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.
Modify your subgraph schemas to use Apollo Federation 2.4 or later:
GraphQLstocks.graphql1extend 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.
If you're using Apollo Server to implement subgraphs, update your Apollo Server instances to version 4 or later.
Follow the Apollo Server 4 guide for enabling subscriptions.
Update
@apollo/subgraph
.
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:
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.
- Each
path
must be set as an absolute path. For example, givenhttp://localhost:8080/foo/bar/graphql/ws
, set the path configuration aspath: "/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
Legacy subprotocol used by the
subscriptions-transport-ws
library, which is unmaintained but provided for backward compatibility.
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
- Your router must use whichever subprotocol is expected by each of your subgraphs.
- To disambiguate between
graph-ws
andgraph_ws
:graph-ws
(with a hyphen-
) is the name of the library that uses the recommendedgraphql_ws
(with un underscore_
) WebSocket subprotocol.
- Each
path
must be set as an absolute path. For example, givenhttp://localhost:8080/foo/bar/graphql/ws
, set the absolute path aspath: "/foo/bar/graphql/ws"
. - The
public_url
must include the configuredpath
on the router. For example, given a server URL ofhttp://localhost:8080
and the router'spath
=/my_callback
, then yourpublic_url
must append thepath
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 thepublic_url
, for examplehttp://localhost:8080/external_path
. - Given a
public_url
, the router appends a subscription id to thepublic_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 inpublic_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.
Here's an example configuration that sets up subgraph subscriptions in callback mode:
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.
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:
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.
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:
1type Product @key(fields: "id") {
2 id: ID!
3 name: String!
4 price: Int!
5}
6
7type Subscription {
8 productPriceChanged: Product!
9}
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):
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):
1subscription {
2 productPriceChanged {
3 id # Added for entity fetching
4 name
5 price
6 # Reviews fields removed
7 }
8}
- This operation adds the
Product.id
field. The router needs@key
fields of theProduct
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:
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:
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":
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:
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
through1000
, 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 not0
.
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:
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:
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:
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}
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:
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:
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.