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 support enabled, you can add Subscription to the schema of any that supports common WebSocket protocols for communication:

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

What are subscriptions for?

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 against your over HTTP:

    Example subscription
    subscription OnStockPricesChanged {
    stockPricesChanged {
    symbol
    price
    }
    }
    • The client doesn't use a WebSocket protocol. Instead, it receives updates via multipart HTTP responses.
    • By using HTTP for , clients can execute all types over HTTP instead of using two different protocols.
    • Apollo Client, Apollo Kotlin, and Apollo iOS all support over HTTP with minimal configuration. See each library's documentation for details. also provides network adapters for the Relay and urql libraries.
  2. When your receives a , it executes that same subscription against whichever defines the requested stockPricesChanged in the code snippet above.

    • This communication usually does use a WebSocket subprotocol, for compatibility with most libraries.
    • With a , you can also configure an HTTP-callback-based protocol.
  3. The periodically sends new data to your . Whenever it does, the router returns that data to the client in an additional HTTP response part.

    • A can include federated that are defined in other . If it does, the 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 updates its at runtime, it terminates all active . 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 to your , do all of the following in the order shown to prevent schema errors:

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

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

    • 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 don't support .
  4. Modify your to use 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 that don't define any Subscription .
  5. If you're using to implement , update your Apollo Server instances to version 4 or later.

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

Router setup

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

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

WebSocket setup

Here's an example configuration snippet that sets up 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)

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

The 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 .
  • graphql_transport_ws

By default, the uses the graphql_ws protocol option for all . 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 , unless it can perform subscription deduplication.

HTTP callback setup

NOTE

  • Your must use whichever subprotocol is expected by each of your .
  • 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 . For example, given a server URL of http://localhost:8080 and the '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 that redirects queries to the path configured in the , you can specify another path for the public_url, for example http://localhost:8080/external_path.
  • Given a public_url, the appends a id to the public_url to get {http://localhost:8080/external_access/{subscription_id}} then passes it directly to your .
  • If you don't specify the path, its default value is /callback, so you'll have to specify it in public_url.

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

  • The doesn't need to maintain a persistent connection for each distinct .
  • 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.

NOTE

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

Here's an example configuration that sets up 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_ms: 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 .

⚠️ CAUTION

Once the heartbeat is disabled, you must manage how a is closed from the server side.

If something crashes on your side for a specific 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 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 require passthrough mode and others require callback mode for , you can apply different modes to different 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 uses WebSocket and the accounts uses HTTP-based callbacks.

⚠️ CAUTION

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

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

Example execution

Let's say our includes the following 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 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 operation against the Products (over a new WebSocket connection):

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

NOTE

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

At any point after the 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 from the Reviews subgraph.

Instead, our executes a standard against the Reviews to fetch the missing :

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

After receiving this 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 's HTTP-based without setting up an library, you can execute a curl command against your 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 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 only emits three events and then directly closes the connection.

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

Subscription deduplication

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

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 .

The considers identical if all of the following are true:

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

Disabling deduplication

You can disable 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 . 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 needs to start by receiving the first value in a particular sequence, instead of the most recent value.
    • If a reuses an existing connection, it starts by receiving the next value for that connection.
    • As a basic example, let's say a should always fire events returning the integers 0 through 1000, in order. If a new 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 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 .

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 , 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 connections to .

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 events for each queue, not the total size of those events.

Whenever your receives a 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 event, increase the size of your event queue as needed.

Limiting the number of client connections

Client are long-lived HTTP connections, which means they might remain open indefinitely. You can limit the number of simultaneous client 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 on your when it's already at max_open_subscriptions, the 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