Query depth & cost limits¶
GraphQL lets a client shape its own queries, which means a single request can ask
for far more work than you intend — by nesting deeply, or by paging wide.
django-graphex ships two validation rules that reject such queries
before any resolver runs: a depth limiter and a cost analyzer. Both are
enabled by default in the library's GraphQLView.
DepthLimitValidationRule— reject abusively nested queries.CostLimitValidationRule— reject (or just report) queries whose estimated cost exceeds a budget.
All the settings used below live under DJANGO_GRAPHEX — see
Settings for the full reference. For auth,
introspection and protected fields, see Security.
Query depth limiting¶
Deeply related models let a client ask for company { properties { units {
tenant { company { … } } } } }, which can be expensive or abusive.
DepthLimitValidationRule rejects over-nested queries during validation, so
no resolver runs. It counts nested object levels below a field; scalar leaves
do not count, and it follows fragments (so they can't be used to bypass it).
Two sources combine — the most restrictive wins:
| Source | Where | Measured from |
|---|---|---|
| Global default | DJANGO_GRAPHEX['MAX_QUERY_DEPTH'] |
the query root |
| Per-type | Meta.max_deep on a DjangoObjectType / DjangoListObjectType / DjangoModelType |
any field returning that type |
from django_graphex import DjangoModelType
from myapp.models import RentalCompany
class RentalCompanyModelType(DjangoModelType):
class Meta:
model = RentalCompany
max_deep = 2 # from a rental company, allow 2 nested object levels
rentalCompany { # depth 0
name # scalar — free
properties { # depth 1 ✅
units { # depth 2 ✅
tenant { name } # depth 3 ❌ -> "Query exceeds the maximum nesting depth of 2 ..."
}
}
}
Enabling it¶
The library's GraphQLView includes the rule by default — per-type
max_deep works out of the box. To set a global cap:
On a plain BaseGraphQLView (or your own view), add it alongside the standard rules (passing a list
replaces the defaults, so include them):
from graphql.validation import specified_rules
from django_graphex import BaseGraphQLView, DepthLimitValidationRule
class MyGraphQLView(BaseGraphQLView):
validation_rules = (*specified_rules, DepthLimitValidationRule)
What counts as a level
Only fields with a sub-selection (object/list-of-object fields) add depth;
scalars don't. max_deep = 0 forbids selecting any nested object on that
type. With nothing configured, the rule is a no-op.
Query cost analysis¶
Depth limiting doesn't catch a query that is shallow but wide:
rentalCompanies(limit: 100) { properties(limit: 100) { units(limit: 100) { … } } }
can materialize a million objects in three levels. Cost analysis estimates the
work a query asks for during validation and rejects it over a budget — and
can optionally report the cost back to clients. It captures width × depth × page
size in one number:
own_cost—0for scalar leaves,1for object/list fields, or the type'sMeta.complexitywhen declared.multiplier— a list field's page size (thelimit/page_size/first/lastargument), capped atMAX_PAGE_SIZE;1otherwise.
It follows fragments, so they can't be used to under-count.
Configuring¶
# settings.py
DJANGO_GRAPHEX = {
"MAX_QUERY_COST": 1000, # None (default) = never block
"EXPOSE_QUERY_COST": False, # True = add extensions.cost to responses
"DEFAULT_LIST_MULTIPLIER": 10, # used only when no page size / cap is known
"MAX_PAGE_SIZE": 100, # the realistic per-list ceiling (recommended)
# Argument names treated as a list's page size (default below):
"COST_PAGINATION_ARGS": ("limit", "page_size", "first", "last"),
}
| Setting | Default | Effect |
|---|---|---|
MAX_QUERY_COST |
None |
Budget; queries over it are rejected. None never blocks. |
EXPOSE_QUERY_COST |
False |
When True, responses include extensions.cost. |
DEFAULT_LIST_MULTIPLIER |
10 |
Multiplier for a list with no known page size / cap. |
MAX_PAGE_SIZE |
None |
Caps every list multiplier (also a pagination setting). |
COST_PAGINATION_ARGS |
("limit", "page_size", "first", "last") |
Argument names read as a field's page size. |
Declare per-type weights with Meta.complexity, so expensive types eat more of
the budget:
class RentalCompanyModelType(DjangoModelType):
class Meta:
model = RentalCompany
complexity = 5 # base weight to resolve one rental company
Meta.complexity is read on DjangoObjectType, DjangoListObjectType, and
DjangoModelType (forwarded to its generated output type). The library's
GraphQLView enables the rule by default. On a custom view, add it next to
the standard rules (and the depth rule, if you want both):
from graphql.validation import specified_rules
from django_graphex import (
BaseGraphQLView, CostLimitValidationRule, DepthLimitValidationRule,
)
class MyGraphQLView(BaseGraphQLView):
validation_rules = (*specified_rules, DepthLimitValidationRule, CostLimitValidationRule)
Operating modes¶
The two settings give you four behaviors:
MAX_QUERY_COST |
EXPOSE_QUERY_COST |
Behavior |
|---|---|---|
| set | False |
block over budget, silent otherwise |
| set | True |
block and report cost in extensions (GitHub-style) |
None |
True |
observation — never block, just report the cost |
None |
False |
no-op |
When exposed, responses carry:
Observation mode (MAX_QUERY_COST=None, EXPOSE_QUERY_COST=True) is the safe way
to roll this out: watch real costs in production, calibrate complexity weights
and the budget, then set MAX_QUERY_COST to start enforcing.
Set MAX_PAGE_SIZE
The multiplier for a list is its page size. If a list has no page-size
argument in the query and MAX_PAGE_SIZE is None, the cost falls back to
DEFAULT_LIST_MULTIPLIER (a soft guess) and the rule emits a one-shot
RuntimeWarning. Set MAX_PAGE_SIZE so unbounded lists are costed at the
ceiling the server actually enforces — this also closes the limit: $var
bypass, since an unknown variable page size is costed at the cap.
Per-type only (for now)
Weights are declared per type via Meta.complexity. Per-field weights
aren't supported yet (graphene rejects an unknown complexity= kwarg on
Field); wrap an expensive field's return in a type with Meta.complexity,
or watch for a future @cost directive.
Error codes¶
Both rules fail during validation and tag their error with a
machine-readable extensions.code:
| Code | Raised by |
|---|---|
QUERY_TOO_DEEP |
DepthLimitValidationRule |
QUERY_TOO_COMPLEX |
CostLimitValidationRule |
{ "errors": [{ "message": "Query exceeds the maximum nesting depth of 2 for 'RentalCompany'.",
"extensions": { "code": "QUERY_TOO_DEEP" } }] }
See Security for the full table of execution-time error codes (auth, permissions, introspection).