GraphQL Security
Securing GraphQL APIs Against Expensive Queries and Data Exposure
A defense-in-depth approach to GraphQL authorization, query cost controls, pagination, batching, trusted operations, error handling, and production observability.
GraphQL gives clients precise control over response shape, but that flexibility also lets one HTTP request trigger deep traversal, large lists, repeated aliases, resolver fan-out, or access to fields the caller should not see. Network request counts alone no longer describe workload cost.
A secure GraphQL service treats every operation as a workload plan. It authenticates the caller, validates and prices the document before execution, authorizes each business object, bounds data access, and observes the resulting resolver and database behavior.
2. Reject unreasonable operations before execution
Set limits for document size, parsing tokens, nesting depth, aliases, root fields, list sizes, and batched operations. Depth alone is insufficient: a shallow query with several large list fields can cost more than a deeper query returning one object per level.
Use complexity or cost analysis that reflects expected resolver work. Multiply list fields by bounded pagination arguments, assign higher weights to expensive computed fields, and reject operations above a caller-specific budget before resolvers begin.
3. Make pagination and batching bounded
Require pagination for collections and cap every page size on the server, even when the client omits or exaggerates an argument. Avoid returning unbounded nested collections. Apply total-result and execution-time controls where one operation can walk several connections.
DataLoader-style batching reduces duplicate downstream calls, but request batching can also hide brute-force attempts or multiply work behind one network request. Limit the number of operations and object lookups per request, especially for login, recovery, invitation, and identifier-discovery workflows.
- Enforce server-side maximum page sizes
- Batch and cache only within the correct user or tenant scope
- Limit aliases and operations in a batch
- Use timeouts and cancellation through resolvers and data access
- Protect downstream services with concurrency limits
4. Reduce schema and error exposure
Decide deliberately whether production introspection is public, authenticated, restricted, or replaced by trusted documents. Disabling introspection alone is not an authorization control because an attacker can still discover behavior through errors and known operations.
Return stable public error codes without stack traces, SQL details, internal hostnames, or authorization diagnostics. Log detailed failures securely with a correlation identifier. Review schema descriptions, deprecations, default values, and naming for information that should not be public.
5. Observe cost by operation and identity
Record operation name or trusted-document identifier, calculated cost, depth, selected fields, duration, resolver count, database queries, rows, cache behavior, errors, and caller identity or tenant. Avoid logging sensitive variables without redaction.
Rate-limit by user, tenant, API client, and workload cost rather than IP alone. Alert on repeated rejected operations, abnormal alias or batch counts, authorization denials, high resolver fan-out, and query-cost distributions that shift after schema changes.
Key takeaways
What to carry into your next decision
- Authorize every object and action in services resolvers cannot bypass.
- Combine depth, breadth, list, alias, batch, timeout, and cost controls.
- Bound pagination and make batching tenant-aware and abuse-resistant.
- Measure operation cost through resolvers and downstream data access.
Sources and further reading
Version-specific and platform guidance was checked against these primary sources.