GraphQL Rate Limiting Strategies
Why GraphQL rate limiting is harder than REST and what to do about it. Covers query complexity analysis, depth limits, cost-based budgets, and per-field throttling.
What you'll learn
- ✓Why request counts are the wrong unit for GraphQL
- ✓How depth and complexity limits work
- ✓How to assign costs to fields
- ✓Patterns for per-user budgets
- ✓Trade-offs between strictness and flexibility
Prerequisites
- •Basic GraphQL
Rate limiting a REST endpoint is straightforward: count requests per second per user. GraphQL breaks that model because one request can do a thousand things. A more thoughtful approach is needed if you want to keep abusive queries from melting your database.
What and Why
Rate limiting protects your service from accidental load and intentional abuse. In REST, the unit of work is a request, so requests per minute is a sensible unit to limit. In GraphQL, a single request can ask for deeply nested data, fan out across relationships, and pull back megabytes of rows. Counting requests would treat a one-field query and a query that joins ten tables as equivalent.
The fix is to measure something closer to the actual work. Depth, breadth, and an estimated cost per field are the standard signals. Apply budgets against those, not against raw request counts.
Mental Model
Imagine every incoming query as a shopping cart. Each field has a price tag. Before the server runs anything, you sum the prices and check whether the customer has enough credit left in their wallet for this minute. If they do, the cart goes through and the credit is deducted. If they do not, you reject the query before touching the database.
The wallet is per-user or per-API-key and refills on a schedule. The price tags come from a static analysis of the query against your schema. The database never sees abusive queries because they fail at the gate.
Hands-on Example
Use a query complexity library on top of your GraphQL server. The example uses graphql-query-complexity with Apollo.
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';
const rule = createComplexityRule({
maximumComplexity: 1000,
estimators: [
simpleEstimator({ defaultComplexity: 1 }),
],
onComplete: (complexity) => {
console.log('Query complexity:', complexity);
},
});
const server = new ApolloServer({
schema,
validationRules: [depthLimit(7), rule],
});
Attach explicit costs to expensive fields in your schema with directives so list fields charge more than scalars and pagination multipliers count.
type Query {
posts(first: Int!): [Post!]! @cost(value: 2, multipliers: ["first"])
}
client query
|
v
[parse] -> syntax errors? -> reject
|
v
[depth check] -> too deep? -> reject
|
v
[complexity calc] -> over budget? -> reject
|
v
[wallet check] -> exhausted? -> 429
|
v
resolvers + database A persistent store like Redis holds the per-user budget so multiple server instances share state.
Common Pitfalls
The first pitfall is picking a complexity limit out of thin air. Numbers like 1000 mean nothing on their own. Measure real traffic, plot the distribution, and set the limit just above the legitimate p99.
The second is forgetting introspection. A malicious client can ask for the schema in a deeply nested way to probe your service. Apply the same depth and complexity rules to introspection, or disable it in production.
The third is double-counting. If you charge for the parent list and also for each child, the cost can explode in a way that blocks reasonable queries. Pick one consistent model for nested fields and apply it everywhere.
Practical Tips
Surface the cost back to clients in response extensions. Developers can then see why a query was rejected and shape it differently, instead of guessing.
Use persisted queries for trusted clients. The cost of every persisted query is known up front, so you can skip dynamic analysis and just enforce the wallet.
Have different budgets for different operations. A mutation that triggers a payment can be slower and more expensive than a feed query, but both deserve limits.
Wrap-up
GraphQL rate limiting is less about counting and more about budgeting. Decide what a query costs, decide what a user gets to spend, and enforce the rest at the gate. The result is an API that flexes for real use cases without bending under abuse.
Related articles
- GraphQL GraphQL Schema Design Best Practices
Practical patterns for designing GraphQL schemas that age well: nullability, pagination, mutations, errors, and evolution without versioning.
- GraphQL GraphQL vs REST Tradeoffs
An honest comparison of GraphQL and REST: where each one shines, where each one hurts, and how to choose without religion.
- GraphQL GraphQL with Prisma Tutorial
A practical guide to wiring GraphQL on top of Prisma. Schema design, resolvers, the N+1 problem, batching, and the patterns that keep your API fast as it grows.
- REST APIs REST API Throttling and Rate Limiting
Protect your API from abuse and accidental overload using token buckets, leaky buckets, and standard rate-limit headers that clients can actually respect.