Skip to content

Limiting expensive queries

Query-complexity limits, demand control, cost-based rate limiting, DoS protection: every guard against expensive GraphQL queries needs the same thing, a cost per query to threshold on. Today you hand-author that cost with @cost directives or per-field weights and hope the numbers are right. costQL measures it instead.

The mechanism here is mature and well-trodden: score the query statically before it runs, bound the pagination, reject anything over budget before a resolver fires. graphql-query-complexity, graphql-cost-analysis, GraphQL Armor, Apollo Router demand control, HotChocolate’s MaxFieldCost — they agree on all of that. costQL agrees too. They also all leave one thing to you: the cost of each field, declared by hand. That declaration is a guess, and it is the only part costQL replaces — with a number timed from your real API.

Write the gate; costQL supplies the number

Section titled “Write the gate; costQL supplies the number”

costQL prices; it does not block. You keep the gate, a one-liner, and feed it a measured price:

from costql import PricingPack
pack = PricingPack.load("packs/your_pack.json")
def admit(query, variables=None, budget=250):
quote = pack.quote(query, variables)
if quote["price"] > budget: # safe max: never below the real cost
raise TooExpensive(quote["price"], budget)
return quote

price is the safe ceiling — guaranteed never below what the query actually costs — so a query that clears the budget really is within it. Bill on that number or block on it; it is the same measured number either way. The same call works in JavaScript and from the CLI, so the check can live in a gateway, a resolver, a CI step, or an agent’s planning loop.

Why measured beats hand-authored, right here

Section titled “Why measured beats hand-authored, right here”

Two query shapes are where hand-authored weights fail, and they are the two a limit exists to catch:

  • Batching over-blocks the good resolvers. A field behind a DataLoader resolves a list of 100 in one round-trip, not 100. Per-field-times-count math scores it at 100× and rejects exactly the well-built resolver you want people to use. costQL prices shared work once; on our share-heaviest test that cut error from 315% to 12% (see the Northwind case study).
  • Recursion slips under-blocked. A cyclic query is the classic GraphQL DoS vector, and a static hand-authored weight never sees it coming — the usual fix is a penalty bolted on after an incident. costQL detects cycles up front, flags them confidence: low, and prices them at a structural ceiling rather than a too-low guess (auto-flagged unprompted on the Rick & Morty API).

A number tuned to what the server actually does is the difference between a limit that holds and one that punishes your best resolvers while waving the dangerous ones through.

Your first pack needs nothing from the server

Section titled “Your first pack needs nothing from the server”

You do not have to instrument anything to start. T1 prices any GraphQL endpoint black-box, from the outside, and still returns a safe ceiling — enough to threshold on today. If your API batches or caches heavily and you want the limit to stop over-blocking those resolvers, instrument for T2/T3; until then T1 errs high, which is the safe direction for a guard. See tier fidelity for what each tier buys you.

costQL gives you a measured estimate, not a guarantee. Whether the prices fit your business is yours to verify. Provided as-is under Apache-2.0, with no warranty.