Skip to content

GraphQL Rate Limiting — Protect Your API from Abuse and Overuse

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about GraphQL Rate Limiting. We cover key concepts, practical examples, and best practices to help you master this topic.

GraphQL rate limiting protects your API from abusive queries and excessive resource consumption by restricting the number, depth, or complexity of operations a client can perform within a time window.

What You'll Learn

  • Why REST rate limiting doesn't work for GraphQL
  • Query complexity analysis and depth limiting
  • Per-user and per-IP rate limiting
  • Implementing rate limits in Apollo Server
  • Using Sliding Window and token bucket algorithms

Why It Matters

Unlike REST where each endpoint has a predictable cost, a single GraphQL query can request hundreds of fields and deeply nested relations. Without rate limiting, a malicious or buggy client could trigger an expensive query that degrades the entire API. DodaTech's Durga Antivirus Pro uses cost-based rate limiting, assigning weights to fields and rejecting queries that exceed a per-user daily budget.

Real-World Use

A mobile app queries device status every 30 seconds. A bug causes it to send the same query every second. Rate limiting catches the excessive traffic and returns 429 Too Many Requests, preventing the database from being overloaded while the bug is fixed.

flowchart TB
    A["Client Query"] --> B["Rate Limiter"]
    B --> C{Check Budget}
    C -->|Within Limit| D["Execute Query"]
    C -->|Exceeded| E["Return 429 Error"]
    D --> F["Decrement Budget"]
    subgraph Budget Store
        G["Redis\nUser: quota_remaining\nWindow: 1 hour"]
    end
    B <--> G
    style B fill:#fef3c7,stroke:#d97706
    style E fill:#fecaca,stroke:#dc2626

Code Examples

Example 1: Query Depth Limiting

const depthLimit = require('graphql-depth-limit');
const { ApolloServer } = require('apollo-server');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)],
});

// Query rejected: depth exceeds 5
// query { devices { user { devices { user { devices { name } } } } } }

Example 2: Cost-Based Rate Limiting

const { ApolloServer } = require('apollo-server');
const { createComplexityLimitRule } = require('graphql-validation-complexity');

const complexityRule = createComplexityLimitRule(1000, {
  onCost: cost => console.log(`Query cost: ${cost}`),
  formatErrorMessage: cost =>
    `Query too complex (cost ${cost}/1000). Simplify the query.`,
});

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [complexityRule],
});

Example 3: Per-User Sliding Window Rate Limiter

const redis = require('redis');
const client = redis.createClient();

async function checkRateLimit(userId, maxQueries, windowSec) {
  const key = `rate:${userId}`;
  const now = Date.now();
  const windowStart = now - windowSec * 1000;
  
  await client.zRemRangeByScore(key, 0, windowStart);
  const count = await client.zCard(key);
  
  if (count >= maxQueries) {
    const ttl = await client.zRangeByScore(key, 0, now, { LIMIT: [0, 1] });
    throw new Error('Rate limit exceeded');
  }
  
  await client.zAdd(key, { score: now, value: `${now}` });
  await client.expire(key, windowSec);
}

const resolvers = {
  Query: {
    devices: async (_, args, context) => {
      await checkRateLimit(context.user.id, 100, 60);
      return context.db.devices.find().toArray();
    },
  },
};

Common Mistakes

  1. Applying REST-style IP-based rate limiting — a single IP behind NAT represents many users. Rate limit by user ID or API key instead.
  2. Only limiting by query count — a single query requesting 10,000 records costs more than 10 queries requesting 10 each. Use cost analysis.
  3. Ignoring subscription rate limits — subscriptions can send messages continuously. Limit the subscription rate and disconnect abusive clients.
  4. Not communicating limits to clients — return proper 429 status with Retry-After headers and remaining quota in response metadata.
  5. Using fixed Windows instead of sliding windows — fixed windows (reset at midnight) can cause traffic spikes at the boundary.

Practice Questions

  1. Why is query depth limiting insufficient for protection?
  2. How does cost-based analysis assign weights to fields?
  3. What is the difference between per-IP and per-user rate limiting?
  4. How do sliding windows differ from fixed windows?
  5. How should rate-limited clients handle 429 responses?

Challenge: Build a rate limiter plugin for Apollo Server that uses Redis to track per-user query complexity costs across a sliding 1-hour window, with different limits for authenticated vs anonymous users.

Mini Project

Create a GraphQL API protected by a three-layer rate limiter: query depth (max 7), query cost (max 500 points), and per-user query count (1000/hour). Include proper error responses with Retry-After headers and remaining budget metadata.

FAQ

Can I reuse REST API rate limiting for GraphQL?

Not directly. REST rate limiting by endpoint doesn't work for GraphQL's single endpoint. You need query-specific analysis to measure actual cost.

What is a good query complexity limit?

Start at 500-1000 points. Assign base costs: 1 for scalar fields, 5 for list fields, 10 for objects. Monitor real usage and adjust.

How do I rate limit GraphQL subscriptions?

Track messages per connection per second. Disconnect clients that exceed the limit. Use backpressure to slow down publishers.

Should I rate limit mutations differently?

Yes. Mutations modify data and often execute sequentially. Apply stricter limits to mutations — typically 10-20 per minute per user.

What headers should I return for rate limits?

Return X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers on 429 responses.

What's Next

Learn about query cost analysis and field weighting

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro