Skip to content

Commit 57dff02

Browse files
committed
ok encorporate news
1 parent 9588045 commit 57dff02

4 files changed

Lines changed: 137 additions & 10 deletions

File tree

packages/documentation/content/docs/router/configuration/traffic_shaping.mdx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ itself (i.e. how it handles incoming client requests).
2929

3030
Configures inbound in-flight request deduplication. When enabled, identical concurrent
3131
incoming GraphQL `query` requests are coalesced into a single execution, with the result shared
32-
among all waiting clients. This is distinct from the outbound `dedupe_enabled`, which
33-
deduplicates the requests the router sends to subgraphs.
32+
among all waiting clients. Subscriptions are also deduplicated: N clients subscribing to the same
33+
operation with the same variables result in exactly one upstream subgraph connection, with events
34+
fanned out to all connected clients via a broadcast channel.
35+
36+
This is distinct from the outbound `dedupe_enabled`, which deduplicates the requests the router
37+
sends to subgraphs.
3438

3539
For a detailed explanation and tuning guidance see the
3640
[Request Deduplication section](/docs/router/guides/performance-tuning#request-deduplication) of
@@ -107,6 +111,25 @@ traffic_shaping:
107111

108112
</Tabs>
109113

114+
### `router.max_long_lived_clients`
115+
116+
- **Type:** `integer`
117+
- **Default:** `128`
118+
119+
```yaml
120+
traffic_shaping:
121+
router:
122+
max_long_lived_clients: 256
123+
```
124+
125+
Limits the maximum number of concurrent long-lived client connections: WebSocket connections and
126+
HTTP streaming responses (SSE, Incremental Delivery, Multipart HTTP). When the limit is reached,
127+
new long-lived connection attempts are rejected with `503 Service Unavailable` and a
128+
`Retry-After: 5` response header. Regular HTTP requests (queries and mutations) are not affected.
129+
130+
This limit only activates when at least one of WebSocket or Subscriptions is enabled. Setting it
131+
to `0` disables the limit entirely.
132+
110133
## Outbound Options
111134

112135
The following options (`dedupe_enabled`, `pool_idle_timeout`, and `request_timeout`) can be set

packages/documentation/content/docs/router/guides/performance-tuning.mdx

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,8 @@ independently: **inbound** and **outbound**.
9393

9494
### Inbound Deduplication
9595

96-
Inbound deduplication (`traffic_shaping.router.dedupe`) operates at the entry point of the
97-
router. When multiple clients send identical GraphQL `query` operations simultaneously, the router
98-
executes the operation only once and shares the result with all waiting clients — subgraphs receive
99-
just a single request regardless of how many clients are waiting.
96+
Inbound deduplication (`traffic_shaping.router.dedupe`) operates at the entry point of the router
97+
and applies to both queries and subscriptions.
10098

10199
- **Default:** `false` (opt-in)
102100

@@ -107,7 +105,65 @@ traffic_shaping:
107105
enabled: true
108106
```
109107
110-
**Deduplication key**
108+
#### Queries
109+
110+
When multiple clients send identical GraphQL `query` operations simultaneously, the router
111+
executes the operation only once and shares the result with all waiting clients - subgraphs receive
112+
just a single request regardless of how many clients are waiting.
113+
114+
#### Subscriptions
115+
116+
When multiple clients subscribe to the same operation with the same variables, the router opens
117+
exactly one upstream subgraph connection. Events from that upstream are broadcast to all connected
118+
clients via a shared channel.
119+
120+
This is fundamentally different from query deduplication because subscriptions are long-lived
121+
streams. The upstream connection stays open as long as at least one client is subscribed. When
122+
the upstream finishes or all clients disconnect, the entry is removed and the next matching client
123+
starts a fresh upstream.
124+
125+
One consequence: **late joiners do not receive replayed events.** A client that joins an
126+
already-running subscription receives events from the moment it subscribes onward. Events already
127+
delivered to earlier clients are not replayed.
128+
129+
##### Transport-agnostic deduplication
130+
131+
The fingerprint space is shared between HTTP and WebSocket transports. A subscription started over
132+
HTTP and an identical one over WebSocket produce the same fingerprint and can deduplicate against
133+
each other. The same applies to queries: a query sent over WebSocket deduplicates with the same
134+
query sent over HTTP.
135+
136+
However, the `Accept` header is part of the fingerprint when `headers` is set to `all` (the
137+
default). HTTP streaming clients send `Accept: text/event-stream` or `Accept: multipart/mixed`
138+
while WebSocket clients send no `Accept` header, so they produce different fingerprints by default
139+
and do not deduplicate against each other.
140+
141+
To achieve cross-transport subscription deduplication - where a WebSocket subscription and an SSE
142+
subscription for the same operation share one upstream connection - configure `headers: none` or
143+
use an explicit include list that omits transport-specific headers:
144+
145+
```yaml
146+
traffic_shaping:
147+
router:
148+
dedupe:
149+
enabled: true
150+
headers: none
151+
```
152+
153+
or with an explicit include list:
154+
155+
```yaml
156+
traffic_shaping:
157+
router:
158+
dedupe:
159+
enabled: true
160+
headers:
161+
include:
162+
- authorization
163+
- x-tenant-id
164+
```
165+
166+
#### Deduplication key
111167

112168
Two requests are considered identical when the following all match:
113169

@@ -118,7 +174,7 @@ Two requests are considered identical when the following all match:
118174
- Schema checksum (prevents sharing across schema reload transitions)
119175
- Selected request headers (controlled by the `headers` policy below)
120176

121-
**Header policy**
177+
#### Header policy
122178

123179
By default all headers are included in the fingerprint, so requests with different `Authorization`
124180
or `Cookie` headers are **not** deduplicated with each other. You can narrow this down:
@@ -170,11 +226,12 @@ traffic_shaping:
170226

171227
- Many clients frequently issue the same popular queries (dashboards, landing pages, product
172228
listings)
173-
- You want to reduce overall query execution pressure on your subgraphs under concurrent load
229+
- Many clients subscribe to the same data (live scoreboards, shared feeds, broadcast events)
230+
- You want to reduce overall execution pressure on your subgraphs under concurrent load
174231

175232
**When you might leave it disabled:**
176233

177-
- All queries are highly personalised and rarely identical
234+
- All operations are highly personalised and rarely identical
178235
- You're debugging and want every request to execute independently
179236

180237
### Outbound Deduplication

packages/documentation/content/docs/router/subscriptions/index.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ The protocols used between the router and subgraphs are independent from those u
184184

185185
If a client requests a subscription over an unsupported transport, the router returns `406 Not Acceptable`.
186186

187+
## Deduplication
188+
189+
Subscriptions participate in the same inbound deduplication mechanism as queries. When multiple clients subscribe to the same operation with the same deduplication parameters, the router opens exactly one upstream subgraph connection and fans events out to all connected clients via a broadcast channel.
190+
191+
See [Inbound Deduplication](/docs/router/guides/performance-tuning#inbound-deduplication) for how to enable and configure it.
192+
193+
## Schema Reload
194+
195+
When a new supergraph schema is loaded, the router closes all active subscriptions before the schema is swapped in. Each client receives a final error event with the `SUBSCRIPTION_SCHEMA_RELOAD` error code. Clients are expected to handle this code by reconnecting and re-subscribing. After reconnecting, the subscription runs against the new schema.
196+
187197
## Configuration
188198

189199
Subscriptions are disabled by default. To enable them, set `enabled: true` in the `subscriptions` section of your router configuration:
@@ -198,3 +208,38 @@ You can also enable subscriptions using the `SUBSCRIPTIONS_ENABLED` environment
198208
```bash
199209
SUBSCRIPTIONS_ENABLED=true
200210
```
211+
212+
### Broadcast Capacity
213+
214+
Controls the buffer size of the internal broadcast channel used to fan subscription events out to clients.
215+
216+
When a consumer falls behind and the buffer fills up, it skips missed messages and continues from
217+
the latest available event. The consumer is not disconnected - there is a trace log when this
218+
happens. Increase this value if consumers are frequently falling behind on high-throughput
219+
subscriptions.
220+
221+
```yaml
222+
subscriptions:
223+
enabled: true
224+
broadcast_capacity: 64 # defaults to 32
225+
```
226+
227+
### Long-Lived Client Limit
228+
229+
HTTP streaming responses (SSE, Incremental Delivery, Multipart HTTP) and WebSocket connections are
230+
long-lived - they hold an open connection for the duration of the subscription. To protect the
231+
router from resource exhaustion, a global limit caps the number of these connections that can be
232+
active at the same time. Regular HTTP requests (queries and mutations) are not counted toward this
233+
limit.
234+
235+
When the limit is reached, new long-lived connection attempts are rejected with
236+
`503 Service Unavailable` and a `Retry-After: 5` response header. The limit defaults to `128` and
237+
is configurable via [`traffic_shaping.router.max_long_lived_clients`](/docs/router/configuration/traffic_shaping#routermax_long_lived_clients):
238+
239+
```yaml
240+
traffic_shaping:
241+
router:
242+
max_long_lived_clients: 256
243+
```
244+
245+
Set it to `0` to disable the limit entirely.

packages/documentation/content/docs/router/subscriptions/websockets.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Client WebSocket support is configured at the root level under `websocket` - it
5252

5353
Each WebSocket connection from a client is long-lived and can carry multiple GraphQL operations. For every operation sent over the connection - including single-shot operations like queries and mutations - the router creates a **synthetic HTTP request**. This means every operation goes through the full [plugin system](/docs/router/plugin-system) exactly as if it were a regular HTTP request. Authorization, header manipulation, rate limiting, and all other plugins apply uniformly regardless of whether the client connected over HTTP or WebSocket.
5454

55+
Because operations are processed as synthetic HTTP requests, they share the same fingerprint space as regular HTTP requests. A query sent over WebSocket can deduplicate with the same query sent over HTTP, and a WebSocket subscription can deduplicate with the same subscription started over SSE (when the `Accept` header is excluded from the fingerprint). See [Inbound Deduplication](/docs/router/guides/performance-tuning#inbound-deduplication) for details.
56+
5557
### Configuration
5658

5759
```yaml

0 commit comments

Comments
 (0)