Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
23cff25
feat: add configuration options for per-subgraph control
carodewig Jan 22, 2026
e2cb542
feat: compute estimated cost per subgraph
carodewig Jan 23, 2026
82fd252
chore: don't duplicate context update
carodewig Jan 23, 2026
d01960e
feat: store estimated cost by subgraph in context
carodewig Jan 23, 2026
4373f8d
feat: reject subgraph queries when they are too expensive
carodewig Jan 23, 2026
72bcc91
test: check that per-subgraph all does not override configured max
carodewig Jan 23, 2026
7698a99
test: ensure that requests can partially succeed by excluding just on…
carodewig Jan 23, 2026
72f803d
test: add tests that demonstrate changes based on list size
carodewig Jan 23, 2026
546c2e4
test: add cost/result by subgraph to snapshots
carodewig Jan 23, 2026
00ac15b
chore: doc line for tests
carodewig Jan 23, 2026
2eb9475
feat: send per-subgraph costs to rhai
carodewig Jan 23, 2026
f997c4d
test: add test for rhai access to estimates/actuals/results by subgraph
carodewig Jan 23, 2026
78a8a93
chore: remove accidental dbg
carodewig Jan 23, 2026
6fc5c73
test: make sure that coprocessors can access new costs
carodewig Jan 23, 2026
e99216c
chore: fix lints
carodewig Jan 26, 2026
0cbfafc
test: ensure that requests are not rejected in measure mode
carodewig Jan 26, 2026
54baea2
test: remove redundant tests
carodewig Jan 26, 2026
959d242
doc: add subgraph to sample config
carodewig Jan 26, 2026
6d83fce
doc: fix typo
carodewig Jan 26, 2026
1d6ae21
doc: add subgraph-level demand control documentation
carodewig Jan 26, 2026
39fa902
doc: include errors, context, and rhai in docs
carodewig Jan 26, 2026
dd81801
doc: add changeset
carodewig Jan 26, 2026
1e0a6f3
test: refactor requests_exceeding_one_subgraph_cost_are_accepted for …
carodewig Jan 27, 2026
57eb77c
test: use upsert to avoid clobbering existing values
carodewig Jan 28, 2026
3a07ae8
test: demand control non-clobbery
aaronArinder Jan 28, 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
62 changes: 62 additions & 0 deletions .changesets/feat_caroline_demand_control_by_subgraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
### 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