releases.shpreview
Apollo GraphQL/Apollo Router

Apollo Router

Mon
Wed
Fri
JunJulAugSepOctNovDecJanFebMarAprMay
Less
More
Releases27Avg8/moVersionsv2.10.1 to v2.15.0

🐛 Fixes

Preserve null propagation when multiple fragments select the same non-null field (PR #9032)

When a query uses multiple fragment spreads on the same parent type and a subgraph response is missing a required non-null field on a union member, the router now correctly returns null for the affected field rather than a partial object like {"__typename": "A"}.

The GraphQL specification requires that a non-null violation propagates null upward to the nearest nullable parent. Previously, if one fragment nullified a field, a subsequent fragment on the same parent could overwrite that null with a partial result — producing a spec-incorrect response.

By @abernix in https://github.com/apollographql/router/pull/9032

Reject invalid values for client library name and version (PR #8934)

Rejects invalid values (validated against a regex) for library name and version provided in headers or operation extensions, which are used for the Client Awareness feature and telemetry.

By @BoD in https://github.com/apollographql/router/pull/8934

Only delete coprocessor context keys from those that were sent in a given stage (PR #9519)

Addresses a race condition where context keys added by concurrent parallel subgraph stages could unintentionally be deleted.

By @rohan-b99 in https://github.com/apollographql/router/pull/9519

Normalize supergraph.path to support queries with and without trailing slashes (/) (PR #8860)

Normalize trailing / for supergraph.path to support /graphql and /graphql/. This works by stripping trailing / from both the configured path and the incoming query path to ensure they match, regardless of whether the config or query includes a trailing slash.

By @Jephuff in https://github.com/apollographql/router/pull/8860

🐛 Fixes

Support non-ASCII (UTF-8) WebSocket header values (Issue #1485, PR #9051)

The router can now handle WebSocket connections with UTF-8 encoded header values, including non-ASCII characters like "Montréal". Previously, such connections failed because of serialization issues in the underlying tungstenite library.

The fix comes from updating tokio-tungstenite from v0.28.0 to v0.29.0.

By @BobaFetters in https://github.com/apollographql/router/pull/9051

Handle both deprecated enum values when merging coprocessor context (PR #8913)

A change to coprocessor context merges in Router v2.10 caused keys to be deleted when context: true is used as the coprocessor context selector in the router configuration file.

The workaround was to pass context: deprecated instead. This change brings parity when context: true is provided.

By @carodewig in https://github.com/apollographql/router/pull/8913

🛠 Maintenance

Pin transitive h2 dependency at minimum v0.4.13 to pick up critical flow-control, deadlock, and tracing fixes (PR #9033)

h2 0.4.13 (released January 5, 2026) contains three fixes directly relevant to the router, which uses h2 exclusively as a client when connecting to subgraphs:

  • Capacity deadlock under concurrent streams (#860) — high relevance: Under concurrent load with max_concurrent_streams limits in effect, flow-control capacity could be assigned to streams still in pending_open state. Those streams could never consume the capacity, starving already-open streams and permanently freezing all outgoing traffic on the connection with no error surfaced. This is directly triggerable in the router: any subgraph behind Envoy or a gRPC backend advertises a max_concurrent_streams limit (Envoy defaults to 100), and under production load the router will routinely queue more concurrent requests than that limit allows.

  • OTel tracing span lifetime leak (#868) — high relevance: The h2 Connection object captured the active tracing span at connection creation time as its parent, keeping that span alive for the entire lifetime of the connection. Since the router wraps every subgraph request in an OpenTelemetry span and connections are pooled, affected spans could linger indefinitely under sustained traffic — never being exported to the tracing backend and accumulating in memory.

  • Flow-control stall on padded DATA frames (#869) — lower relevance for typical subgraphs, higher for connectors: Padding bytes in DATA frames were not being returned to the flow-control window, causing the connection window to drain to zero and permanently stalling downloads with no error. Typical GraphQL/gRPC subgraphs do not send padded frames, but router connectors calling arbitrary HTTP APIs (e.g., Google Cloud Storage or CDN-backed endpoints) can encounter this.

By @theJC in https://github.com/apollographql/router/pull/9033

🚀 Features

Add ignore_auth_context option to subscription deduplication config (PR #9078)

When the router's JWT authentication plugin validates a token, it decodes the claims and stores them internally on the request — before any subgraph request is built. The router then factors those stored claims into its check for whether two subscriptions are identical, separately from any HTTP headers it may forward downstream.

This means that on any router with JWT authentication enabled, every authenticated user effectively gets their own subgraph WebSocket connection — even if the subscription data is identical for all users, and even if the Authorization header is never forwarded to the subgraph at all. Adding authorization to ignored_headers doesn't help here, because it only affects HTTP headers; the decoded claims live in a different layer that ignored_headers never touches.

Two new capabilities are added to the deduplication config block:

  • ignore_auth_context: bool (default: false) — when true, the router skips stored JWT claims when checking subscription identity, allowing all authenticated users to share a single subgraph WebSocket connection when the subscription data is truly non-personalized (e.g., product price updates, stock price feeds).
  • Per-subgraph deduplication control via all: / subgraphs: — deduplication settings can now be set globally with a default and overridden per subgraph by name, using the standard SubgraphConfiguration<T> pattern already used elsewhere in the router config.
subscription:
  deduplication:
    all:
      enabled: true
    subgraphs:
      stocks:
        ignore_auth_context: true

By @abernix in https://github.com/apollographql/router/pull/9078

Add include_cache_control_header_on_router_response to suppress Cache-Control on client responses (PR #9002)

The response cache plugin now supports a include_cache_control_header_on_router_response boolean config option (defaults to true). When set to false, the router omits the Cache-Control header from supergraph responses sent to clients, while all internal caching behavior — Redis storage, TTL enforcement, cache key computation, and the cache debugger — remains unchanged.

This is useful when the router sits behind a CDN or reverse proxy that manages its own caching headers, or when you want to prevent clients from caching responses locally while keeping server-side caching active.

response_cache:
  enabled: true
  include_cache_control_header_on_router_response: false  # default: true
  subgraph:
    all:
      enabled: true
      redis:
        urls: ["redis://..."]

By @ebylund in https://github.com/apollographql/router/pull/9002

Add per-subgraph and per-connector HTTP response size limits (PR #9160)

The router can now cap the number of bytes it reads from subgraph and connector HTTP response bodies, protecting against out-of-memory conditions when a downstream service returns an unexpectedly large payload.

The limit is enforced as the response body streams in — the router stops reading and returns a GraphQL error as soon as the limit is exceeded, without buffering the full body first.

Configure a global default and optional per-subgraph or per-source overrides:

limits:
  subgraph:
    all:
      http_max_response_size: 10MB # 10 MB for all subgraphs
    subgraphs:
      products:
        http_max_response_size: 20MB # 20 MB override for 'products'

  connector:
    all:
      http_max_response_size: 5MB # 5 MB for all connector sources
    sources:
      products.rest:
        http_max_response_size: 10MB # 10 MB override for 'products.rest'

There is no default limit; responses are unrestricted unless you configure this option.

When a response is aborted due to the limit, the router:

  • Returns a GraphQL error to the client with extension code SUBREQUEST_HTTP_ERROR
  • Increments the apollo.router.limits.subgraph_response_size.exceeded or apollo.router.limits.connector_response_size.exceeded counter
  • Records apollo.subgraph.response.aborted: "response_size_limit" or apollo.connector.response.aborted: "response_size_limit" on the relevant span

Configuration migration: Existing limits fields (previously at the top level of limits) are now nested under limits.router. A configuration migration is included that updates your config file automatically.

By @carodewig in https://github.com/apollographql/router/pull/9160

Add apollo.router.connection.acquire.duration metric for TCP/TLS connection timing (PR #9309)

Adds a new histogram metric, apollo.router.connection.acquire.duration, that records how long it takes to establish a new TCP or Unix socket connection to a downstream service (subgraph, connector, or coprocessor). The metric fires only when the connection pool opens a new connection — pool hits are not recorded.

This metric is useful for diagnosing connection establishment latency. For example, if a subgraph shows elevated overall response latency, a high connection.acquire.duration indicates the delay is in TCP/TLS setup; a near-zero value (or no data) points to post-connection causes like slow server responses.

Attributes:

  • network.transport: tcp for HTTP connections, unix for Unix socket connections
  • subgraph.name: name of the subgraph (for subgraph connections)
  • connector.source.name: name of the connector source (for connector connections)
  • coprocessor: true (for coprocessor connections)

By @carodewig in https://github.com/apollographql/router/pull/9309

Add max_lifetime configuration for subscriptions (PR #9216)

Adds a new max_lifetime field to the subscription configuration block, allowing operators to set a maximum duration for how long a subscription can remain open. After the configured duration the subscription is closed and the client receives a terminal error with extension code SUBSCRIPTION_MAX_LIFETIME_EXCEEDED.

subscription:
  enabled: true
  max_lifetime: 10m  # close subscriptions after 10 minutes
  mode:
    callback:
      public_url: "https://my-router.example.com/subscription/callback"

By default (max_lifetime unset) there is no lifetime limit, preserving the existing behaviour.

By @BobaFetters in https://github.com/apollographql/router/pull/9216

Add operation_body_timeout for file upload requests (PR #9243)

Adds a new operation_body_timeout limit to the file uploads plugin, allowing operators to set a tight deadline on reading the operations field (GraphQL query + variables) from multipart request bodies, independently of the overall router timeout.

File uploads is the only router flow where the request body is read as a stream in the plugin layer: the multipart body must be parsed to extract the operations field before query planning can begin. This means a slow or stalled client can hold a connection open until the global router timeout fires. The new operation_body_timeout lets you set a tighter deadline specifically for that body-reading phase.

If operation_body_timeout is not set, no additional body-read timeout is applied — the overall router timeout remains the only bound.

preview_file_uploads:
  enabled: true
  protocols:
    multipart:
      enabled: true
      limits:
        operation_body_timeout: 5s  # optional; no default

When the timeout fires, the router returns a 504 Gateway Timeout response with extension code GATEWAY_TIMEOUT.

By @carodewig in https://github.com/apollographql/router/pull/9243

🐛 Fixes

Resolve @connect field values when a root query alias is combined with field-level aliases (Issue #9347)

Queries that aliased both the root query field and one or more of its subfields on a @connect-backed type returned null for every aliased subfield, which could cascade into null propagation for non-nullable types. Either alias in isolation worked correctly — only the combination of both triggered the bug.

Given this query:

{
  items: search_items(query: "test") {
    results {
        id 
        link: viewUri 
    }
  }
}

Before

Aliased fields returned null, and null propagation bubbled up through non-nullable types until the entire result was nullified:

{
  "data": { "items": null },
  "extensions": {
    "valueCompletion": [
      { "message": "Null value found for non-nullable type String", "path": ["items", "results", 0] },
      { "message": "Null value found for non-nullable type Item", "path": ["items", "results", 0] },
      { "message": "Null value found for non-nullable type [Item!]", "path": ["items", "results"] }
    ]
  }
}

After

Root and field aliases now work together as expected:

{
  "data": {
    "items": {
      "results": [
        { "id": "1", "link": "https://example.com/docs/001" }
      ]
    }
  }
}

By @jhrldev in https://github.com/apollographql/router/pull/9358

Record apollo.router.operations.coprocessor.duration even when the coprocessor call times out (PR #9296)

apollo.router.operations.coprocessor.duration is now recorded even when a coprocessor call is cut short by a router timeout. Previously, the metric was only emitted when the call completed normally, leaving timeout latencies invisible in the histogram.

By @conwuegb and @carodewig in https://github.com/apollographql/router/pull/9296

Retry reloads automatically after transient failures instead of staying on a stale schema (PR #9391)

Previously, if a schema reload failed — for example, because a persisted query manifest fetch from Uplink encountered a transient network error — the router would log "error while reloading, continuing with previous configuration" and then stop retrying. All subsequent background polls from Uplink would return Unchanged (because last_id had already advanced to the new schema ID), leaving the router permanently serving the old schema until manually restarted.

The router now enters a Reloading state on reload failure and schedules automatic retries. The retry delay (default 10 seconds) and maximum retry count (default 5) are configurable via the new reload configuration key:

reload:
  max_retries: 5    # 0 to disable, null for unlimited
  retry_delay: 10s

The retry budget is reset whenever a new reload trigger arrives — a new schema or license from Uplink, a configuration or rhai script file change, or an explicit reload signal — so any new change always gets a fresh set of attempts even if previous retries were exhausted.

By @carodewig in https://github.com/apollographql/router/pull/9391

Scope SigV4 signing params to a single subgraph request instead of sharing them across the operation (PR #9385)

When authentication.subgraph.subgraphs configured aws_sig_v4 for a specific subgraph, the signing parameters were visible to every other subgraph in the same operation — causing unconfigured subgraphs to have their Authorization header overwritten with AWS credentials.

Signing parameters are now scoped to the individual subgraph HTTP request rather than the operation, so AWS credentials only travel with requests to the subgraph they were configured for.

By @carodewig in https://github.com/apollographql/router/pull/9385

Identify the primary @defer chunk correctly in the is_primary_response telemetry selector (PR #9238)

The is_primary_response: true supergraph telemetry selector returned false for every chunk of a multipart @defer response — including the primary (first) chunk — when evaluated at on_response or on_response_event scope. This made it impossible to distinguish primary from deferred chunks in metrics, events, and conditional telemetry.

The selector now returns true for the primary chunk and false for subsequent deferred chunks, so per-chunk filtering works as documented:

telemetry:
  instrumentation:
    instruments:
      supergraph:
        my.defer.primary.duration:
          value: event_duration
          type: histogram
          attributes:
            is_primary:
              is_primary_response: true

Now produces split metric series (is_primary="true" for the primary chunk, is_primary="false" for deferred chunks) instead of a single series with is_primary="false" for everything.

By @ebylund in https://github.com/apollographql/router/pull/9238

Evaluate on_graphql_error per response part in coprocessors and telemetry for @defer responses (PR #9365)

Previously, the on_graphql_error condition used in coprocessor and telemetry configurations did not work correctly for deferred (@defer) multipart responses:

  • The supergraph coprocessor evaluated the condition once before any stream chunks were consumed, so it never fired for errors that appeared only in incremental chunks.
  • The router coprocessor read the sticky CONTAINS_GRAPHQL_ERROR context flag, which caused it to fire for every subsequent chunk once any earlier chunk had errors — and to never fire if the first chunk was clean.
  • The on_graphql_error telemetry selector at the supergraph stage returned the accumulated error state rather than the current chunk's error state.

The router and supergraph coprocessors, along with telemetry selectors, now evaluate on_graphql_error conditions per response part, so the condition fires exactly once per part that contains GraphQL errors — no more, no less.

Additionally, on_graphql_error: false (fire when there are no errors) now works correctly in all selector contexts: router, supergraph, and subgraph.

By @carodewig in https://github.com/apollographql/router/pull/9365

Drop duplicate Rhai script watcher notifications when the change channel is full (PR #9391)

When many filesystem events arrived in quick succession, the Rhai script watcher could spin an OS thread or panic — the previous retry loop kept trying to send on a full channel, and would panic if the receiver closed before the retry succeeded.

The watcher now drops duplicate notifications when the channel is already full, matching the behavior introduced for the configuration file watcher in PR #8336. Reloads always re-read the current file from disk, so a single pending notification in the channel is sufficient to guarantee the latest contents will be picked up.

By @carodewig in https://github.com/apollographql/router/pull/9391

🛠 Maintenance

Make batching.mode optional and reject unknown subgraph batching fields (PR #9315)

The batching configuration struct now uses a struct-level #[serde(default)] with an explicit impl Default, rather than per-field #[serde(default)] annotations. This aligns with the project's YAML design guidance, which requires that the serde deserialization path and the Default implementation use the same mechanism.

As a result of this change:

  • The mode field now has an explicit default of batch_http_link (the only supported mode), so it is no longer required in YAML configuration. Existing configurations that specify mode: batch_http_link are unaffected.
  • The subgraph batching configuration now rejects unknown fields, consistent with the rest of the router configuration.

By @BobaFetters in https://github.com/apollographql/router/pull/9315

Deprecate connectors.subgraphs configuration field (PR #9415)

The connectors.subgraphs configuration field is now deprecated in favor of connectors.sources. When connectors.subgraphs is set, the router will emit a deprecation warning at startup directing operators to rename the key. The field will be removed in a future 3.x release.

By @BobaFetters in https://github.com/apollographql/router/pull/9415

Improve Query::apply_root_selection_set performance by 5-15% (PR #9458)

Query::apply_root_selection_set now combines three separate map lookups into one, reducing work on every query plan application by 5-15%.

By @rohan-b99 in https://github.com/apollographql/router/pull/9458

📚 Documentation

Document how to log operations that exceed a candidate operation limit (PR #9294)

Adds a new section to the request limits docs showing how to use a custom telemetry event with a gt condition on the query selector to log operations that exceed a candidate max_aliases, max_depth, max_height, or max_root_fields value — without configuring limits or enabling warn_only mode. The example also captures the client name and version from the apollographql-client-name and apollographql-client-version headers so you can see which clients are sending the offending operations. The warn_only section now cross-references this approach.

By @smyrick in https://github.com/apollographql/router/pull/9294

Clarify authorization directive behavior on federated root operation types (PR #9213)

The authorization docs now explain what happens when you apply @authenticated, @requiresScopes, or @policy directly to a root operation type (Query, Mutation, or Subscription) in a subgraph. Because root operation types are shared merged types in a federated graph, the directive composes into the supergraph root type and applies to every field on that type, including fields contributed by other subgraphs. To scope authorization reliably, apply the directive to each field rather than to the root type.

By @andywgarcia in https://github.com/apollographql/router/pull/9213

🐛 Fixes

Recover the Redis-backed caches after cluster events and honor required_to_start: true on startup

The router's Redis-backed caches (query planner, entity cache, APQ, response cache) could silently stall after a network event involving Redis replicas or the full cluster — accumulating queued commands, command timeouts, latency, and memory pressure until the router was redeployed. The router now detects when the underlying Redis client has given up reconnecting, drains the connection pool, and rebuilds it on the next request. In deployments where the broadcast cluster topology contains nodes that aren't routeable from the router's network position (for example, internal IPs reserved for replica promotion), a new replica filter screens those nodes out before they enter the routing table.

The required_to_start: true flag — available on each cache under supergraph.query_planning.cache.redis, apq.router.cache.redis, preview_entity_cache.subgraph.all.redis, and experimental_response_cache.subgraph.all.redis — now actually fails the router's startup fast when Redis is unreachable, instead of hanging indefinitely or silently returning success under broadcast overflow.

The router also now supports required_to_start: false, allowing the router to start when Redis is unavailable at boot and to begin caching once Redis becomes reachable.

For more technical internal details, see PR #9023 and PR #9418. For more details on configuring the router's Redis-backed caches, see Response Cache Customization and the related caching docs.

By @aaronArinder in https://github.com/apollographql/router/pull/9023 and https://github.com/apollographql/router/pull/9418

🐛 Fixes

Avoid spurious "meter provider after shutdown" error during router shutdown (PR #9248)

The router no longer emits the spurious cannot use meter provider after shutdown error during shutdown. The metrics aggregation layer now returns a noop instrument in that path instead of panicking.

@rohan-b99 in https://github.com/apollographql/router/pull/9248

Use lazy idle eviction in connection pool to avoid inter-request TCP closes (PR #9308)

When pool_idle_timeout was introduced in v2.13.0, the router unconditionally enabled a background timer that proactively closed idle connections exceeding the timeout. In some network environments, the TCP close sent by this background task raced with a new connection attempt and caused significant latency spikes on the next request.

The router now uses lazy eviction: connections are only closed at checkout time, when a request finds a pooled connection that has exceeded pool_idle_timeout. No TCP closes are sent between requests. This matches router behavior before v2.13.0.

@carodewig in https://github.com/apollographql/router/pull/9308

Include URL and failure category in JWKS fetch error logs (PR #9258)

When the JWKS server is unreachable, the router now logs a specific, actionable message including the URL and the failure category (timed out, connection failed, or generic failure) — replacing the previous vague "could not get url" message.

@carodewig in https://github.com/apollographql/router/pull/9258

Pick up hickory 0.26.1 to close two upstream DNS DoS advisories (RUSTSEC-2026-0119, RUSTSEC-2026-0120)

The router's DNS resolver (via hickory-resolver) inherits two upstream advisories in hickory-proto / hickory-net 0.26.0. Both are fixed in 0.26.1, which is now pinned in Cargo.lock.

Source-built consumers were already insulated by caret-semver dependency declarations; this change picks up the fix in Apollo's pre-built binaries and Docker images.

@carodewig in https://github.com/apollographql/router/pull/9321

Return a single JSON response for unsupported defer-with-batch queries (PR #9311)

Batched queries that use @defer are not supported by the router. Previously these requests produced a malformed multipart response; they now return a single JSON response with errors that explicitly indicates the lack of support.

@rohan-b99 in https://github.com/apollographql/router/pull/9311

Apply http_max_request_bytes only to the operations field, not file streams (PR #9226, PR #9327)

Previously, limits.http_max_request_bytes (default 2 MB) was applied to the entire multipart body of file upload requests, causing large file uploads to be rejected even when preview_file_uploads.protocols.multipart.limits.max_file_size was configured to allow them.

The limit now applies only to the GraphQL operations field (the query and variables). File data is bounded separately by max_file_size, enforced by the multer parser.

@carodewig in https://github.com/apollographql/router/pull/9226 and https://github.com/apollographql/router/pull/9327

🛠 Maintenance

Instrument experimental config features with OTLP gauges (PR #9330)

Adds apollo.router.config.experimental_* OTLP gauge metrics for all customer-facing experimental config flags, using the existing populate_config_instrument! pattern in configuration/metrics.rs. This enables Apollo to track adoption of experimental features so we can inform decisions about which to promote or remove in future releases.

Features now instrumented:

  • experimental_chaos
  • experimental_type_conditioned_fetching
  • experimental_hoist_orphan_errors
  • experimental_log_on_broken_pipe
  • experimental_plans_limit
  • experimental_paths_limit
  • experimental_reuse_query_plans
  • experimental_cooperative_cancellation
  • experimental_prewarm_query_plan_cache
  • experimental_local_field_metrics
  • experimental_response_trace_id
  • experimental_otlp_endpoint
  • experimental_otlp_tracing_protocol
  • experimental_otlp_metrics_protocol
  • experimental_http2
  • experimental_http2_keep_alive_interval
  • experimental_http2_keep_alive_timeout
  • experimental_mock_subgraphs
  • experimental.expose_query_plan (recorded as apollo.router.config.experimental_expose_query_plan)

The mandatory experimental_diagnostics plugin is intentionally excluded because it is loaded on every router and would always report adoption as 100%.

@aaronArinder in https://github.com/apollographql/router/pull/9330

Avoid unnecessary clones on subgraph requests (PR #9266)

The router now avoids some unnecessary memory allocations when making subgraph requests, particularly on the APQ (Automatic Persisted Queries) path.

@carodewig in https://github.com/apollographql/router/pull/9266

Improve query plan cache throughput with an in-memory fast path (PR #9279)

Every query plan cache lookup — including cache hits — previously acquired the wait_map mutex before checking whether the value was in memory. On a warm cache this was pure overhead: the mutex was locked twice, a broadcast::Sender was allocated, and a cleanup task was spawned, all to be immediately discarded.

A fast path now checks the in-memory cache before acquiring the mutex. On a hit the value is returned immediately; the wait_map path is only entered on a miss, which is the only case where deduplication is needed.

By @theJC in https://github.com/apollographql/router/pull/9279

🚀 Features

Add expand_json_string_values option to JSON log formatter (PR #9156)

When expand_json_string_values: true is set on a stdout or file JSON log formatter, string attribute values that contain valid JSON objects or arrays are emitted as native JSON instead of quoted strings. This enables log aggregators like Splunk to index sub-fields such as errors{}.extensions.code.

This is useful when telemetry selectors like response_errors: "$[*]" produce structured data: OpenTelemetry's attribute model serializes objects to JSON strings, but log formatters can now expand those strings back to native JSON at emit time. OTLP exporters are unaffected.

By @carodewig in https://github.com/apollographql/router/pull/9156

Normalize supergraph.path to support queries with and without trailing slashes (/) (PR #8860)

Normalize trailing / for supergraph.path to support /graphql and /graphql/. This works by stripping trailing / from both the configured path and the incoming query path to ensure they match, regardless of whether the config or query includes a trailing slash.

By @Jephuff in https://github.com/apollographql/router/pull/8860

Accept JWTs without exp on a per-JWKS basis while still rejecting expired tokens (Issue #8910, PR #8911)

Adds a per-JWKS allow_missing_exp configuration option to Router JWT authentication. When enabled for a JWKS entry, tokens without an exp claim are accepted for that JWKS. Tokens that include an exp claim continue to be validated and rejected if expired.

This is useful for deployments that rely on long-lived machine-to-machine or service tokens that omit exp, without relaxing expiry validation globally.

By @fernando-apollo in https://github.com/apollographql/router/pull/8911

Add selective body field filtering for coprocessor responses (Issue #5020)

Adds the ability to selectively send only specific parts of GraphQL response bodies (data, errors, or extensions) to the coprocessor, instead of the entire response body. This reduces serialization/deserialization overhead and network payload size when the coprocessor only needs to inspect certain fields.

Previously, the body configuration was a boolean that sent either the entire response body or nothing. Now it supports selective field filtering:

coprocessor:
  url: http://127.0.0.1:8081

  # Supergraph responses
  supergraph:
    response:
      body:
        data: false
        errors: true        # Only send errors
        extensions: true    # and extensions

  # Execution responses
  execution:
    response:
      body:
        data: true
        errors: false
        extensions: false   # Only send data

  # Subgraph responses
  subgraph:
    all:
      response:
        body:
          data: false
          errors: true      # Only send errors
          extensions: false

The boolean syntax (body: true or body: false) continues to work for backward compatibility. When using selective filtering, the coprocessor can only modify the fields that were sent to it. Other fields are preserved from the original response.

This feature is available for the supergraph, execution, and subgraph response stages.

By @zachfettersmoore in https://github.com/apollographql/router/pull/9019

Emit apollo.router.operations.rhai.duration histogram metric for Rhai script callbacks (PR #9072)

A new apollo.router.operations.rhai.duration histogram metric (unit: s, value type: f64) is now emitted for every Rhai script callback execution across all pipeline stages. This mirrors the existing apollo.router.operations.coprocessor.duration metric.

Attributes on each datapoint:

  • rhai.stage — the pipeline stage (e.g. RouterRequest, SubgraphResponse)
  • rhai.succeededtrue if the callback returned without throwing an error

By @theJC in https://github.com/apollographql/router/pull/9072

Add intern_strings configuration option for the Rhai plugin (PR #9070)

The Rhai plugin now exposes an intern_strings option that controls Rhai's internal string interning. Under high concurrency, threads encountering new strings must acquire a write lock, which can serialize Rhai execution across concurrent requests.

Setting intern_strings: false disables interning, eliminating the lock:

rhai:
  scripts: ./rhai
  main: main.rhai
  intern_strings: false

String interning can alleviate memory allocation and make string equality checks a little faster. For deployments serving many concurrent requests, the cost likely outweighs the benefit, so we recommend experimenting with intern_strings: false and observing if it improves performance.

The default (true) preserves the existing behavior.

By @theJC in https://github.com/apollographql/router/pull/9070

Add request_duration router selector (PR #9187)

Adds a new request_duration selector for the router service that returns the total elapsed time from when the router received the request. The unit is configurable:

  • seconds (float)
  • milliseconds (integer)
  • nanoseconds (integer)

The selector can be used as a custom instrument attribute or combined with conditions to filter based on request duration. For example, to count requests that complete in under 10 seconds:

telemetry:
  instrumentation:
    instruments:
      router:
        my.short.requests:
          type: counter
          value: unit
          unit: reqs
          description: "Requests completing in under 10 seconds"
          condition:
            lt:
              - request_duration: seconds
              - 10

By @carodewig in https://github.com/apollographql/router/pull/9187

Add subscription and defer observability: end reason span attributes and termination metrics (PR #8858)

Adds new span attributes and metrics to improve observability of streaming responses.

Span attributes:

  • apollo.subscription.end_reason: Records the reason a subscription was terminated. Possible values are server_close, subgraph_error, heartbeat_delivery_failed, client_disconnect, schema_reload, and config_reload.
  • apollo.defer.end_reason: Records the reason a deferred query ended. Possible values are completed (all deferred chunks were delivered successfully) and client_disconnect (the client disconnected before all deferred data was delivered).

Both attributes are added dynamically to router spans only when relevant (i.e., only on requests that actually use subscriptions or @defer), instead of being present on every router span.

Metrics:

A single counter is emitted when a subscription terminates:

  • apollo.router.operations.subscriptions.terminated.client (default attributes: reason, subgraph.name): Incremented once per client connection when a subscription stream ends. The reason attribute indicates why (possible values: server_close, subgraph_error, client_disconnect, heartbeat_delivery_failed, schema_reload, config_reload). The subgraph.name attribute is populated if available. When deduplication is enabled, a single subgraph WebSocket closure produces one terminated event per deduplicated client sharing that connection (each with reason=server_close).

    Attributes for this metric are configurable. By default, reason and subgraph.name are enabled. You can also enable client.name via configuration:

    telemetry:
      instrumentation:
        instruments:
          router:
            apollo.router.operations.subscriptions.terminated.client:
              attributes:
                reason: true
                subgraph.name: true
                client.name: true

The following counter is emitted when a subscription request is rejected:

  • apollo.router.operations.subscriptions.rejected (attributes: reason, subgraph.name): A subscription request was rejected. The reason attribute indicates why: max_opened_subscriptions_limit_reached (the router has reached its max_opened_subscriptions limit) or subgraph (the subgraph WebSocket connection failed, e.g. connection refused, protocol error, or failed subscription handshake). The subgraph.name attribute is populated when available, and defaults to an empty string otherwise.

The following counter is emitted when a subgraph ends a subscription:

  • apollo.router.operations.subscriptions.terminated.subgraph (attributes: subgraph.name): Incremented once per subgraph WebSocket closure. Each deduplicated client sharing that connection will also emit a corresponding apollo.router.operations.subscriptions.terminated.client event with reason=server_close.

By @rohan-b99 in https://github.com/apollographql/router/pull/8858

🐛 Fixes

Recognize 204 (No Content) responses without Content-Length header in connectors (PR #9141)

Connectors now correctly handle HTTP 204 (No Content) responses from spec-compliant servers that don't include a Content-Length header.

Previously, empty body detection relied on the presence of a Content-Length: 0 header. Because the HTTP spec explicitly forbids including this header in 204 responses, connectors would fail to recognize empty bodies from compliant servers. The fix checks body.is_empty() directly, with Content-Length: 0 kept as a fallback for non-compliant servers.

By @apollo-mateuswgoettems in https://github.com/apollographql/router/pull/9141

Retry JWKS candidates on issuer/audience mismatch (PR #9214)

When multiple JWKS entries share identical key material (e.g., Azure AD B2C multi-policy tenants where different policies use the same RSA key), the router now correctly retries validation against all matching candidates. Previously, issuer and audience validation happened after the candidate loop, so a token that passed signature verification against the first JWKS entry would be rejected if that entry's configured issuer or audience didn't match — with no attempt to try the remaining entries.

By @carodewig in https://github.com/apollographql/router/pull/9214

Support non-ASCII (UTF-8) WebSocket header values (Issue #1485, PR #9051)

The router can now handle WebSocket connections with UTF-8 encoded header values, including non-ASCII characters like "Montréal". Previously, such connections failed because of serialization issues in the underlying tungstenite library.

The fix comes from updating tokio-tungstenite from v0.28.0 to v0.29.0.

By @BobaFetters in https://github.com/apollographql/router/pull/9051

Accept pool_idle_timeout: null in traffic_shaping configuration (PR #9165)

Setting pool_idle_timeout: null in traffic_shaping configuration caused a startup failure with a schema validation error, despite null being a documented valid value that disables idle connection eviction. The JSON schema incorrectly only allowed strings. It now correctly accepts both strings and null.

By @carodewig in https://github.com/apollographql/router/pull/9165

Honor zero values for minAvailable and maxUnavailable in the Helm PDB template (Issue #8350)

Setting minAvailable: 0 or maxUnavailable: 0 in the podDisruptionBudget Helm values was silently ignored, producing a PDB with no disruption rules. This happened because Go templates treat 0 as falsy in a simple if check.

The template now uses kindIs "invalid" to correctly detect whether a value was explicitly set, and an else if to prevent both fields from being rendered simultaneously — which Kubernetes rejects. If both maxUnavailable and minAvailable are set, maxUnavailable takes precedence.

By @apollo-mateuswgoettems in https://github.com/apollographql/router/pull/9028

Ensure client.name, client.version, http.route, and http.request.method can be aliased on router spans (PR #9048)

client.name and client.version have now been added to RouterAttributes, http.route has been added to HttpServerAttributes, and http.request.method has been added to HttpCommonAttributes. The default behavior should remain the same. Example configuration using aliases:

telemetry:
  instrumentation:
    spans:
      router:
        attributes:
          http.route:
            alias: http_route
          http.request.method:
            alias: http_request_method

By @rohan-b99 in https://github.com/apollographql/router/pull/9048

Preserve null propagation when multiple fragments select the same non-null field (PR #9032)

When a query uses multiple fragment spreads on the same parent type and a subgraph response is missing a required non-null field on a union member, the router now correctly returns null for the affected field instead of a partial object like {"__typename": "A"}.

The GraphQL specification requires that a non-null violation propagates null upward to the nearest nullable parent. Previously, if one fragment nullified a field, a subsequent fragment on the same parent could overwrite that null with a partial result — producing a spec-incorrect response.

By @abernix in https://github.com/apollographql/router/pull/9032

🛠 Maintenance

Reduce flaky CI in federation validation and Redis cache metrics tests (PR #9102)

Removes a wall-clock performance assertion from connector validation snapshot tests (timing is unreliable under CI load) and replaces a fixed sleep in Redis cache metrics tests with polling until command_queue_length reports zero before asserting gauges.

By @smyrick in https://github.com/apollographql/router/pull/9102

📚 Documentation

Document list-type argument support for slicingArguments in demand control (PR #9196)

The router supports using the length of list-type arguments as the cost multiplier in @listSize(slicingArguments: [...]), but this was not documented. Adds a new "List-type arguments in slicingArguments" subsection to the demand control docs with schema examples, query examples (both inline arrays and variables), and cost calculation breakdowns.

By @shanemyrick in https://github.com/apollographql/router/pull/9196

Document HTTP proxy support for GraphOS OTLP exporters (PR #9055)

Documents that the router's GraphOS OTLP exporters respect the standard HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables when using the HTTP transport. Includes a note that TLS-inspecting proxies also require the proxy's root certificate to be added to the router's trust store.

Also corrects the minimum version badge for experimental_otlp_tracing_protocol / experimental_otlp_metrics_protocol to Router v1.49.0, which is when HTTP transport support was first introduced.

By @abernix in https://github.com/apollographql/router/pull/9152

Deprecate apollo.router.session.count.active in favor of http.server.active_requests (PR #9069)

The Standard Instruments reference now marks apollo.router.session.count.active as deprecated and directs users to http.server.active_requests instead, which follows OpenTelemetry semantic conventions. The metric remains in the router for backward compatibility but might be removed in a future release.

By @mabuyo in https://github.com/apollographql/router/pull/9069

🧪 Experimental

Add experimental HTTP transport for Apollo OTLP metrics and traces (PR #9055)

The router can now send Apollo OTLP metrics and traces over HTTP (experimental). Enable it with these config values:

  • telemetry.apollo.experimental_otlp_tracing_protocol
  • telemetry.apollo.experimental_otlp_metrics_protocol

gRPC remains the preferred transport for Apollo OTLP, but HTTP is available for deployments that can't use gRPC.

By @bonnici in https://github.com/apollographql/router/pull/9055

🐛 Fixes

Fix spurious REQUEST_RATE_LIMITED errors when no rate limiting is configured (PR #9034)

Under sustained load, the router could return REQUEST_RATE_LIMITED (429) errors even when no rate limiting was configured. An internal queue had an implicit limit that could trigger load shedding, even if the queue was not actually overloaded.

This fix removes that implicit limit, so requests are shed only when the queue is genuinely full. The queue still has explicit limits to ensure quality of service.

By @jhrldev in https://github.com/apollographql/router/pull/9034

🚀 Features

Add context_id selector for telemetry to expose unique per-request identifier (PR #8899)

A new context_id selector is now available for router, supergraph, subgraph, and connector telemetry instrumentation. This selector exposes the unique per-request context ID, which you can use to reliably correlate and debug requests in traces, logs, and custom events.

The context ID was previously accessible in Rhai scripts as request.id but had no telemetry selector. You can now include context_id: true in your telemetry configuration to add the context ID to spans, logs, and custom events.

Example configuration:

telemetry:
  instrumentation:
    spans:
      router:
        attributes:
          "request.id":
            context_id: true
      supergraph:
        attributes:
          "request.id":
            context_id: true
      subgraph:
        attributes:
          "request.id":
            context_id: true
      connector:
        attributes:
          "request.id":
            context_id: true

By @BobaFetters in https://github.com/apollographql/router/pull/8899

Enable Unix Domain Socket paths (PR #8894)

Enables Unix Domain Socket (UDS) paths for both coprocessors and subgraphs. Paths must use ?path= as the query param: unix:///tmp/some.sock?path=some_path

By @aaronarinder in https://github.com/apollographql/router/pull/8894

Add configurable pool_idle_timeout for HTTP client connection pools (PR #9014)

Adds a new pool_idle_timeout configuration option to the HTTP client used by subgraphs, connectors, and coprocessors. This controls how long idle keep-alive connections remain in the connection pool before being evicted. The default is 15 seconds (up from the previous hardcoded 5 seconds). Setting it to null disables the idle eviction interval entirely, meaning pooled connections are never evicted due to idleness.

The option is available at every level where HTTP client configuration applies:

traffic_shaping:
  all:
    pool_idle_timeout: 30s      # applies to all subgraphs
  subgraphs:
    products:
      pool_idle_timeout: 60s    # per-subgraph override
  connector:
    all:
      pool_idle_timeout: 30s    # applies to all connectors
    sources:
      my_source:
        pool_idle_timeout: 60s  # per-source override

coprocessor:
  url: http://localhost:8081
  client:
    pool_idle_timeout: 30s      # coprocessor client

By @aaronarinder in https://github.com/apollographql/router/pull/9014

Add persisted query ID context key (PR #8959)

Adds a context key for the persisted query ID in the router. The PersistedQueryLayer now stores the persisted query ID in the request context, and the Rhai engine can access it via that key.

By @faisalwaseem in https://github.com/apollographql/router/pull/8959

Add retry layer for push metrics exporters (PR #9036)

Adds a RetryMetricExporter layer that retries up to three times with jittered exponential backoff for the apollo metrics and otlp named exporters.

By @rohan-b99 in https://github.com/apollographql/router/pull/9036

🐛 Fixes

Support more types of nullable elements in response/entity cache keys (PR #8923)

PR #8767 (released in Router v2.11.0) changed the entity and response caching keys to support nullable elements. The implementation covered the case of a field explicitly being set to null, but didn't cover the following cases:

  • Nullable field being missing
  • Nullable list items

This change adds support for those cases.

By @carodewig in https://github.com/apollographql/router/pull/8923

Pin transitive h2 dependency at minimum v0.4.13 to pick up critical flow-control, deadlock, and tracing fixes (PR #9033)

h2 0.4.13 (released January 5, 2026) contains three fixes directly relevant to the router, which uses h2 exclusively as a client when connecting to subgraphs:

  • Capacity deadlock under concurrent streams (#860) — high relevance: Under concurrent load with max_concurrent_streams limits in effect, flow-control capacity could be assigned to streams still in pending_open state. Those streams could never consume the capacity, starving already-open streams and permanently freezing all outgoing traffic on the connection with no error surfaced. This is directly triggerable in the router: any subgraph behind Envoy or a gRPC backend advertises a max_concurrent_streams limit (Envoy defaults to 100), and under production load the router will routinely queue more concurrent requests than that limit allows.

  • OTel tracing span lifetime leak (#868) — high relevance: The h2 Connection object captured the active tracing span at connection creation time as its parent, keeping that span alive for the entire lifetime of the connection. Since the router wraps every subgraph request in an OpenTelemetry span and connections are pooled, affected spans could linger indefinitely under sustained traffic — never being exported to the tracing backend and accumulating in memory.

  • Flow-control stall on padded DATA frames (#869) — lower relevance for typical subgraphs, higher for connectors: Padding bytes in DATA frames were not being returned to the flow-control window, causing the connection window to drain to zero and permanently stalling downloads with no error. Typical GraphQL/gRPC subgraphs do not send padded frames, but router connectors calling arbitrary HTTP APIs (e.g., Google Cloud Storage or CDN-backed endpoints) can encounter this.

By @theJC in https://github.com/apollographql/router/pull/9033

Return 503 for rate limit traffic shaping (PR #9013)

Reverts PR #8765.

When the router's rate limit or buffer capacity is exceeded, it now returns HTTP 503 (Service Unavailable) instead of HTTP 429 (Too Many Requests).

HTTP 429 implies that a specific client has sent too many requests and should back off. HTTP 503 more accurately reflects the situation: the router is temporarily unable to handle the request due to overall service load, not because of the behavior of any individual client.

This change affects both router-level and subgraph-level rate limiting. Documentation has been updated to reflect the new status code.

By @carodewig in https://github.com/apollographql/router/pull/9013

Set Cache-Control: no-store when the response cache returns GraphQL errors (PR #8933)

When using the response cache plugin, if a query spans multiple subgraphs and one returns an error or times out, the final HTTP response was still carrying the successful subgraph's Cache-Control header (e.g. max-age=1800, public). This allowed intermediate caches (CDNs, reverse proxies) to cache and serve incomplete or stale partial responses to other clients.

If the response cache plugin is enabled and was going to set a Cache-Control header, but the response contains any GraphQL errors, it now sets Cache-Control: no-store instead of the merged subgraph cache control value.

By @carodewig in https://github.com/apollographql/router/pull/8933

Apply entity-less subgraph errors to the nearest parent instead of every entity

When making an entity resolution, if entity resolution fails (for example, because the path from the subgraph was malformed), the router applied errors to every item in the list of entities expected. For example, if 2000 entities were expected but 2000 errors were returned instead, each error was applied to every entity. This causes an explosion of errors and leads to significant memory allocations that can cause OOMKills.

When the router can't determine where an error should be applied, it now applies it to the most immediate parent of the targeted entity — for a list of users, it applies to the list itself rather than to each index of that list.

By @aaronArinder in https://github.com/apollographql/router/pull/8962

Report http.client.response.body.size and http.server.response.body.size consistently when content-length is absent or compression is used (PR #8972)

Reporting these metrics previously relied on either the Content-Length header or the size_hint of the body, which reports the uncompressed size. OpenTelemetry semantic conventions recommend reporting the compressed size.

The router now consistently reports the compressed size when compression is used, even when Content-Length is absent, for:

  • Router → client responses
  • Subgraph → router responses
  • Connector → router responses

By @rohan-b99 in https://github.com/apollographql/router/pull/8972

Ensure query planning allocation stats are still recorded if cooperative cancellation is not enabled (PR #8902)

The metric apollo.router.query_planner.memory was unintentionally only showing allocations during the query_parsing compute job if cooperative cancellation for query planning was not enabled. Both query_parsing and query_planning should now be available.

By @rohan-b99 in https://github.com/apollographql/router/pull/8902

Align ServiceMonitor naming with other chart resources using the router.fullname helper (Issue #TSH-22160)

The ServiceMonitor Helm resource was using .Release.Name directly as its metadata.name, while all other chart resources (e.g. Service, Deployment) already used the router.fullname helper. This caused a naming inconsistency: for a release named my-release, the Service would be named my-release-router but the ServiceMonitor would be named my-release.

This change aligns the ServiceMonitor name with the rest of the chart by using {{ include "router.fullname" . }}, ensuring consistent naming and proper support for nameOverride and fullnameOverride values.

By @mateusgoettems in https://github.com/apollographql/router/pull/8929

Return null data and respect error location config for fully-unauthorized requests (PR #9022)

When the query planner rejected a request because all fields were unauthorized, the response always placed errors in the errors array and returned data: {}, ignoring the configured errors.response location (errors, extensions, or disabled). The router now returns data: null and respects errors.response and errors.log, consistent with partially-unauthorized requests.

By @carodewig in https://github.com/apollographql/router/pull/9022

Handle both deprecated enum values when merging coprocessor context (PR #8913)

A change to coprocessor context merges in Router v2.10 caused keys to be deleted when context: true is used as the coprocessor context selector in the router configuration file.

The workaround was to pass context: deprecated instead. This change brings parity when context: true is provided.

By @carodewig in https://github.com/apollographql/router/pull/8913

Block router startup when certain OTEL environment variables are set (PR #8915)

The router now fails to start if any of the following OpenTelemetry (OTEL) environment variables are set:

  • OTEL_EXPORTER_OTLP_ENDPOINT
  • OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
  • OTEL_EXPORTER_OTLP_METRICS_ENDPOINT

Using these variables isn't supported by the router because they can override or interfere with its built-in telemetry configuration, leading to unexpected behavior.

Previously, the router emitted a warning when OTEL_EXPORTER_OTLP_ENDPOINT was present. Startup is now blocked to prevent unintended telemetry configuration conflicts.

If your deployment defines any of these environment variables (for example, through base container images, platform defaults, or infrastructure tooling), remove them before starting the router.

By @OriginLeon in https://github.com/apollographql/router/pull/8915

Prevent the readiness ticker from resetting the sampling window clock on recovery (PR #8966)

The readiness ticker recreated its interval_at inside the recovery loop on every cycle, which reset the sampling window clock each time the router returned to ready. This caused inconsistent sampling behavior across recovery cycles. Additionally, the rejection counter was read and zeroed with separate atomic operations (load + store(0)), leaving a race window where rejections arriving between the two operations could be silently dropped.

The fix creates the interval once outside the loop, sets MissedTickBehavior::Delay to prevent catch-up ticks after the recovery sleep, and uses swap(0, Relaxed) to atomically read and reset the rejection counter in a single operation.

By @OriginLeon in https://github.com/apollographql/router/pull/8966

Ensure http2only uses h2c for cleartext connections (PR #9018)

hyper does not support upgrading cleartext connections from HTTP/1.1 to HTTP/2. To use HTTP/2 without TLS, clients must use 'prior knowledge' — connecting with the HTTP/2 preface directly. This is what experimental_http2: http2only is for, but previously HTTP/1 was always enabled in the connector, causing the client to fall back to HTTP/1.1 regardless. This fix applies to all outbound HTTP connections: subgraphs, connectors, and coprocessors.

experimental_http2TLSprotocol used
disableyesHTTP/1.1
disablenoHTTP/1.1
enableyesHTTP/2 (if server supports it), else HTTP/1.1
enablenoHTTP/1.1
http2onlyyesHTTP/2
http2onlynoHTTP/2 (h2c — cleartext prior knowledge)

By @carodewig in https://github.com/apollographql/router/pull/9018

Reject invalid values for client library name and version (PR #8934)

Rejects invalid values (validated against a regex) for library name and version provided in headers or operation extensions, which are used for the Client Awareness feature and telemetry.

By @BoD in https://github.com/apollographql/router/pull/8934

Correct no-store and no-cache behavior for response cache and entity cache plugins (PR #8948 and PR #8952)

no-store and no-cache have different meanings. Per RFC 9111:

  • no-store: allows serving a response from cache, but prohibits storing the response in cache
  • no-cache: prohibits serving a response from cache, but allows storing the response in cache

(Note: no-cache actually prohibits serving a response from cache without revalidation — but the router doesn't distinguish between lookup and revalidation.)

The response caching and entity caching plugins were incorrectly treating no-store as both 'no serving response from the cache' and 'no storing response in the cache.' This change corrects that behavior.

By @carodewig in https://github.com/apollographql/router/pull/8948 and https://github.com/apollographql/router/pull/8952

Allow exists conditions with request_header selectors on response-stage coprocessor and event configurations (PR #8964)

Using exists: { request_header: <name> } as a condition on response-stage coprocessor or telemetry event configurations (e.g. on: response) previously caused the router to reject the configuration at startup with a validation error, even though the condition is valid and works correctly at runtime.

The validator was incorrectly rejecting request-stage selectors inside Exists conditions for response-stage configurations. This is safe because evaluate_request() pre-resolves these conditions before they are stored for response-time evaluation: if the header is present the condition becomes True; if absent, the event or coprocessor call is discarded and never reaches the response stage.

By @OriginLeon in https://github.com/apollographql/router/pull/8964

📃 Configuration

Enable the router to pull graph artifacts from insecure (non-SSL) registries

You can configure a list of safe registry hostnames to enable the router to pull graph artifacts over HTTP instead of HTTPS. Insecure registries are commonly run within a private network such as a Kubernetes cluster or as a pull-through cache, where you want to avoid the overhead of setting up and distributing SSL certificates.

By @sirddoger in https://github.com/apollographql/router/pull/8919

🛠 Maintenance

Update to OpenTelemetry 0.31.0 (PR #8922)

The router now uses v0.31.0 of the OpenTelemetry Rust libraries. This update includes many bug fixes and performance improvements from upstream.

The router doesn't guarantee the stability of downstream pre-1.0 APIs, so users that directly interact with OpenTelemetry must update their code accordingly.

As part of this upgrade, Zipkin Native exporter is deprecated upstream. Switch to the OTLP exporter, which Zipkin now supports natively. Note that Zipkin Native exporter no longer supports setting a service name — if you need this, switch to the OTLP exporter.

By @BrynCooke @goto-bus-stop @rohan-b99 in https://github.com/apollographql/router/pull/8922

Use cargo build --locked --release in DIY Dockerfile.repo for deterministic, lockfile-respecting builds (PR #8983)

The DIY Dockerfile.repo previously used cargo install --path apollo-router, which doesn't enforce the versions in Cargo.lock — resulting in possible non-deterministic dependency resolution and builds that could diverge from what CI produces.

Using cargo build --locked --release -p apollo-router ensures the versions in the lockfile are respected and the DIY build path more closely aligns with CI.

By @theJC in https://github.com/apollographql/router/pull/8983

📚 Documentation

Document AWS API Gateway support for HTTP multipart subscriptions

Updates the API gateway subscriptions documentation to reflect that Amazon API Gateway now supports response streaming for REST APIs. HTTP multipart subscriptions are supported when the router is behind AWS API Gateway. Includes a link to the AWS announcement (November 2025) and a short configuration note linking to Response transfer mode.

By @smyrick in https://github.com/apollographql/router/pull/8907

Add documentation for experimental_hoist_orphan_errors configuration

Adds the experimental_hoist_orphan_errors configuration option to the YAML configuration reference. The documentation covers the feature's purpose (reducing multiplicative error propagation from entity resolution), per-subgraph and global enablement examples, the spec compliance caveat, and the known limitation that error counts are reduced but not hard-capped.

By @andywgarcia in https://github.com/apollographql/router/pull/8999

Document proxy root certificate configuration for router containers (PR #8823)

Documents how to add corporate proxy root certificates to Apollo Router containers — required for enterprise environments where TLS inspection proxies intercept HTTPS traffic.

The documentation includes:

  • Instructions for Docker deployments (runtime mount and custom image approaches)
  • Instructions for Kubernetes deployments (init container and custom image approaches)
  • Guidance for cloud deployments (AWS, Azure, GCP)
  • Links added to all containerization deployment guides

By @the-gigi-apollo in https://github.com/apollographql/router/pull/8823

Correct JWT authentication example hierarchy in the router authentication guide

Updates the GraphOS Router authentication guide to use the correct JWT configuration path in the YAML example. The example now shows authentication.router.jwt.jwks and authentication.router.jwt.on_error, matching the actual router configuration hierarchy.

By @the-gigi-apollo in https://github.com/apollographql/router/pull/8901

Document http_client span attribute limitations (PR #8967)

Documents that http_client span attributes don't support conditions or the static selector, causing a router startup failure when attempted.

By @mabuyo in https://github.com/apollographql/router/pull/8967

🧪 Experimental

Add experimental_hoist_orphan_errors configuration for controlling orphan error path assignment

Adds a new experimental_hoist_orphan_errors configuration that controls how entity-less ("orphan") errors from subgraphs are assigned paths in the response. When enabled for a subgraph, orphan errors are assigned to the nearest non-array ancestor in the response path, preventing them from being duplicated across every element in an array. This can be enabled globally via all or per-subgraph via the subgraphs map. Per-subgraph settings override all.

Here's an example when targeting a specific subgraph, my_subgraph:

experimental_hoist_orphan_errors:
  subgraphs:
    my_subgraph:
      enabled: true

An example when targeting all subgraphs:

experimental_hoist_orphan_errors:
  all:
    enabled: true

And an example enabling for all subgraphs except one:

experimental_hoist_orphan_errors:
  all:
    enabled: true
  subgraphs:
    noisy_one:
      enabled: false

Use this feature only if you know you have subgraphs that don't respond with the correct paths when making entity calls. If you're unsure, you probably don't need this.

By @aaronArinder in https://github.com/apollographql/router/pull/8998

🔒 Security

Note

For more information on the impact of the fix in this release and how your deployment might be affected or remediated, see the corresponding GitHub Security Advisory (GHSA) linked below. Updating to a patched Router version will resolve any vulnerabilities.

Reject GET requests with a non-application/json Content-Type header (GHSA-hff2-gcpx-8f4p)

The router now rejects GraphQL GET requests that include a Content-Type header with a value other than application/json (with optional parameters such as ; charset=utf-8). Any other value is rejected with a 415 status code.

GET requests without a Content-Type header continue to be allowed (subject to the router's existing CSRF prevention check), since GET requests have no body and therefore technically do not require this header.

This improvement makes the router's CSRF prevention more resistant to browsers that implement CORS in non-spec-compliant ways. Apollo is aware of one browser which as of March 2026 has a bug allowing an attacker to circumvent the router's CSRF prevention to carry out read-only XS-Search-style attacks. The browser vendor is in the process of patching this vulnerability; upgrading to this version of the router mitigates the vulnerability.

If your graph uses cookies (or HTTP Basic Auth) for authentication, Apollo encourages you to upgrade to this version.

This is technically a backwards-incompatible change. Apollo is not aware of any GraphQL clients that provide non-empty Content-Type headers on GET requests with types other than application/json. If your use case requires such requests, please contact support, and we may add more configurability in a follow-up release.

By @glasser and @carodewig in GHSA-hff2-gcpx-8f4p

🔒 Security

Note

For more information on the impact of the fix in this release and how your deployment might be affected or remediated, see the corresponding GitHub Security Advisory (GHSA) linked below. Updating to a patched Router version will resolve any vulnerabilities.

Reject GET requests with a non-application/json Content-Type header (GHSA-hff2-gcpx-8f4p)

The router now rejects GraphQL GET requests that include a Content-Type header with a value other than application/json (with optional parameters such as ; charset=utf-8). Any other value is rejected with a 415 status code.

GET requests without a Content-Type header continue to be allowed (subject to the router's existing CSRF prevention check), since GET requests have no body and therefore technically do not require this header.

This improvement makes the router's CSRF prevention more resistant to browsers that implement CORS in non-spec-compliant ways. Apollo is aware of one browser which as of March 2026 has a bug allowing an attacker to circumvent the router's CSRF prevention to carry out read-only XS-Search-style attacks. The browser vendor is in the process of patching this vulnerability; upgrading to this version of the router mitigates the vulnerability.

If your graph uses cookies (or HTTP Basic Auth) for authentication, Apollo encourages you to upgrade to this version.

This is technically a backwards-incompatible change. Apollo is not aware of any GraphQL clients that provide non-empty Content-Type headers on GET requests with types other than application/json. If your use case requires such requests, please contact support, and we may add more configurability in a follow-up release.

By @glasser and @carodewig in GHSA-hff2-gcpx-8f4p

🔒 Security

Note

For more information on the impact of the fix in this release and how your deployment might be affected or remediated, see the corresponding GitHub Security Advisory (GHSA) linked below. Updating to a patched Router version will resolve any vulnerabilities.

Reject GET requests with a non-application/json Content-Type header (GHSA-hff2-gcpx-8f4p)

The router now rejects GraphQL GET requests that include a Content-Type header with a value other than application/json (with optional parameters such as ; charset=utf-8). Any other value is rejected with a 415 status code.

GET requests without a Content-Type header continue to be allowed (subject to the router's existing CSRF prevention check), since GET requests have no body and therefore technically do not require this header.

This improvement makes the router's CSRF prevention more resistant to browsers that implement CORS in non-spec-compliant ways. Apollo is aware of one browser which as of March 2026 has a bug allowing an attacker to circumvent the router's CSRF prevention to carry out read-only XS-Search-style attacks. The browser vendor is in the process of patching this vulnerability; upgrading to this version of the router mitigates the vulnerability.

If your graph uses cookies (or HTTP Basic Auth) for authentication, Apollo encourages you to upgrade to this version.

This is technically a backwards-incompatible change. Apollo is not aware of any GraphQL clients that provide non-empty Content-Type headers on GET requests with types other than application/json. If your use case requires such requests, please contact support, and we may add more configurability in a follow-up release.

(This is a backport of a change from v2.12.1. This fix is not part of Router v2.11.0 through v2.12.0.)

By @glasser and @carodewig in GHSA-hff2-gcpx-8f4p

🐛 Fixes

Enforce feature restrictions for warning-state licenses

The router now enforces license restrictions even when a license is in a warning state. Previously, warning-state licenses could bypass enforcement for restricted features.

If your deployment uses restricted features, the router returns an error instead of continuing to run.

By @aaronArinder in https://github.com/apollographql/router/pull/8768

🧪 Experimental

Add experimental_hoist_orphan_errors to control orphan error path assignment

The GraphQL specification requires that errors include a path pointing to the most specific field where the error occurred. When a subgraph returns entity errors without valid paths, the router's default behavior is its closest attempt at spec compliance: it assigns each error to every matching entity path in the response. This is the correct behavior when subgraphs respond correctly.

However, when a subgraph returns a large number of entity errors without valid paths — for example, 2000 errors for 2000 expected entities — this causes a multiplicative explosion in the error array that can lead to significant memory pressure and out-of-memory kills. The root cause is the subgraph: a spec-compliant subgraph includes correct paths on its entity errors, and fixing the subgraph is the right long-term solution.

The new experimental_hoist_orphan_errors configuration provides an important mitigation while you work toward that fix. When enabled, the router assigns each orphaned error to the nearest non-array ancestor path instead of duplicating it across every entity. This trades spec-precise path assignment for substantially reduced error volume in the response — a conscious trade-off, not a strict improvement.

To target a specific subgraph:

experimental_hoist_orphan_errors:
  subgraphs:
    my_subgraph:
      enabled: true

To target all subgraphs:

experimental_hoist_orphan_errors:
  all:
    enabled: true

To target all subgraphs except one:

experimental_hoist_orphan_errors:
  all:
    enabled: true
  subgraphs:
    noisy_one:
      enabled: false

Per-subgraph settings override all. Note that this feature reduces the number of propagated errors but doesn't impose a hard cap — if your subgraph returns an extremely large number of errors, the router still processes all of them.

You'll likely know if you need this. Use it sparingly, and enable it only if you're affected and have been advised to do so. The behavior of this option is expected to change in a future release.

For full configuration reference and additional examples, see the experimental_hoist_orphan_errors documentation.

By @aaronArinder in https://github.com/apollographql/router/pull/8998

🚀 Features

Support Unix domain socket (UDS) communication for coprocessors (Issue #5739)

Many coprocessor deployments run side-by-side with the router, typically on the same host (for example, within the same Kubernetes pod).

This change brings coprocessor communication to parity with subgraphs by adding Unix domain socket (UDS) support. When the router and coprocessor are co-located, communicating over a Unix domain socket bypasses the full TCP/IP network stack and uses shared host memory instead, which can meaningfully reduce latency compared to HTTP.

By @theJC in https://github.com/apollographql/router/pull/8348

Add redact_query_validation_errors supergraph config option (PR #8888)

The new redact_query_validation_errors option in the supergraph configuration section replaces all query validation errors with a single generic error:

{
  "message": "invalid query",
  "extensions": {
    "code": "UNKNOWN_ERROR"
  }
}

By @phryneas in https://github.com/apollographql/router/pull/8888

Support multiple @listSize directives on the same field (PR #8872)

Warning

Multiple @listSize directives on a field only take effect after Federation supports repeatable @listSize in the supergraph schema. Until then, composition continues to expose at most one directive per field. This change makes the router ready for that Federation release.

The router now supports multiple @listSize directives on a single field, enabling more flexible cost estimation when directives from different subgraphs are combined during federation composition.

  • The router processes all @listSize directives on a field (stored as Vec<ListSizeDirective> instead of Option<ListSizeDirective>).
  • When multiple directives specify assumedSize values, the router uses the maximum value for cost calculation.
  • Existing schemas with single directives continue to work exactly as before.

This change prepares the router for federation's upcoming support for repeatable @listSize directives, and maintains full compatibility with current non-repeatable directive schemas.

By @cmorris in https://github.com/apollographql/router/pull/8872

Add parser recursion and lexical token metrics (PR #8845)

The router now emits two new metrics: apollo.router.operations.recursion for the recursion level reached, and apollo.router.operations.lexical_tokens for the number of lexical tokens in a query.

By @jhrldev in https://github.com/apollographql/router/pull/8845

Support subgraph-level demand control (PR #8829)

Subgraph-level demand control lets you enforce per-subgraph query cost limits in the router, in addition to the existing global cost limit for the whole supergraph. This helps you protect specific backend services that have different capacity or cost profiles from being overwhelmed by expensive operations.

When a subgraph-specific cost limit is exceeded, the router:

  • Still runs the rest of the operation, including other subgraphs whose cost is within limits.
  • Skips calls to only the over-budget subgraph, and composes the response as if that subgraph had returned null, instead of rejecting the entire query.

Per-subgraph limits apply to the total work for that subgraph in a single operation. For each request, the router tracks the aggregate estimated cost per subgraph across the entire query plan. If the same subgraph is fetched multiple times (for example, through entity lookups, nested fetches, or conditional branches), those costs are summed together and the subgraph's limit is enforced against that total.

Configuration
demand_control:
  enabled: true
  mode: enforce
  strategy:
    static_estimated:
      max: 10
      list_size: 10
      actual_cost_mode: by_subgraph
      subgraphs: # everything from here down is new (all fields optional)
        all:
          max: 8
          list_size: 10
        subgraphs:
          products:
            max: 6
            # list_size omitted, 10 implied because of all.list_size
          reviews:
            list_size: 50
            # max omitted, 8 implied because of all.max
Example

Consider a topProducts query that fetches a list of products from a products subgraph and then performs an entity lookup for each product in a reviews subgraph. Assume the products cost is 10 and the reviews cost is 5, leading to a total estimated cost of 15 (10 + 5).

Previously, you could only restrict that query via demand_control.static_estimated.max:

  • If you set it to 15 or higher, the query executes.
  • If you set it below 15, the query is rejected.

Subgraph-level demand control enables much more granular control. In addition to demand_control.static_estimated.max, which operates as before, you can also set per-subgraph limits.

For example, if you set max = 20 and reviews.max = 2, the query passes the aggregate check (15 < 20) and executes on the products subgraph (no limit specified), but doesn't execute against the reviews subgraph (5 > 2). The result is composed as if the reviews subgraph had returned null.

By @carodewig in https://github.com/apollographql/router/pull/8829

Improve @listSize directive parsing and nested path support (PR #8893)

Demand control cost calculation now supports:

  • Array-style parsing for @listSize sizing (for example, list arguments)
  • Nested input paths when resolving list size from query arguments
  • Nested field paths in the sizedFields argument on @listSize for more accurate cost estimation

These changes are backward compatible with existing schemas and directives.

By @cmorris in https://github.com/apollographql/router/pull/8893

Add coprocessor hooks for connector request and response stages (PR #8869)

You can now configure a coprocessor hook for the ConnectorRequest and ConnectorResponse stages of the router lifecycle.

coprocessor:
  url: http://localhost:3007
  connector:
    all:
      request:
        uri: true
        headers: true
        body: true
        context: all
        service_name: true
      response:
        headers: true
        body: true
        context: all
        service_name: true

By @andrewmcgivery in https://github.com/apollographql/router/pull/8869

🐛 Fixes

Pass variables to introspection queries (PR #8816)

Introspection queries now receive variables, enabling @include and @skip directives during introspection.

By @jephuff in https://github.com/apollographql/router/pull/8816

Log warning instead of returning error for non-UTF-8 headers in externalize_header_map (PR #8828)
  • The router now emits a warning log with the name of the header instead of returning an error.
  • The remaining valid headers are returned, which is more consistent with the router's default behavior when a coprocessor isn't used.

By @rohan-b99 in https://github.com/apollographql/router/pull/8828

Place http_client span attributes on the http_request span (PR #8798)

Attributes configured under telemetry.instrumentation.spans.http_client are now added to the http_request span instead of subgraph_request.

Given this config:

telemetry:
  instrumentation:
    spans:
      http_client:
        attributes:
          http.request.header.content-type:
            request_header: "content-type"
          http.response.header.content-type:
            response_header: "content-type"

Both attributes are now placed on the http_request span.

By @rohan-b99 in https://github.com/apollographql/router/pull/8798

Validate ObjectValue variable fields against input type definitions (PR #8821 and PR #8884)

The router now validates individual fields of input object variables against their type definitions. Previously, variable validation checked that the variable itself was present but didn't validate the fields within the object.

Example:

## schema ##
input MessageInput {
    content: String
    author: String
}
type Receipt {
    id: ID!
}
type Query{
    send(message: MessageInput): Receipt
}

## query ##
query(: MessageInput) {
    send(message: ) {
        id
    }
}

## input variables ##
{"msg":
    {
    "content": "Hello",
    "author": "Me",
    "unknownField": "unknown",
    }
}

This request previously passed validation because the variable msg was present in the input, but the fields of msg weren't validated against the MessageInput type.

Warning

To opt out of this behavior, set the supergraph.strict_variable_validation config option to measure.

Enabled:

supergraph:
  strict_variable_validation: enforce

Disabled:

supergraph:
  strict_variable_validation: measure

By @conwuegb in https://github.com/apollographql/router/pull/8821 and https://github.com/apollographql/router/pull/8884

Increase internal Redis timeout from 5s to 10s (PR #8863)

Because mTLS handshakes can be slow in some environments, the internal Redis timeout is now 10s (previously 5s). The connection "unresponsive" threshold is also increased from 5s to 10s.

By @aaronarinder in https://github.com/apollographql/router/pull/8863

Enforce and log operation limits for cached query plans (PR #8810)

The router now logs the operation-limits warning for cached query plans as well, ensuring the query text is included whenever limits are exceeded. This also fixes a case where a cached plan could bypass enforcement after changing warn_only from true to false during a hot reload.

By @rohan-b99 in https://github.com/apollographql/router/pull/8810

Prevent duplicate content-type headers in connectors (PR #8867)

When you override the content-type header in a connector @source directive, the router no longer appends the default value. The custom header value now properly replaces the default.

For example:

@source(
    name: "datasetInsightsAPI"
    http: {
        headers: [
            { name: "Content-Type", value: "application/vnd.iaas.v1+json" },
        ]
    }
)

Previously resulted in:

content-type: application/json, application/vnd.iaas.v1+json

Now correctly results in:

content-type: application/vnd.iaas.v1+json

By @andrewmcgivery in https://github.com/apollographql/router/pull/8867

Prevent duplicate tags in router spans added by dynamic attributes (PR #8865)

When dynamic attributes are added via SpanDynAttribute::insert, SpanDynAttribute::extend, LogAttributes::insert, LogAttributes::extend, EventAttributes::insert, or EventAttributes::extend and the key already exists, the router now replaces the existing value instead of creating duplicate attributes.

By @rohan-b99 in https://github.com/apollographql/router/pull/8865

Compute actual demand control costs across all subgraph fetches (PR #8827)

The demand control feature estimates query costs by summing together the cost of each subgraph operation, capturing any intermediate work that must be completed to return a complete response.

Previously, the actual query cost computation only considered the final response shape and didn't include any of the intermediate work in its total.

The router now computes the actual query cost as the sum of all subgraph response costs. This more accurately reflects the work done per operation and enables a more meaningful comparison between actual and estimated costs.

To disable the new actual cost computation behavior, set the router configuration option demand_control.strategy.static_estimated.actual_cost_mode to response_shape:

demand_control:
  enabled: true
  mode: enforce
  strategy:
    static_estimated:
      max: 10
      list_size: 10
      actual_cost_mode: by_subgraph # the default value
      # actual_cost_mode: response_shape # revert to prior actual cost computation mode

By @carodewig in https://github.com/apollographql/router/pull/8827

📚 Documentation

Correct response caching FAQ for schema updates and multi-root-field caching (PR #8794)

Updated the response caching FAQ to accurately describe caching behavior:

  • Clarify that schema updates generate new cache keys, so old entries don't receive cache hits (effectively expired from your perspective) instead of implying stale data might be served.
  • Correct the multi-root-field caching explanation to state that the router caches the entire subgraph response as a single unit, not separately per root field.
  • Add clarification that the configured TTL is a fallback when subgraph responses don't include Cache-Control: max-age headers.
  • Change example TTL from 300s to 5m for better readability.

By @the-gigi-apollo in https://github.com/apollographql/router/pull/8794

🚀 Features

Support client awareness metadata via HTTP headers (PR #8503)

Clients can now send library name and version metadata for client awareness and enhanced client awareness using HTTP headers. This provides a consistent transport mechanism instead of splitting values between headers and request.extensions.

By @calvincestari in https://github.com/apollographql/router/pull/8503

Reload OCI artifacts when a tag reference changes (PR #8805)

You can now configure tag-based OCI references in the router. When you use a tag reference such as artifacts.apollographql.com/my-org/my-graph:prod, the router polls and reloads when that tag points to a new artifact.

This also applies to automatically generated variant tags and custom tags.

By @graytonio in https://github.com/apollographql/router/pull/8805

Add memory limit option for cooperative cancellation (PR #8808)

The router now supports a memory_limit option on experimental_cooperative_cancellation to cap memory allocations during query planning. When the memory limit is exceeded, the router:

  • In enforce mode, cancels query planning and returns an error to the client.
  • In measure mode, records the cancellation outcome in metrics and allows query planning to complete.

The memory limit works alongside the existing timeout option. Whichever limit is reached first triggers cancellation.

This feature is only available on Unix platforms when the global-allocator feature is enabled and dhat-heap is not enabled.

Example configuration:

supergraph:
  query_planning:
    experimental_cooperative_cancellation:
      enabled: true
      mode: enforce  # or "measure" to only record metrics
      memory_limit: 50mb  # Supports formats like "50mb", "1gb", "1024kb", etc.
      timeout: 5s  # Optional: can be combined with memory_limit

By @rohan-b99 in https://github.com/apollographql/router/pull/8808

Add memory tracking metrics for requests (PR #8717)

The router now emits two histogram metrics to track memory allocation activity during request processing:

  • apollo.router.request.memory: Memory activity across the full request lifecycle (including parsing, validation, query planning, and plugins)
  • apollo.router.query_planner.memory: Memory activity for query planning work in the compute job thread pool

Each metric includes:

  • allocation.type: allocated, deallocated, zeroed, or reallocated
  • context: The tracking context name (for example, router.request or query_planning)

This feature is only available on Unix platforms when the global-allocator feature is enabled and dhat-heap is not enabled.

By @rohan-b99 in https://github.com/apollographql/router/pull/8717

🐛 Fixes

Support nullable @key fields in response caching (PR #8767)

Response caching can now use nullable @key fields. Previously, the response caching feature rejected nullable @key fields, which prevented caching in schemas that use them.

When you cache data keyed by nullable fields, keep your cache keys simple and avoid ambiguous null values.

By @aaronArinder in https://github.com/apollographql/router/pull/8767

Return 429 instead of 503 when enforcing a rate limit (PR #8765)

In v2.0.0, the router changed the rate-limiting error from 429 (TOO_MANY_REQUESTS) to 503 (SERVICE_UNAVAILABLE). This change restores 429 to align with the router error documentation.

By @carodewig in https://github.com/apollographql/router/pull/8765

Add status code and error type attributes to http_request spans (PR #8775)

The router now always adds the http.response.status_code attribute to http_request spans (for example, for router -> subgraph requests). The router also conditionally adds error.type for non-success status codes.

By @rohan-b99 in https://github.com/apollographql/router/pull/8775

Report response cache invalidation failures as errors (PR #8813)

The router now returns an error when response cache invalidation fails. Previously, an invalidation attempt could fail without being surfaced as an error.

After you upgrade, you might see an increase in the apollo.router.operations.response_cache.invalidation.error metric.

By @bnjjj in https://github.com/apollographql/router/pull/8813

Reuse response cache Redis connections for identical subgraph configuration (PR #8764)

The response cache now reuses Redis connection pools when subgraph-level configuration resolves to the same Redis configuration as the global all setting. Previously, the router could create redundant Redis connections even when the effective configuration was identical.

Impact: If you configure response caching at both the global and subgraph levels, you should see fewer Redis connections and lower connection overhead.

By @bnjjj in https://github.com/apollographql/router/pull/8764

Prevent TLS connections from hanging when a handshake stalls (PR #8779)

The router listener loop no longer blocks while waiting for a TLS handshake to complete. Use server.http.tls_handshake_timeout to control how long the router waits before terminating a connection (default: 10s).

By @rohan-b99 in https://github.com/apollographql/router/pull/8779

Emit cardinality overflow metrics for more OpenTelemetry error formats (PR #8740)

The router now emits the apollo.router.telemetry.metrics.cardinality_overflow metric for additional OpenTelemetry cardinality overflow error formats.

By @bonnici in https://github.com/apollographql/router/pull/8740

Propagate trace context on WebSocket upgrade requests (PR #8739)

The router now injects trace propagation headers into the initial HTTP upgrade request when it opens WebSocket connections to subgraphs. This preserves distributed trace continuity between the router and subgraph services.

Trace propagation happens during the HTTP handshake only. After the WebSocket connection is established, headers cannot be added to individual messages.

By @theJC in https://github.com/apollographql/router/pull/8739

Stop query planning compute jobs when the parent task is canceled (PR #8741)

Query planning compute jobs now stop when cooperative cancellation cancels the parent task.

By @rohan-b99 in https://github.com/apollographql/router/pull/8741

Reject invalidation requests with unknown fields (PR #8752)

The response cache invalidation endpoint now rejects request payloads that include unknown fields. When unknown fields are present, the router returns HTTP 400 (Bad Request).

By @bnjjj in https://github.com/apollographql/router/pull/8752

Restore plugin access to SubscriptionTaskParams in execution::Request builders (PR #8771)

Plugins and other external crates can use SubscriptionTaskParams with execution::Request builders again. This restores compatibility for plugin unit tests that construct subscription requests.

By @aaronArinder in https://github.com/apollographql/router/pull/8771

Support JWT tokens with multiple audiences (PR #8780)

When issuers or audiences is included in the router's JWK configuration, the router will check each request's JWT for iss or aud and reject requests with mismatches.

Expected behavior:

  • If present, the iss claim must be specified as a string.
    • ✅ The JWK's issuers is empty.
    • ✅ The iss is a string and is present in the JWK's issuers.
    • ✅ The iss is null.
    • ❌ The iss is a string but is not present in the JWK's issuers.
    • ❌ The iss is not a string or null.
  • If present, the aud claim can be specified as either a string or an array of strings.
    • ✅ The JWK's audiences is empty.
    • ✅ The aud is a string and is present in the JWK's audiences.
    • ✅ The aud is an array of strings and at least one of those strings is present in the JWK's audiences.
    • ❌ The aud is not a string or array of strings (i.e., null).

Behavior prior to this change:

  • If the iss was not null or a string, it was permitted (regardless of its value).
  • If the aud was an array, it was rejected (regardless of its value).

By @carodewig in https://github.com/apollographql/router/pull/8780

Enforce feature restrictions for warning-state licenses (PR #8768)

The router now enforces license restrictions even when a license is in a warning state. Previously, warning-state licenses could bypass enforcement for restricted features.

If your deployment uses restricted features, the router returns an error instead of continuing to run.

By @aaronArinder in https://github.com/apollographql/router/pull/8768

🛠 Maintenance

Warn at startup when OTEL_EXPORTER_OTLP_ENDPOINT is set (PR #8729)

The router now displays a warning at startup if the OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set. This variable takes precedence over default configurations and can override trace export to Apollo Studio, so the warning helps you identify when telemetry data might not be sent where expected.

By @apollo-mateuswgoettems in https://github.com/apollographql/router/pull/8729

Increase Redis 'unresponsive' check frequency (PR #8763)

Perform the 'unresponsive' check every two seconds. This aligns with the Redis client's guideline that the check interval should be less than half the timeout value.

By @carodewig in https://github.com/apollographql/router/pull/8763

📚 Documentation

Fix subscription licensing discrepancy in documentation (PR #8726)

Corrected the subscription support documentation to reflect that subscriptions are available on all GraphOS plans (Free, Developer, Standard, and Enterprise) with self-hosted routers.

The documentation previously stated that subscription support was an Enterprise-only feature for self-hosted routers, which was incorrect. Subscriptions are a licensed feature available to all GraphOS plans when the router is connected to GraphOS with an API key and graph ref.

Updated both the configuration and overview pages to remove the misleading Enterprise-only requirement and clarify the actual requirements.

By @the-gigi-apollo in https://github.com/apollographql/router/pull/8726

Clarify traffic shaping compression headers in documentation (PR #8773)

The traffic shaping documentation now clearly explains how the router handles HTTP compression headers for subgraph requests. It clarifies that content-encoding is set when compression is configured via traffic_shaping, while accept-encoding is automatically set on all subgraph requests to indicate the router can accept compressed responses (gzip, br, or deflate). The documentation also notes that these headers are added after requests are added to the debug stack, so they won't appear in the Connectors Debugger.

By @the-gigi-apollo in https://github.com/apollographql/router/pull/8773

Document default histogram buckets and their relationship to timeout settings (PR #8783)

The documentation now explains how histogram bucket configuration affects timeout monitoring in Prometheus and other metrics exporters.

The documentation now includes:

  • Default bucket values: The router's default histogram buckets (0.001 to 10.0 seconds)
  • Timeout behavior: Histogram metrics cap values at the highest bucket boundary, which can make timeouts appear ignored if they exceed ten seconds
  • Customization guidance: Configure custom buckets via telemetry.exporters.metrics.common.buckets to match your timeout settings

This update helps users understand why their timeout metrics may not behave as expected and provides clear guidance on customizing buckets for applications with longer timeout configurations.

By @the-gigi-apollo in https://github.com/apollographql/router/pull/8783

v2.10.0

Long-Term Support

This release is marked for LTS under the v2026.1 LTS Policy for the GraphOS Runtime. It will be supported until September 30, 2026 with patch updates.

🚀 Features

Response caching is now Generally Available 🎉 (PR #8678)

Response caching is now Generally Available (GA) and ready for production use!

Response caching enables the router to cache subgraph query responses using Redis, improving query latency and reducing load on your underlying services. Unlike traditional HTTP caching solutions, response caching provides GraphQL-aware caching at the entity and root field level, making cached data reusable across different users and queries.

For complete documentation, configuration options, and quickstart guide, see the response caching documentation.

Key benefits
  • Improved performance: Cache origin responses and reuse them across queries to reduce latency
  • Reduced subgraph load: Minimize redundant requests to your subgraphs by serving cached data
  • Entity-level caching: Cache individual entity representations independently, enabling fine-grained control over data freshness
  • Flexible cache control: Set different TTLs for different types of data based on @cacheControl directives or Cache-Control response headers
  • Privacy-aware: Share cached data across users while maintaining privacy for personalized data
  • Active cache invalidation: Tag cached data with @cacheTag and invalidate specific cache entries via HTTP endpoint when data changes
What's cached

The router caches two kinds of data:

  • Root query fields: Cached as complete units (the entire response for these root fields)
  • Entity representations: Cached independently—each origin's contribution to an entity is cached separately and can be reused across different queries
Additional features
  • Cache debugger: See exactly what's being cached during development
  • Redis cluster support: Scale your cache with Redis cluster deployments and read replicas
  • Comprehensive metrics: Monitor cache performance with detailed Redis-specific metrics

By @bnjjj in https://github.com/apollographql/router/pull/8678

Support Redis read replicas (PR #8405)

Read-only queries are now sent to replica nodes when using clustered Redis. Previously, all commands were sent to the primary nodes.

This change applies to all Redis caches, including the query plan cache and the response cache.

By @carodewig in https://github.com/apollographql/router/pull/8405

Enable HTTP/2 header size limits for TCP and UDS (PR #8673)

The router's HTTP/2 header size limit configuration option now applies to requests using TCP and UDS (Unix domain sockets). Previously, this setting only worked for TLS connections.

By @aaronArinder in https://github.com/apollographql/router/pull/8673

🐛 Fixes

Enable invalidation endpoint when any subgraph has invalidation enabled (PR #8680)

Previously, the response cache invalidation endpoint was only enabled when global invalidation was enabled via response_cache.subgraph.all.invalidation.enabled. If you enabled invalidation for only specific subgraphs without enabling it globally, the invalidation endpoint wouldn't start, preventing cache invalidation requests from being processed.

The invalidation endpoint now starts if either:

  • Global invalidation is enabled (response_cache.subgraph.all.invalidation.enabled: true), OR
  • Any individual subgraph has invalidation enabled

This enables more flexible configuration where you can enable invalidation selectively for specific subgraphs:

response_cache:
  enabled: true
  invalidation:
    listen: 127.0.0.1:4000
    path: /invalidation
  subgraph:
    all:
      enabled: true
      # Global invalidation not enabled
    subgraphs:
      products:
        invalidation:
          enabled: true  # Endpoint now starts
          shared_key: 

By @bnjjj in https://github.com/apollographql/router/pull/8680

Require Redis configuration only when response caching is enabled (PR #8684)

Previously, the router attempted to connect to Redis for response caching regardless of whether response caching was enabled or disabled. This caused unnecessary connection attempts and configuration errors even when the feature was explicitly disabled.

The router now ignores Redis configuration if response caching is disabled. If response caching is configured to be enabled, Redis configuration is required, and missing Redis configuration raises an error on startup:

Error: you must have a redis configured either for all subgraphs or for subgraph "products"

By @bnjjj in https://github.com/apollographql/router/pull/8684

Prevent deleted coprocessor context keys from reappearing in later stages (PR #8679)

Coprocessor context keys deleted in a previous stage no longer reappear in later stages.

By @rohan-b99 in https://github.com/apollographql/router/pull/8679

Customize response caching behavior at the subgraph level (PR #8652)

You can now customize cached responses using Rhai or coprocessors. You can also set a different private_id based on subgraph request headers.

Example Rhai script customizing private_id:

fn subgraph_service(service, subgraph) {
    service.map_request(|request| {
        if "private_id" in request.headers {
            request.context["private_id"] = request.headers["private_id"];
        }
    });
}

By @bnjjj in https://github.com/apollographql/router/pull/8652

Prevent glibc mismatch in DIY Docker images (Issue #8450)

The DIY Dockerfile now pins the Rust builder to the Bookworm variant (for example, rust:1.91.1-slim-bookworm) so the builder and runtime share the same Debian base. This prevents the image from failing at startup with /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39' not found.

This resolves a regression introduced when the rust:1.90.0 bump used a generic Rust image without specifying a Debian variant. The upstream Rust image default advanced to a newer variant with glibc 2.39, although the DIY runtime remained on Bookworm, creating a version mismatch.

By @theJC in https://github.com/apollographql/router/pull/8629

Correct response cache fetch error metric (PR #8711)

The apollo.router.operations.response_cache.fetch.error metric was out of sync with the apollo.router.cache.redis.errors metric because errors weren't being returned from the Redis client wrapper. The response caching plugin now increments the error metric as expected.

By @carodewig in https://github.com/apollographql/router/pull/8711

Emit http.client.request.body.size metric correctly (PR #8712)

The histogram for http.client.request.body.size was using the SubgraphRequestHeader selector, looking for Content-Length before it had been set in on_request, so http.client.request.body.size wasn't recorded. The router now uses the on_response handler and stores the body size in the request context extensions.

By @rohan-b99 in https://github.com/apollographql/router/pull/8712

Record http.server.response.body.size metric correctly (PR #8697)

Previously, the http.server.response.body.size metric wasn't recorded because the router attempted to read from the Content-Length header before it had been set. The router now uses the size_hint of the body if it's exact.

By @rohan-b99 in https://github.com/apollographql/router/pull/8697

Treat interface objects as entities in response caching (PR #8582)

Interface objects can be entities, but response caching wasn't treating them that way. Interface objects are now respected as entities so they can be used as cache keys.

By @aaronArinder in https://github.com/apollographql/router/pull/8582

🛠 Maintenance

Warn when Datadog propagator isn't exclusively active (PR #8677)

The router now validates propagator configuration and emits a warning log if:

  • The Datadog propagator is enabled and any other propagators are enabled (except baggage)
  • Datadog tracing is enabled and other propagators are enabled (except baggage)

By @rohan-b99 in https://github.com/apollographql/router/pull/8677

🚀 Features

Add CORS Private Network Access support (PR #8279)

CORS configuration now supports private network access (PNA). Enable PNA for a CORS policy by specifying the private_network_access field, which supports two optional subfields: access_id and access_name.

Example configuration:

cors:
  policies:
    - origins: ["https://studio.apollographql.com"]
      private_network_access:
        access_id:
    - match_origins: ["^https://(dev|staging|www)?\\.my-app\\.(com|fr|tn)$"]
      private_network_access:
        access_id: "01:23:45:67:89:0A"
        access_name: "mega-corp device"

By @TylerBloom in https://github.com/apollographql/router/pull/8279

Configure maximum HTTP/2 header list size (PR #8636)

The router now supports configuring the maximum size for HTTP/2 header lists via the limits.http2_max_headers_list_bytes setting. This protects against excessive resource usage from clients sending large sets of HTTP/2 headers.

The default remains 16KiB. When a client sends a request with HTTP/2 headers whose total size exceeds the configured limit, the router rejects the request with a 431 error code.

Example configuration:

limits:
  http2_max_headers_list_bytes: "48KiB"

By @aaronArinder in https://github.com/apollographql/router/pull/8636

Customize response cache key per subgraph via context (PR #8543)

The response cache key can now be customized per subgraph using the apollo::response_cache::key context entry. The new subgraphs field enables defining separate cache keys for individual subgraphs.

Subgraph-specific data takes precedence over data in the all field—the router doesn't merge them. To set common data when providing subgraph-specific data, add it to the subgraph-specific section.

Example payload:

{
  "all": 1,
  "subgraph_operation1": "key1",
  "subgraph_operation2": {
    "data": "key2"
  },
  "subgraphs": {
    "my_subgraph": {
      "locale": "be"
    }
  }
}

By @bnjjj in https://github.com/apollographql/router/pull/8543

Add telemetry selector for Cache-Control metrics (PR #8524)

The new response_cache_control selector enables telemetry metrics based on the computed Cache-Control header from subgraph responses.

Example configuration:

telemetry:
  exporters:
    metrics:
      common:
        service_name: apollo-router
        views:
          - name: subgraph.response.cache_control.max_age
            aggregation:
              histogram:
                buckets:
                - 10
                - 100
                - 1000
                - 10000
                - 100000
  instrumentation:
    instruments:
      subgraph:
        subgraph.response.cache_control.max_age:
          value:
            response_cache_control: max_age
          type: histogram
          unit: s
          description: A histogram of the computed TTL for a subgraph response

By @bnjjj in https://github.com/apollographql/router/pull/8524

🐛 Fixes

Remove _redacted suffix from event attributes in apollo.router.state.change.total metric (Issue #8464)

Event names in the apollo.router.state.change.total metric no longer include the _redacted suffix. The metric now uses the Display trait instead of Debug for event names, changing values like updateconfiguration_redacted to updateconfiguration in APM platforms.

The custom behavior for UpdateLicense events is retained—the license state name is still appended.

By @rohan-b99 in https://github.com/apollographql/router/pull/8464

Preserve Content-Length header for responses with known size (Issue #7941)

The router now uses the Content-Length header for GraphQL responses with known content lengths instead of transfer-encoding: chunked. Previously, the fleet_detector plugin destroyed HTTP body size hints when collecting metrics.

This extends the fix from #6538, which preserved size hints for router → subgraph requests, to also cover client → router requests and responses. Size hints now flow correctly through the entire pipeline for optimal HTTP header selection.

By @morriswchris in https://github.com/apollographql/router/pull/7977

Correct apollo.router.operations.subscriptions.events metric counting (PR #8483)

The apollo.router.operations.subscriptions.events metric now increments correctly for each subscription event (excluding ping/pong/close messages). The counter call has been moved into the stream to trigger on each event.

This change also removes custom pong response handling before connection acknowledgment, which previously caused duplicate pongs because the WebSocket implementation already handles pings by default.

By @rohan-b99 in https://github.com/apollographql/router/pull/8483

Unify timeout codes in response caching metrics (PR #8515)

Tokio- and Redis-based timeouts now use the same timeout code in apollo.router.operations.response_cache.*.error metrics. Previously, they were inadvertently given different code values.

By @carodewig in https://github.com/apollographql/router/pull/8515

📃 Configuration

Remove unused TTL parameter from response cache Redis configuration (PR #8513)

The ttl parameter under redis configuration had no effect and is removed. Configure TTL at the subgraph level to control cache entry expiration:

preview_response_cache:
  enabled: true
  subgraph:
    all:
      enabled: true
      ttl: 10m  # ✅ Configure TTL here
      redis:
        urls: [ "redis://..." ]
        # ❌ ttl was here previously (unused)

By @carodewig in https://github.com/apollographql/router/pull/8513

📚 Documentation

Document active subgraph requests selector (PR #8530)

The telemetry selectors documentation now correctly reflects the active_subgraph_requests attribute.

By @faisalwaseem in https://github.com/apollographql/router/pull/8530

Add Redis cache suggestions to response cache documentation (PR #8624)

The FAQ now includes information about supported Redis versions and Redis key eviction setup.

By @carodewig in https://github.com/apollographql/router/pull/8624

🐛 Fixes

Support arrays in complex @key fields for entity caching (PR #8367)

Entity caching now supports arrays (including arrays of objects and scalars) in complex @key fields when resolving entities by key. This improves entity matching when using complex @key fields as primary cache keys.

By @aaronArinder, @bnjjj, and @duckki in https://github.com/apollographql/router/pull/8367

Parse scientific notation correctly in Rhai scripts (PR #8528)

The router now correctly parses scientific notation (like 1.5e10) in Rhai scripts and JSON operations. Previously, the Rhai scripting engine failed to parse these numeric formats, causing runtime errors when your scripts processed data containing exponential notation.

This fix upgrades Rhai from 1.21.0 to 1.23.6, resolving the parsing issue and ensuring your scripts handle scientific notation seamlessly.

By @BrynCooke in https://github.com/apollographql/router/pull/8528

Support enum types in @cacheTag directive format (PR #8496)

Composition validation no longer raises an error when using enum types in the @cacheTag directive's format argument. Previously, only scalar types were accepted.

Example:

type Query {
  testByCountry(id: ID!, country: Country!): Test
    @cacheTag(format: "test-{.id}-{.country}")
}

By @bnjjj in https://github.com/apollographql/router/pull/8496

Improve debugging data with caching flag and enhanced warnings (PR #8459)

Debugging data now includes a flag that indicates to Apollo Sandbox whether the data should be cached, preventing unnecessary local computation. This update also includes improved warnings.

By @bnjjj in https://github.com/apollographql/router/pull/8459

Display cache tags from subgraph responses in debugger (PR #8531)

The debugger now displays cache tags generated from subgraph responses (in extensions). For performance reasons, these generated cache tags are only displayed when the data has been cached in debug mode.

By @bnjjj in https://github.com/apollographql/router/pull/8531

📚 Documentation

Clarify guidance for OpenTelemetry "Recommended" attributes in telemetry documentation

The router telemetry documentation now clarifies that OpenTelemetry's "Recommended" attributes from their development-status GraphQL semantic conventions are experimental and still evolving. Apollo recommends using required attributes instead of recommended attributes because of high cardinality, security, and performance risks with attributes like graphql.document.

Learn more in Router Telemetry.

By @abernix

🧪 Experimental

Prevent panic when record/replay plugin encounters non-UTF-8 header values (PR #8485)

The record/replay plugin no longer panics when externalizing headers with invalid UTF-8 values. Instead, the plugin writes the header keys and errors to a header_errors object for both requests and responses.

By @rohan-b99 in https://github.com/apollographql/router/pull/8485

v1.61.12

Long-Term Support

This release is part of the LTS of Router v1.61. It is marked for End-of-Support on March 30, 2026.

🔒 Security

Note

For more information on the impact of the fixes in this release and how your deployment might be affected or remediated, see the corresponding GitHub Security Advisory (GHSA) linked on the entries below. In both listed cases, updating to a patched Router version will resolve any vulnerabilities.

Fix authorization plugin handling of polymorphic types

Updates the auth plugin to correctly handle access control requirements when processing polymorphic types.

When querying interface types/fields, the auth plugin was verifying only whether all implementations shared the same access control requirements. In cases where interface types/fields did not specify the same access control requirements as the implementations, this could result in unauthorized access to protected data.

The auth plugin was updated to correctly verify that all polymorphic access control requirements are satisfied by the current context.

See GHSA-x33c-7c2v-mrj9 for additional details and the associated CVE number.

By @dariuszkuc

Fixed authorization plugin handling of directive renames

The router auth plugin did not properly handle access control requirements when subgraphs renamed their access control directives through imports. When such renames occurred, the plugin’s @link-processing code ignored the imported directives entirely, causing access control constraints defined by the renamed directives to be ignored.

The plugin code was updated to call the appropriate functionality in the apollo-federation crate, which correctly handles both because spec and imports directive renames.

See GHSA-g8jh-vg5j-4h3f for additional details and the associated CVE number.

By @sachindshinde

🔒 Security

Note

For more information on the impact of the fixes in this release and how your deployment might be affected or remediated, see the corresponding GitHub Security Advisory (GHSA) linked on the entries below. In both listed cases, updating to a patched Router version will resolve any vulnerabilities.

Fix authorization plugin handling of polymorphic types

Updates the auth plugin to correctly handle access control requirements when processing polymorphic types.

When querying interface types/fields, the auth plugin was verifying only whether all implementations shared the same access control requirements. In cases where interface types/fields did not specify the same access control requirements as the implementations, this could result in unauthorized access to protected data.

The auth plugin was updated to correctly verify that all polymorphic access control requirements are satisfied by the current context.

See GHSA-x33c-7c2v-mrj9 for additional details and the associated CVE number.

By @dariuszkuc

Fixed authorization plugin handling of directive renames

The router auth plugin did not properly handle access control requirements when subgraphs renamed their access control directives through imports. When such renames occurred, the plugin’s @link-processing code ignored the imported directives entirely, causing access control constraints defined by the renamed directives to be ignored.

The plugin code was updated to call the appropriate functionality in the apollo-federation crate, which correctly handles both because spec and imports directive renames.

See GHSA-g8jh-vg5j-4h3f for additional details and the associated CVE number.

By @sachindshinde

Last Checked
32m ago
Latest
v2.10.4
Tracking since Mar 26, 2025