Docs
Launch GraphOS Studio
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.

support is also available for with a or . For cloud router subscription information, refer to the cloud-specific docs.

provides support for :

subscription OnStockPricesChanged {
stockPricesChanged {
symbol
price
}
}

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

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

What are subscriptions for?

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

GraphOS RouterGraphQL ClientGraphOS RouterGraphQL ClientNew data availableNew data availableInitiates subscriptionSends new dataSends new data

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

Subscribes
over WebSocket
(or via callback)
Can query for
entity fields
as needed
Subscribes
over HTTP
Client
GraphOS
Router
Stocks
subgraph
Portfolios
subgraph

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

    Example subscription
    subscription OnStockPricesChanged {
    stockPricesChanged {
    symbol
    price
    }
    }
  2. When your router receives a subscription, it executes that same subscription against whichever subgraph defines the requested stockPricesChanged in the code snippet above.

    • This communication usually does use a WebSocket subprotocol, for compatibility with most subgraph libraries.
    • With a , 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 fields that are defined in other . If it does, the router first fetches those fields by 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 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 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 .
  3. If you compose your router's supergraph schema with GraphOS (instead of with the ), update your build pipeline to use 2.4 or later.

    • Previous versions of Apollo Federation don't support subscription operations.
  4. Modify your 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 subgraph schemas that don't define any Subscription fields.
  5. If you're using 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:

router.yaml
subscription:
enabled: true
mode:
passthrough:
all: # The router uses these subscription settings UNLESS overridden per-subgraph
path: /subscriptions # The absolute 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 # Absolute path that overrides '/subscriptions' defined above
protocol: graphql_ws # The WebSocket-based subprotocol to use for subscription communication (Default: graphql_ws)
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: If your subgraph implementation (e.g. DGS) can close idle connection, set heartbest_interval to keep 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 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:

router.yaml
subscription:
enabled: true
mode:
callback:
public_url: https://example.com:4000/callback # The router's public URL, which your subgraphs access, must include the path configured on the router
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
path: /callback # The path of the router's callback endpoint
heartbeat_interval: 5s # Optional (default: 5secs)
subgraphs: # The list of subgraphs that use the HTTP callback protocol
- 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 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:

router.yaml
subscription:
enabled: true
mode:
passthrough:
subgraphs:
reviews:
path: /ws
protocol: graphql_ws
callback:
public_url: http://public_url_of_my_router_instance:4000/callback # This must include the path configured on the router
listen: 127.0.0.1:4000
path: /callback
subgraphs:
- 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 includes the following subgraphs and partial schemas:

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 router (over HTTP):

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

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

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

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 against the Reviews subgraph to fetch the missing entity fields:

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

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 's HTTP-based subscriptions without setting up an Apollo Client library, you can execute a curl command against your router with the following format:

curl 'http://localhost:4000/' -v \
-H 'accept: multipart/mixed;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 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:

router.yaml
subscription:
enabled: true
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:

{
"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 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:

{
"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 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:

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 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:

router.yaml
subscription:
enabled: true
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.

Previous
Query batching
Next
Subgraph protocol: HTTP callback
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company