Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
24b228e
feat: add mode to support toggling between cost calculation modes
carodewig Jan 21, 2026
4808218
feat: compute cost on each subgraph response when enabled
carodewig Jan 21, 2026
007da11
fix: support cost calculation for `_entities` query
carodewig Jan 21, 2026
bea0e31
test: update snapshots for new actuals
carodewig Jan 21, 2026
23d4e3c
test: add call count to context to assert in tests
carodewig Jan 21, 2026
65ca9c2
test: add integration tests for demand control
carodewig Jan 22, 2026
1f77c2c
test: clean up tests to simplify via #[values]
carodewig Jan 22, 2026
9e23447
test: additional cases based on federated_ships_fragment
carodewig Jan 22, 2026
6641c6b
test: fix snapshot with new cost
carodewig Jan 22, 2026
337f6a5
doc: add new config option to the docs
carodewig Jan 22, 2026
7e84668
doc: create changeset
carodewig Jan 22, 2026
b73e877
Merge branch 'dev' into caroline/demand-control-actuals
carodewig Jan 22, 2026
643a14e
chore: refactor StrategyConfig::validate
carodewig Jan 23, 2026
468b80b
test: test_cache_metrics is super flaky
carodewig Jan 26, 2026
0b25099
test: remove dbg! that can cause test to timeout
carodewig Jan 28, 2026
32285c3
chore: refactor s/legacy/response_shape
carodewig Jan 29, 2026
314bbe1
Merge branch 'dev' into caroline/demand-control-actuals
carodewig Jan 29, 2026
e0113cc
feat: add configuration options for per-subgraph control
carodewig Jan 22, 2026
02aebb0
feat: compute estimated cost per subgraph
carodewig Jan 23, 2026
fae723e
chore: don't duplicate context update
carodewig Jan 23, 2026
6a5a8b0
feat: store estimated cost by subgraph in context
carodewig Jan 23, 2026
db2d8ff
feat: reject subgraph queries when they are too expensive
carodewig Jan 23, 2026
66647a3
test: check that per-subgraph all does not override configured max
carodewig Jan 23, 2026
d2910b4
test: ensure that requests can partially succeed by excluding just on…
carodewig Jan 23, 2026
698a525
test: add tests that demonstrate changes based on list size
carodewig Jan 23, 2026
4fcddbd
test: add cost/result by subgraph to snapshots
carodewig Jan 23, 2026
6fda99c
chore: doc line for tests
carodewig Jan 23, 2026
04cc5da
feat: send per-subgraph costs to rhai
carodewig Jan 23, 2026
c0657c8
test: add test for rhai access to estimates/actuals/results by subgraph
carodewig Jan 23, 2026
59813f1
test: make sure that coprocessors can access new costs
carodewig Jan 23, 2026
8f71074
chore: fix lints
carodewig Jan 26, 2026
f9150f1
test: ensure that requests are not rejected in measure mode
carodewig Jan 26, 2026
20197e8
test: remove redundant tests
carodewig Jan 26, 2026
ee6ce91
doc: add subgraph to sample config
carodewig Jan 26, 2026
76a178a
doc: fix typo
carodewig Jan 26, 2026
ad768d2
doc: add subgraph-level demand control documentation
carodewig Jan 26, 2026
c639761
doc: include errors, context, and rhai in docs
carodewig Jan 26, 2026
1f659df
doc: add changeset
carodewig Jan 26, 2026
641c33a
test: refactor requests_exceeding_one_subgraph_cost_are_accepted for …
carodewig Jan 27, 2026
4b043f7
test: use upsert to avoid clobbering existing values
carodewig Jan 28, 2026
ac0201a
chore: address typos
carodewig Jan 29, 2026
aee2352
chore: reformat doc
carodewig Jan 29, 2026
a20370e
chore: remove trailing *
carodewig Jan 29, 2026
9128300
test: demand control non-clobbery
aaronArinder Jan 28, 2026
1ce842e
doc: use html bc <> breaks things
carodewig Jan 29, 2026
ee36cf6
Merge branch 'dev' into caroline/demand-control-by-subgraph
carodewig Feb 3, 2026
e58dcb4
test: fix renaming from merge
carodewig Feb 3, 2026
e441c64
doc: apply PR suggestions
carodewig Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .changesets/feat_caroline_demand_control_by_subgraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
### Support subgraph-level demand control ([PR #8829](https://github.com/apollographql/router/pull/8829))

Subgraph-level demand control lets you enforce per-subgraph query cost limits in Apollo 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

```yaml
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, which fetches a list of products from a products subgraph and then performs an entity
lookup for each product in a reviews subgraph. Assume that the products cost is 10 and the reviews cost is 5, leading to
a total estimated cost of 15 (10 + 5).

Previously, you would only be able to restrict that query via `demand_control.static_estimated.max`:

* If you set it <= 15, the query would execute
* If you set it >15, the query would be rejected

This feature allows much more granular control. In addition to `demand_control.static_estimated.max`, which operates as
before, there are also per subgraph maxes.

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

By [@carodewig](https://github.com/carodewig) in https://github.com/apollographql/router/pull/8829
Original file line number Diff line number Diff line change
Expand Up @@ -9170,6 +9170,21 @@ expression: "&schema"
"description": "The maximum cost of a query",
"format": "double",
"type": "number"
},
"subgraph": {
"allOf": [
{
"$ref": "#/definitions/SubgraphSubgraphStrategyConfigConfiguration"
}
],
"default": {
"all": {
"list_size": null,
"max": null
},
"subgraphs": {}
},
"description": "Cost control by subgraph"
}
},
"required": [
Expand Down Expand Up @@ -10441,6 +10456,28 @@ expression: "&schema"
},
"type": "object"
},
"SubgraphStrategyConfig": {
"properties": {
"list_size": {
"description": "The assumed length of lists returned by the operation for this subgraph.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"max": {
"description": "The maximum query cost routed to this subgraph.",
"format": "double",
"type": [
"number",
"null"
]
}
},
"type": "object"
},
"SubgraphSubgraphApqConfiguration": {
"description": "Configuration options pertaining to the subgraph server component.",
"properties": {
Expand Down Expand Up @@ -10524,6 +10561,32 @@ expression: "&schema"
},
"type": "object"
},
"SubgraphSubgraphStrategyConfigConfiguration": {
"description": "Configuration options pertaining to the subgraph server component.",
"properties": {
"all": {
"allOf": [
{
"$ref": "#/definitions/SubgraphStrategyConfig"
}
],
"default": {
"list_size": null,
"max": null
},
"description": "options applying to all subgraphs"
},
"subgraphs": {
"additionalProperties": {
"$ref": "#/definitions/SubgraphStrategyConfig"
},
"default": {},
"description": "per subgraph options",
"type": "object"
}
},
"type": "object"
},
"SubgraphTlsClientConfiguration": {
"description": "Configuration options pertaining to the subgraph server component.",
"properties": {
Expand Down
15 changes: 15 additions & 0 deletions apollo-router/src/configuration/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,21 @@ where
pub(crate) fn get(&self, subgraph_name: &str) -> &T {
self.subgraphs.get(subgraph_name).unwrap_or(&self.all)
}

// Create a new `SubgraphConfiguration<V>` by extracting a value `V` from `&T`
pub(crate) fn extract<V: Default + Serialize + JsonSchema>(
&self,
extract_fn: fn(&T) -> V,
) -> SubgraphConfiguration<V> {
SubgraphConfiguration {
all: extract_fn(&self.all),
subgraphs: self
.subgraphs
.iter()
.map(|(k, v)| (k.clone(), extract_fn(v)))
.collect(),
}
}
}

impl<T> Debug for SubgraphConfiguration<T>
Expand Down
56 changes: 56 additions & 0 deletions apollo-router/src/plugins/demand_control/cost_calculator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ pub(in crate::plugins::demand_control) mod schema;
pub(crate) mod static_cost;

use std::collections::HashMap;
use std::ops::AddAssign;

use crate::plugins::demand_control::DemandControlError;

#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
pub(crate) struct CostBySubgraph(HashMap<String, f64>);
impl CostBySubgraph {
pub(crate) fn new(subgraph: &str, value: f64) -> Self {
Self(HashMap::from([(subgraph.to_string(), value)]))
}

pub(crate) fn add_or_insert(&mut self, subgraph: &str, value: f64) {
if let Some(subgraph_cost) = self.0.get_mut(subgraph) {
*subgraph_cost += value;
Expand All @@ -17,7 +22,58 @@ impl CostBySubgraph {
}
}

pub(crate) fn get(&self, subgraph: &str) -> Option<f64> {
self.0.get(subgraph).copied()
}

pub(crate) fn total(&self) -> f64 {
self.0.values().sum()
}

/// Creates a new `CostBySubgraph` where each value in the map is the maximum of its value
/// in the two input `CostBySubgraph`s.
///
/// ```rust
/// let cost1 = CostBySubgraph::new("hello", 1.0);
/// let mut cost2 = CostBySubgraph::new("hello", 2.0);
/// cost2.add_or_insert("world", 1.0);
///
/// let max = CostBySubgraph::maximum(cost1, cost2);
/// assert_eq!(max.0.get("hello"), Some(2.0));
/// assert_eq!(max.0.get("world"), Some(1.0));
/// ```
pub(crate) fn maximum(mut cost1: Self, cost2: Self) -> Self {
for (subgraph, value) in cost2.0.into_iter() {
if let Some(subgraph_cost) = cost1.0.get_mut(&subgraph) {
*subgraph_cost = subgraph_cost.max(value);
} else {
cost1.0.insert(subgraph, value);
}
}

cost1
}
}

impl AddAssign for CostBySubgraph {
fn add_assign(&mut self, rhs: Self) {
for (subgraph, value) in rhs.0.into_iter() {
if let Some(subgraph_cost) = self.0.get_mut(&subgraph) {
*subgraph_cost += value;
} else {
self.0.insert(subgraph, value);
}
}
}
}

#[cfg(test)]
impl From<&[(&str, f64)]> for CostBySubgraph {
fn from(values: &[(&str, f64)]) -> Self {
let mut cost = Self(HashMap::default());
for (subgraph, value) in values {
cost.add_or_insert(subgraph, *value);
}
cost
}
}
Loading