Join us from October 8-10 in New York City to learn the latest tips, trends, and news about GraphQL Federation and API platform engineering.Join us for GraphQL Summit 2024 in NYC
Docs
Start for Free
Since 1.48.0

Demand Control

Protect your graph from high-cost GraphQL operations


Want to learn about graph security in-person?

Don't miss the Securing your graph: A defense-in-depth strategy workshop at this year's GraphQL Summit.

This feature is only available with a GraphOS Enterprise plan.
You can test it out by signing up for a free Enterprise trial.

What is demand control?

Demand control provides a way to secure your from overly complex , based on the IBM GraphQL Cost Directive specification.

Application clients can send overly costly operations that overload your supergraph infrastructure. These operations may be costly due to their complexity and/or their need for expensive . In either case, demand control can help you protect your infrastructure from these expensive operations. When your receives a request, it calculates a cost for that operation. If the cost is greater than your configured maximum, the operation is rejected.

Calculating cost

When calculating the cost of an , the router sums the costs of the sub-requests that it plans to send to your .

  • For each operation, the cost is the sum of its base cost plus the costs of its .
  • For each , the cost is defined recursively as its own base cost plus the cost of its selections. In the IBM specification, this is called field cost.

The cost of each operation type:

MutationQuerySubscription
type1000

The cost of each element type, per operation type:

MutationQuerySubscription
Object111
Interface111
Union111
Scalar000
Enum000

Using these defaults, the following operation would have a cost of 4.

query BookQuery {
book(id: 1) {
title
author {
name
}
publisher {
name
address {
zipCode
}
}
}
}

Customizing cost

Since version 1.53.0, the router supports customizing the cost calculation with the @cost . The @cost directive has a single , weight, which overrides the default weights from the table above.

NOTE

The @cost directive differs from the IBM specification in that the weight argument is of type Int! instead of String!.

Annotating your schema with the @cost directive customizes how the router scores operations. For example, imagine that the Address for an example is particularly expensive. We can annotate the schema with the @cost directive with a larger weight:

type Query {
book(id: ID): Book
}
type Book {
title: String
author: Author
publisher: Publisher
}
type Author {
name: String
}
type Publisher {
name: String
address: Address
}
type Address
@cost(weight: 5) {
zipCode: Int!
}

This increases the cost of BookQuery from 4 to 8.

Handling list fields

During the static analysis phase of demand control, the router doesn't know the size of the list fields in a given query. It must use estimates for list sizes. The closer the estimated list size is to the actual list size for a field, the closer the estimated cost will be to the actual cost.

NOTE

The difference between estimated and actual operation cost calculations is due only to the difference between assumed and actual sizes of list fields.

There are two ways to indicate the expected list sizes to the router:

The @listSize directive supports field-level granularity in setting list size. By using its assumedSize argument, you can set a statically defined list size for a field. If you are using paging parameters which control the size of the list, use the slicingArguments argument.

Continuing with our example above, let's add two queryable fields. First, we will add a field which returns the top five best selling books:

type Query {
book(id: ID): Book
bestsellers: [Book] @listSize(assumedSize: 5)
}

With this schema, the following query has a cost of 40:

query BestsellersQuery {
bestsellers {
title
author {
name
}
publisher {
name
address {
zipCode
}
}
}
}

The second field we will add is a paginated resolver. It returns the latest additions to the inventory:

type Query {
book(id: ID): Book
bestsellers: [Book] @listSize(assumedSize: 5)
newestAdditions(after: ID, limit: Int!): [Book]
@listSize(slicingArguments: ["limit"])
}

The number of books returned by this resolver is determined by the limit argument.

query NewestAdditions {
newestAdditions(limit: 3) {
title
author {
name
}
publisher {
name
address {
zipCode
}
}
}
}

The router will estimate the cost of this query as 24. If the limit was increased to 7, then the cost would increase to 56.

When requesting 3 books:
1 Query (0) + 3 book objects (3 * (1 book object (1) + 1 author object (1) + 1 publisher object (1) + 1 address object (5))) = 24 total cost
When requesting 7 books:
1 Query (0) + 3 book objects (7 * (1 book object (1) + 1 author object (1) + 1 publisher object (1) + 1 address object (5))) = 56 total cost

Configuring demand control

To enable demand control in the router, configure the demand_control option in router.yaml:

router.yaml
demand_control:
enabled: true
mode: measure
strategy:
static_estimated:
list_size: 10
max: 1000

When demand_control is enabled, the router measures the cost of each operation and can enforce operation cost limits, based on additional configuration.

Customize demand_control with the following settings:

OptionValid valuesDefault valueDescription
enabledbooleanfalseSet true to measure operation costs or enforce operation cost limits.
modemeasure, enforce--- measure collects information about the cost of operations.
- enforce rejects operations exceeding configured cost limits
strategystatic_estimated--static_estimated estimates the cost of an operation before it is sent to a subgraph
static_estimated.list_sizeinteger--The assumed maximum size of a list for fields that return lists.
static_estimated.maxinteger--The maximum cost of an accepted operation. An operation with a higher cost than this is rejected.

When enabling demand_control for the first time, set it to measure mode. This will allow you to observe the cost of your operations before setting your maximum cost.

Telemetry for demand control

💡 TIP

New to router telemetry? See Router Telemetry.

You can define router telemetry to gather cost information and gain insights into the cost of operations sent to your router:

  • Generate histograms of operation costs by , where the estimated cost is greater than an arbitrary value.
  • Attach cost information to spans.
  • Generate log messages whenever the cost delta between estimated and actual is greater than an arbitrary value.

Instruments

InstrumentDescription
cost.actualThe actual cost of an operation, measured after execution.
cost.estimatedThe estimated cost of an operation before execution.
cost.deltaThe difference between the actual and estimated cost.

Attributes

Attributes for cost can be applied to instruments, spans, and events—anywhere supergraph attributes are used.

AttributeValueDescription
cost.actualbooleanThe actual cost of an operation, measured after execution.
cost.estimatedbooleanThe estimated cost of an operation before execution.
cost.deltabooleanThe difference between the actual and estimated cost.
cost.resultbooleanThe return code of the cost calculation. COST_OK or an error code

Selectors

Selectors for cost can be applied to instruments, spans, and events—anywhere supergraph attributes are used.

KeyValueDefaultDescription
costestimated, actual, delta, resultThe estimated, actual, or delta cost values, or the result string

Examples

Example instrument

Enable a cost.estimated instrument with the cost.result attribute:

router.yaml
telemetry:
instrumentation:
instruments:
supergraph:
cost.estimated:
attributes:
cost.result: true
graphql.operation.name: true

Example span

Enable the cost.estimated attribute on supergraph spans:

router.yaml
telemetry:
instrumentation:
spans:
supergraph:
attributes:
cost.estimated: true

Example event

Log an error when cost.delta is greater than 1000:

router.yaml
telemetry:
instrumentation:
events:
supergraph:
COST_DELTA_TOO_HIGH:
message: "cost delta high"
on: event_response
level: error
condition:
gt:
- cost: delta
- 1000
attributes:
graphql.operation.name: true
cost.delta: true

Filtering by cost result

In router telemetry, you can customize instruments that filter their output based on cost results.

For example, you can record the estimated cost when cost.result is COST_ESTIMATED_TOO_EXPENSIVE:

router.yaml
telemetry:
instrumentation:
instruments:
supergraph:
# custom instrument
cost.rejected.operations:
type: histogram
value:
# Estimated cost is used to populate the histogram
cost: estimated
description: "Estimated cost per rejected operation."
unit: delta
condition:
eq:
# Only show rejected operations.
- cost: result
- "COST_ESTIMATED_TOO_EXPENSIVE"
attributes:
graphql.operation.name: true # Graphql operation name is added as an attribute

Configuring instrument output

When analyzing the costs of operations, if your histograms are not granular enough or don't cover a sufficient range, you can modify the views in your telemetry configuration:

telemetry:
exporters:
metrics:
common:
views:
# Define a custom view because cost is different than the default latency-oriented view of OpenTelemetry
- name: cost.*
aggregation:
histogram:
buckets:
- 0
- 10
- 100
- 1000
- 10000
- 100000
- 1000000

An example chart of a histogram:

You can also chart the percentage of operations that would be allowed or rejected with the current configuration:

Previous
Safelisting with Persisted Queries
Next
Privacy and Data Collection
Rate articleRateEdit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc., d/b/a Apollo GraphQL.

Privacy Policy

Company