How it works
The attacker selects costly filters, deep pagination, bulk operations, deeply nested queries or workflows that trigger downstream calls. Nothing is malformed; the requests are simply the most expensive ones the interface permits.
Request count is the wrong unit
Rate limiting almost universally counts requests, which assumes requests are comparable. Within any real API they are not, often by three or four orders of magnitude.
One request fetches a record by primary key. Another runs a full-text search across several joined tables with sorting and no index on the sort column. A third exports a report, and a fourth triggers an email, an SMS and two third-party API calls. A limit of a hundred requests per minute is invisible to the first and ruinous if applied identically to the fourth.
Attackers do not need to find a vulnerability to exploit this. They read the documentation, identify the most expensive operation the interface offers, and request it within whatever limit exists. The traffic is authorised, well formed and entirely legitimate by every check the application performs.
The correction is to limit on the resource actually being consumed — database time, downstream calls, generated messages, spend — rather than on the count of requests that consumed it.
Query languages that let clients choose the cost
GraphQL and similar interfaces are valuable precisely because a client can express exactly what it needs in one request. That same property hands cost control to the client.
A nested query can expand into an enormous number of resolver invocations — a list of records, each with a related collection, each of those with another, multiplying at every level. Aliasing lets the same expensive field be requested many times within one document. Introspection helpfully describes the entire schema, so an attacker does not have to guess which shapes are costly.
Depth limits alone are insufficient because breadth multiplies too. What works is a complexity budget: assign a cost to each field, weight list fields by the number of items they may return, compute the total before execution, and reject anything over budget. For first-party clients, persisted queries remove the problem outright by permitting only a known set of documents.
Resolver-level batching matters as much for availability as for performance, since a naive resolver issuing one query per item turns a modest response into thousands of database round trips.
When the expense is somebody else's
A distinct and frequently missed variant targets operations that cause the application to spend money or consume a third-party quota.
Authentication endpoints are the common example. A login attempt may trigger password hashing that is deliberately slow, a risk-scoring call to an external provider, and an SMS if a factor is required. All three are intentional security investments, and all three are costs an attacker can trigger at will with invalid credentials. Password reset, signup and verification flows share the property.
The consequences extend past availability. Messaging costs accumulate in real money, third-party quotas exhaust and take unrelated functionality down with them, and a provider may rate-limit the account entirely — at which point legitimate users cannot authenticate because the quota was spent on an attack.
Controls have to sit in front of the expensive step: challenge or throttle before dispatching a message, cap outbound volume per identity and in aggregate, queue rather than send synchronously, and treat a third-party quota as a resource requiring its own budget.
High-cardinality lookups
A related pattern defeats caching by ensuring nothing can ever be reused. Random-subdomain attacks against DNS are the clearest example: queries for names that do not exist cannot be answered from cache, so every one is forwarded to the authoritative servers, which must generate a negative answer for each.
The same shape appears wherever a lookup is keyed by client-supplied input — a status endpoint queried with random identifiers, a game server queried for arbitrary match codes, a product API asked for keys that do not exist. Cache hit ratio collapses and every request reaches the expensive backend.
Negative caching is the specific countermeasure that is most often missing: remembering that something does not exist is as valuable as remembering that it does, and it is frequently disabled or set to a very short lifetime by default.
Budgets and failure behaviour
Two design decisions determine whether this category causes degradation or an outage.
The first is per-identity budgets denominated in cost. Track what an identity has consumed — query time, downstream calls, messages, spend — and refuse further expensive work when the budget is exhausted, while still permitting cheap requests. This degrades one abusive client rather than the whole service.
The second is what happens when a dependency saturates. Without a circuit breaker, requests queue against a slow database until connection pools are exhausted, and the failure propagates to every route sharing that pool including ones that were never targeted. A breaker that fails fast keeps the blast radius on the affected dependency and leaves the rest of the application answering.
Both are ordinary reliability engineering. That is worth noting: a service built to degrade gracefully under legitimate load is substantially harder to take down deliberately, and the same work serves both purposes.
Also known as
What defenders may observe
- Cost per request rises while request rate stays flat
- Database or dependency connection pools saturate
- A narrow set of parameters or query shapes dominates traffic
- Query depth or resolver counts spike
- Authentication latency and outbound message volume rise together
- Unique high-cardinality names surge against DNS or lookup services
Prevention and mitigation
- Set per-identity cost budgets rather than uniform request limits
- Bound pagination, filter combinations and result set sizes
- Enforce depth, breadth and complexity budgets on nested query languages, or use persisted queries
- Cap and queue downstream work such as messages and third-party calls
- Apply circuit breakers so a saturated dependency fails fast instead of consuming pools
- Require authentication before expensive operations wherever the product allows
Initial response priorities
- Activate application-aware edge controls and origin shielding.
- Identify the expensive route, query or protocol behavior.
- Challenge, rate-limit or shed abusive work while preserving normal users.
Use your approved incident-response plan and adapt decisions to your environment. This is educational guidance, not a substitute for live incident leadership.