GraphQL Cost Analysis — Query Complexity and Field Weighting for API Protection
In this tutorial, you will learn about Graphql Cost Analysis. We cover key concepts, practical examples, and best practices to help you master this topic.
GraphQL cost analysis assigns computational weights to each field in your schema and calculates the total cost of a query before execution, rejecting queries that exceed a configured budget to protect server resources.
What You'll Learn
- Why query cost varies between GraphQL and REST
- Assigning static and dynamic field weights
- Implementing cost analysis with graphql-query-complexity
- Handling list fields with pagination multipliers
- Integrating cost limits with Apollo Server
Why It Matters
A deeply nested GraphQL query can trigger hundreds of database calls and return megabytes of data. Cost analysis catches these queries before they execute, preventing cascading failures. DodaTech's Durga Antivirus Pro uses cost analysis to budget 1000 points per query — fetching 50 devices with their latest scan results costs 120 points, but requesting all 10 million devices with nested user data costs over 5 million points and gets rejected.
Real-World Use
A developer accidentally writes a recursive query: devices { user { devices { user { devices } } } }. Cost analysis detects the complexity grows exponentially and rejects the query with a clear error message, preventing a database meltdown.
flowchart TB
A["Incoming Query"] --> B["Parse AST"]
B --> C["Walk All Selections"]
C --> D["Lookup Field Weight"]
D --> E["Apply Multipliers\n(list size, depth)"]
E --> F["Sum Total Cost"]
F --> G{Exceeds Budget?}
G -->|No| H["Execute Query"]
G -->|Yes| I["Reject with Error"]
style G fill:#fef3c7,stroke:#d97706
style I fill:#fecaca,stroke:#dc2626
Code Examples
Example 1: Static Field Weights with graphql-query-complexity
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityRule = createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 5,
listFactor: 10,
formatErrorMessage: cost =>
`Query complexity ${cost} exceeds maximum of 1000`,
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityRule],
});
Example 2: Custom Field-Level Weights
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const complexityRule = createComplexityLimitRule(1000, {
onCost: cost => console.log(`Query cost: ${cost}`),
estimators: [
// Static field weights based on database query cost
{
type: 'field',
fieldConfigs: {
'Query.devices': { complexity: 10 },
'Query.threats': { complexity: 15 },
'Device.scanResults': { complexity: 20 },
'User.paymentInfo': { complexity: 50 },
'ThreatReport.rawLog': { complexity: 30 },
},
},
],
});
Example 3: Dynamic Cost with List Size Estimation
function createDynamicCostEstimator() {
return (args) => {
const { parentType, fieldName, childComplexity, args: fieldArgs } = args;
let baseCost = 1;
// Database-backed fields cost more
if (fieldName === 'devices' || fieldName === 'threats') {
baseCost = 10;
}
// List fields multiply by expected size
if (fieldArgs.first) {
return baseCost + (childComplexity * fieldArgs.first);
}
if (fieldArgs.limit) {
return baseCost + (childComplexity * fieldArgs.limit);
}
// Default list size assumption
if (parentType.name === 'Query' && childComplexity > 0) {
return baseCost + (childComplexity * 20);
}
return baseCost + childComplexity;
};
}
const server = new ApolloServer({
validationRules: [
createComplexityLimitRule(1000, {
estimators: [createDynamicCostEstimator()],
}),
],
});
Common Mistakes
- Using only query depth limiting — two queries at the same depth can have wildly different costs if one requests 1000 items and the other requests 10.
- Forgetting pagination multipliers — a field that returns a list must account for the page size, not just the base field cost.
- Applying the same weight to all fields — a
usernamefield costs almost nothing while ascanReportfield may require a JOIN across three tables. - Not Caching complexity calculations — computing cost for every request is wasteful. Cache validated queries by their hash.
- Ignoring mutation costs — mutations that write to multiple tables or trigger Background Jobs should have higher weights.
Practice Questions
- How does cost analysis differ from depth limiting?
- Why should list fields have dynamic multipliers?
- How do you assign weights to fields with different database costs?
- What happens when a query complexity exceeds the limit?
- How do you handle complexity for union and interface types?
Challenge: Design a cost analysis system for a GraphQL schema with 50+ types where User.devices costs 5 + 2 per device, Device.scanHistory costs 10 + 1 per scan, and the total budget per user per hour is 10,000 points.
Mini Project
Build a cost analysis middleware that logs every query's complexity score, stores daily usage per API key, and automatically increases complexity limits for premium tier users. Include a GraphQL introspection endpoint that returns field weights.
FAQ
What's Next
Learn about rate limiting for GraphQL APIs
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro