On this page
- Quick read
- Readiness check
- Publish the cost model before the limit
- Scope the review to one schema surface
- Attack the limit with documents a normal client would not send
- Enforce once, at a point you can explain
- Compare computed cost with observed work
- Test batching, caching, and partial failure
- Review cadence and measures with denominators
- Close with a calculator, a test set, and a rollout record
- Role brief
- Frequently asked questions
- Sources
Is the handoff ready?
Use these checks before access expands or the first ticket begins.
Model
Ready: Field weights, depth rule, and list multiplier written
Needs work: A request count limit only
Budget
Ready: Named per client and per operation limit
Needs work: One global number nobody owns
Tests
Ready: Adversarial documents and expected rejection codes
Needs work: One normal query from the frontend
Rollout
Ready: Shadow mode, then enforce, with owner sign off
Needs work: Immediate production block
Publish the cost model before the limit
A GraphQL limit without a published model becomes an arbitrary rejection that nobody can predict. Write the weight of each field, the multiplier for list fields, the depth ceiling, and the maximum page size. Put the document in the repository next to the schema.
The model does not have to be perfect. It has to be stable, testable, and explainable to a client team. A Philippines-based programmer can implement a deterministic calculator and show the same input producing the same cost.
Decide whether the budget applies per operation, per client, or per user session. A generous per operation limit with no per client budget still allows one client to consume the pool.
Name the unit of work the cost represents. If a weight of one means a single indexed lookup, say so and check a few resolvers against that definition. Weights copied from a blog post without a unit become numbers that nobody can defend when a client asks why a query was refused.
Scope the review to one schema surface
The ticket names the schema version, the gateway, the resolver set under review, the calculator location, the rejection status, and the owner of the client allowlist. Ask for a test command and a table of cost against measured work.
Start with the public read surface. Mutations and subscriptions have different failure modes and should follow in a second ticket once the read limit is stable.
State the rollout order: log only, then reject above a high threshold, then tighten. Each stage needs an observation window and a named person who decides to move on.
Attack the limit with documents a normal client would not send
Alias inflation duplicates the same field many times in one document and multiplies resolver work without increasing apparent depth. Nested fragments spread the cost across files that look small in isolation. Wide list fields with large page sizes multiply everything beneath them.
Ask the programmer to build a document set with expected costs and expected outcomes. The set should include one document just under the budget and one just over it, so the boundary is proven rather than assumed.
Test introspection and schema queries under the same rule. A limit that exempts introspection leaves a cheap way to do expensive work.
- Alias inflation on a list field with a page size at the maximum.
- Fragment chain that repeats a costly subtree at depth.
- Query just below and just above the declared budget.
- Introspection query under the same calculator.
Enforce once, at a point you can explain
Enforcement usually belongs at the gateway, before execution, using the parsed document and the schema metadata. Resolver level checks arrive after work has started and produce partial responses that are hard to reason about.
If the gateway cannot see variable values that affect cost, such as page size, the calculator must use the declared maximum rather than the supplied value. Otherwise a client can send a small variable and a large effective query.
Rejections need a stable error code and a message that names the budget and the computed cost. That message is part of the contract, not a debugging convenience.
Compare computed cost with observed work
For each test document, record the computed cost, the decision, the response time, the resolver count, and the database query count. A calculator that rejects the right documents but misprices the accepted ones will still allow a costly query through.
Keep the raw query document hash and the calculator version beside the result. When weights change, the old result should be explainable rather than mysterious.
Report the false rejection case too. A legitimate dashboard that trips the limit is a product problem, and the client owner needs to see it before enforcement widens.
Test batching, caching, and partial failure
Batched operations can hide several expensive documents behind one transport request. Astute clients also retry after a rejection, so the limit must not create a retry loop that increases load.
Persisted queries change the calculation surface. If the gateway trusts a stored hash without rechecking cost, a previously accepted document can outlive a budget change.
Test the case where the calculator throws. A failure in cost estimation should fail closed with a clear error, not fall through to execution.
Check the header and transport layer too. A client that can request compression, batching, or a different content type may reach a path where the parsed document differs from the one the calculator measured, and that gap is worth one adversarial test.
- Batched request containing two documents near the budget.
- Retry storm after a rejection.
- Persisted document accepted before a weight change.
- Calculator exception during parsing.
Review cadence and measures with denominators
Measure rejected operations divided by all operations, costly accepted operations divided by all accepted operations, and p95 latency for accepted operations. Segment by client so one heavy consumer is visible.
Track the count of documents above the old budget that were accepted before enforcement. That number is the backlog the limit is meant to address.
Review monthly and after any schema change that adds a list field or a new resolver fan out. A cost model that is not revised with the schema decays into a formality.
Close with a calculator, a test set, and a rollout record
The handoff contains the cost document, the calculator revision, the adversarial document set, the observed decisions, the false rejection list, and the rollout stage with its owner. It also names the next schema review.
Accept the work when a reviewer can compute the cost of a new document by hand from the published model and match the gateway decision.
Record which clients are exempt and why. An exemption list that lives only in a gateway configuration file is invisible to the next reviewer.
Copy-ready brief
Put the operating agreement in writing.
One schema version and its public read operations.
Field weights, list multiplier, depth ceiling, max page size.
Adversarial documents with expected cost and rejection code.
Company owner approves each enforcement stage.
Frequently asked questions
Is a request rate limit enough for GraphQL?
No. One expensive document can do the work of many cheap requests. Cost needs its own model and budget.
Should the limit reject or truncate?
Reject with a stable error code. Silent truncation changes the meaning of the response and hides load.
Who approves a budget increase?
The company owner of the API, after seeing the client, the cost, and the capacity impact.
Sources
- OWASP Application Security Verification Standard
Input handling and resource consumption verification topics.
- NIST Secure Software Development Framework
Practices for designing and reviewing exposed interfaces.
- OpenTelemetry documentation
Instrumentation for resolver counts, latency, and rejection metrics.
- GitHub pull request reviews
Review mechanics for the calculator and schema changes.
- Google Technical Writing
Guidance for a readable cost model document.
