Query Cost Limits
A depth limit doesn't stop a short query from asking for a lot: three nested
connections of 40 nodes each are 64 000 rows in a dozen lines. Chemist knows how
large every sc.connection page can get, so it can work out an operation's
worst case before running it, and reject operations over a limit.
The estimate
sc.estimate_query_cost(schema, document, operation_name=None, variables=None)
returns a QueryCost:
objects: how many GraphQL objects (every{...}in the response, each list item separately) the operation may returnfields: how many fields it may resolve, leaves included
Both are upper bounds, counted like this:
- a connection's page is as large as its arguments ask, capped the way its
pagination policy caps it (
max_limit, the nested input's limit of 40); the page size multiplies everything selected underedges/items - a plain list field counts
default_list_sizeitems (1 unless you pass another), unless it has a cost hint - fields asked twice under one response key are counted once, aliases each
- a union or interface selection costs as much as its most expensive member
@skip/@includefollow their condition__typenameand introspection fields are free
With variables=None every variable is assumed at its worst (pages at
max_limit, conditional fields included). That suits checking a client's
documents ahead of time, say in CI:
cost = sc.estimate_query_cost(schema, open("getPost.graphql").read())
assert cost.objects < 10_000
Rejecting expensive operations
schema = strawberry.Schema(
query=Query,
extensions=[
*sc.extensions(),
lambda: sc.QueryCostLimiter(max_objects=10_000, max_fields=50_000),
],
)
The limiter estimates every operation with its real variables after validation,
and if max_objects or max_fields is exceeded answers with one error and no
data; nothing is resolved. For
{ authors(pagination: {first: 40}) { edges { node {
books(pagination: {first: 40}) { edges { node { author {
books(pagination: {first: 40}) { edges { node { title } } }
} } } }
} } } }
(64 000 books at the bottom, each with its edge) the error's extensions are
{
"code": "QUERY_COST_EXCEEDED",
"objects": 134521,
"fields": 134522,
"maxObjects": 10000,
"maxFields": 50000
}
and its original_error is a strawberry_chemist.query_cost.QueryCostExceeded
with the cost on it.
Options:
dry_run=Trueruns everything and logs the operations that would be rejected (a warning on thestrawberry_chemist.query_costlogger): measure your clients before turning it oncallback=lambda cost, execution_context: ...gets every operation's cost, for logs or metricsdefault_list_sizeas in the estimate
Strawberry builds a new extension per request from a factory (lambda above).
Older strawberry-graphql versions call the factory with execution_context=;
functools.partial(sc.QueryCostLimiter, max_objects=10_000) works with all of
them.
Fields Chemist can't size
sc.connection fields are sized by their pagination policy. For anything else,
tell the estimate with sc.cost(...) in the field's metadata.
A plain list with a known bound:
@strawberry.field(metadata=sc.cost(list_size=5))
def first_five_objects(self) -> list[ActivityObject]: ...
A resolver that pages by itself: give it the policy its arguments follow, and
its page size is worked out from the arguments like a sc.connection's:
@strawberry.field(
metadata=sc.cost(pagination=sc.CursorPagination(max_limit=20, nested=True))
)
async def activity(
self, info: Info, pagination: CursorPaginationInput
) -> sc.Connection[ActivityNode]: ...
The page size multiplies the list fields of the returned connection (edges);
on a field that returns a list itself, it's the length of that list.
sc.relationship(...), sc.connection(...) and sc.node_lookup(...) take
metadata= too. A custom
pagination policy is sized by its page_size(arguments) method when it has one
(CursorPagination and OffsetPagination do), by its max_limit otherwise.