Join us for GraphQL Summit, October 10-12 in San Diego. Use promo code ODYSSEY for $400 off your pass.
Docs
Launch GraphOS Studio

GraphQL subscriptions with a self-hosted router

Real-time data with GraphOS Enterprise


⚠️ For self-hosted routers, subscription support is an Enterprise feature.

Subscription support is also available for cloud s with a Serverless plan. See the docs.

With GraphOS Enterprise, self-hosted instances of the Apollo provide support for GraphQL subscription s:

subscription OnStockPricesChanged {
stockPricesChanged {
symbol
price
}
}

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

stocks.graphql
type Subscription {
stockPricesChanged: [Stock!]!
}

⚠️ Important: To use subscriptions with your self-hosted , you must first complete certain prerequisites.

What are subscriptions for?

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

GraphQL ClientApollo RouterInitiates subscriptionNew data availableSends new dataNew data availableSends new dataGraphQL ClientApollo Router

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 it works

Your infrastructure
Subscribes
over WebSocket
(or via callback)
Can query for
entity fields
as needed
Subscribes
over HTTP
Apollo
Router
Stocks
subgraph
Portfolios
subgraph
Client
  1. A client executes a GraphQL subscription against your self-hosted over HTTP:

    Example subscription
    subscription OnStockPricesChanged {
    stockPricesChanged {
    symbol
    price
    }
    }
    • The client does not use a WebSocket protocol! Instead, it receives updates via multipart HTTP responses.
    • By using HTTP for subscriptions, clients can execute all GraphQL types over HTTP instead of using two different protocols.
    • Apollo Client for Web, Kotlin, and iOS all support GraphQL subscriptions over HTTP with minimal configuration. See each library's documentation for details.
  2. When your receives a subscription, it executes that same subscription against whichever defines the requested (stockPricesChanged in the example above).

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

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

Special considerations

  • Whenever your updates its 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 composition errors:

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

    • Previous versions of the Apollo don't support subscription s.
  2. Make sure your is connected to a GraphOS Enterprise organization.

    • Subscription support is an Enterprise feature of self-hosted s.
  3. If you compose your router's supergraph schema with GraphOS (instead of with the CLI), update your build pipeline to use Apollo Federation 2.4 or later.

    • Previous versions of Apollo Federation don't support subscription s.
  4. Modify your s to use Apollo Federation 2.4 or later:

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

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

Router setup

⚠️ Make sure you've completed all prerequisites!

In your 's YAML config file, you configure how the communicates with each of your s when executing GraphQL subscriptions.

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

WebSocket setup

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

router.yaml
subscription:
enabled: true
mode:
passthrough:
all: # The router uses these subscription settings UNLESS overridden per-subgraph
path: /subscriptions # The URL path to use for subgraph subscription endpoints (Default: /ws)
subgraphs: # Overrides subscription settings for individual subgraphs
reviews: # Overrides settings for the 'reviews' subgraph
path: /ws # Overrides '/subscriptions' defined above
protocol: graphql_transport_ws # The WebSocket-based subprotocol to use for subscription communication (Default: graphql_ws)

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

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

  • graphql_ws
  • graphql_transport_ws
    • Used by the graphql-ws library
    • This subprotocol is recommended for GraphQL server libraries implementing WebSocket-based subscriptions.

⚠️ Your router must use whichever subprotocol is expected by each of your subgraphs!

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

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

HTTP callback setup (preview)

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

  • The doesn't need to maintain a persistent connection for each distinct subscription.
  • You can publish events directly to the from a pubsub system, instead of routing those events through the .

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

This HTTP callback protocol is in preview! Breaking changes to the protocol might occur during the preview period.

Currently, no libraries support this protocol (Apollo Server support is forthcoming). If you're implementing support in a subgraph library, please create a GitHub discussion.

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

router.yaml
subscription:
enabled: true
mode:
preview_callback:
public_url: https://example.com:4000 # The router's public URL
listen: 0.0.0.0:4000 # The IP address and port the router will listen on for subscription callbacks
path: /callback # The path of the router's callback endpoint
subgraphs: # The list of subgraphs that use the HTTP callback protocol
- accounts

Using a combination of modes

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

router.yaml
subscription:
enabled: true
mode:
passthrough:
subgraphs:
reviews:
path: /ws
protocol: graphql_transport_ws
preview_callback:
public_url: http://public_url_of_my_router_instance:4000
listen: 0.0.0.0:4000
path: /callback
subgraphs:
- accounts

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

Important: If you configure both passthrough mode and callback mode for a particular , the uses the passthrough mode configuration.

If any s require callback mode, do not set the passthrough.all key. If you do, the uses the passthrough mode configuration for all s.

Example execution

Let's say our includes the following s and partial s:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int!
}
type Subscription {
productPriceChanged: Product!
}
Reviews subgraph
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]!
}
type Review {
score: Int!
}

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

subscription OnProductPriceChanged {
productPriceChanged {
# Defined in Products subgraph
name
price
reviews {
# Defined in Reviews subgraph!
score
}
}
}

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

subscription {
productPriceChanged {
id # Added for entity fetching
name
price
# Reviews fields removed!
}
}

Note the following:

  • This adds the Product.id . The needs @key s of the Product entity to merge entity s from across s.
  • This removes all s defined in the Reviews , because the Products subgraph can't resolve them.

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

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

query {
_entities(representations: [...]) {
... on Product {
reviews {
score
}
}
}
}

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

Trying subscriptions with curl

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

curl 'http://localhost:4000/' -v \
-H 'accept: multipart/mixed; boundary="graphql"; subscriptionSpec=1.0, application/json' \
-H 'content-type: application/json' \
--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":

--graphql
content-type: application/json
{}
--graphql
content-type: application/json
{"payload":{"data":{"productPriceChanged":{"name":"Croissant","price":400,"reviews":[{"score":5}]}}}}
--graphql
content-type: application/json
{"payload":{"data":{"productPriceChanged":{"name":"Croissant","price":375,"reviews":[{"score":5}]}}}}
--graphql
content-type: application/json
{"payload":{"data":{"productPriceChanged":{"name":"Croissant","price":425,"reviews":[{"score":5}]}}}}
--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 Apollo Router deduplicates identical subscriptions. This can dramatically reduce load on both your and your s, because the 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 only needs to maintain one connection to your sportsgames to receive events for all of those subscriptions.

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

  • The s sent to the have identical GraphQL selection sets (i.e., requested s).
  • The s provide identical values for all headers that the sends to the .

Disabling deduplication

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

router.yaml
subscription:
enabled: true
enable_deduplication: false # default: true

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

Why disable deduplication?

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

  • Your needs to trigger an important event every time a new client subscribes to its data.
    • This event doesn't trigger whenever the 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 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 's is updated, the router terminates all active subscriptions.

Your 's is updated in the following cases:

  • Your regularly polls for its , and an updated schema becomes available.
  • Your obtains its from a local file, which it watches for updates if the --hot-reload option is set.

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

{
"errors": [
{
"message": "subscription has been closed due to a schema reload",
"extensions": {
"code": "SUBSCRIPTION_SCHEMA_RELOAD"
}
}
]
}

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

WebSocket auth support

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

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

{
"connectionParams": {
"token": "CONTENTS_OF_AUTHORIZATION_HEADER"
}
}

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

fn subgraph_service(service, subgraph) {
let params = Router.APOLLO_SUBSCRIPTION_WS_CUSTOM_CONNECTION_PARAMS;
let f = |request| {
request.context[params] = #{
my_token: "here is my token"
};
};
service.map_request(f);
}

Note: If you specify both a context entry and an Authorization header, the context entry takes precedence.

Expanding event queue capacity

If your 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 maintains a separate event queue for each of its active subscription connections to s.

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

router.yaml
subscription:
enabled: true
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 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 's YAML config file, like so:

router.yaml
subscription:
enabled: true
max_opened_subscriptions: 150 # Only 150 simultaneous connections allowed

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

Previous
Request format
Next
Subgraph protocol: HTTP callback
Edit on GitHubEditForumsDiscord