Skip to content

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.

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:

# settings.py
DJANGO_GRAPHEX = {
    "MAX_QUERY_DEPTH": 10,   # None (default) disables the 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:

cost(field) = own_cost + multiplier × Σ cost(children)
  • own_cost0 for scalar leaves, 1 for object/list fields, or the type's Meta.complexity when declared.
  • multiplier — a list field's page size (the limit / page_size / first / last argument), capped at MAX_PAGE_SIZE; 1 otherwise.

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:

{ "data": { ... }, "extensions": { "cost": { "requestedCost": 411, "maxCost": 1000 } } }

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).