One GraphQL request pins the database
“Request rate is flat, but database CPU hits 100% and p99 explodes. The logs show a single POST /graphql with friends { friends { friends ... } } nested ten levels deep, or the same field aliased two hundred times.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
COST_ESTIMATED_TOO_EXPENSIVEApollo Router demand control · libraryThe router's static cost estimate for the operation exceeded the configured maximum, so it was rejected before any subgraph was called.
Log the operation name with its estimated and actual cost. Run demand control in measure mode first so the limit is set from real traffic, not a guess.
Official reference for COST_ESTIMATED_TOO_EXPENSIVE (opens in new tab)Code definition reviewed429HTTP status codes · RFC 6585 · standardThe client sent too many requests in a given amount of time (rate limiting). The response may include Retry-After.
Read Retry-After and any provider rate-limit headers before retrying. Check whether the limit is per key, per account, or per endpoint, and how many workers share it.
Official reference for 429 (opens in new tab)Code definition reviewed504HTTP status codes · RFC 9110 · standardA gateway or proxy did not receive a timely response from the upstream server it needed to complete the request.
Compare the proxy's timeout with the upstream's real latency. The upstream may still finish the work after the client saw 504, so only retry idempotent requests.
Official reference for 504 (opens in new tab)Code definition reviewed
THE PRINCIPLE
Price the operation before you run it. Reject or throttle on estimated cost, not on the number of HTTP requests.
FIRST MOVES
- Find the operation: log operation name, a document hash, depth, alias count and resolver time per request; one document usually explains the spike.
- Add validation rules for maximum depth (with a lower limit for nested lists), aliases, root fields and batch size. These checks run before any resolver does.
- Require pagination on every list field and clamp first/limit on the server. Static analysis can't see runtime list sizes.
- Assign field costs, reject operations above a maximum, and rate-limit each client by total cost per window. Run in measure mode first so the limits come from real traffic.
- For first-party clients, only execute trusted (persisted) documents registered at build time. Keep server and resolver timeouts as a backstop.
TOOLS: THEN → NOW
PATTERN SNAPSHOT
import { ApolloServer } from "@apollo/server";
import { ApolloArmor } from "@escape.tech/graphql-armor";
// Validation does not know runtime list sizes, so clamp them in resolvers.
const resolvers = { Query: {
posts: (_: unknown, { first = 20 }: { first?: number }) =>
db.posts.list({ limit: Math.min(first, 100) }),
} };
const armor = new ApolloArmor({
maxDepth: { n: 6 },
maxAliases: { n: 15 },
costLimit: { maxCost: 5000 },
});
const server = new ApolloServer({ typeDefs, resolvers, ...armor.protect() });CLOSE THE AI. EXPLAIN THIS.
Why can't per-IP request rate limiting protect a GraphQL endpoint, and what should the limiter count instead?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
Guide reviewed