Advanced Rate Limiting Patterns — Complete Implementation Guide
In this tutorial, you will learn about Advanced Rate Limiting Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
Advanced rate limiting patterns go beyond simple request counting, implementing tiered limits, cost-based accounting, adaptive throttling, and multi-dimensional strategies for complex API requirements.
What You'll Learn
By the end of this tutorial, you will implement tier-based rate limits, cost-weighted requests, adaptive limiting based on system load, and Composite limits across multiple dimensions.
Why It Matters
Simple rate limiting treats all requests equally. Advanced patterns differentiate between user tiers, request costs, and system conditions, providing more nuanced protection.
Real-World Use
DodaZIP's API charges different costs for different operations: listing files costs 1 point, converting a file costs 10 points, and batch processing costs 50 points.
Advanced Rate Limiting Learning Path
flowchart LR
A[Rate Limit Headers] --> B[Advanced Patterns]
B --> C[Tiered Limits]
B --> D[Cost-Based]
B --> E[Adaptive]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Tiered Rate Limits
Different user tiers get different rate limits. Free users get basic limits, premium users get higher limits.
const tierConfig = {
free: { limit: 10, windowMs: 60000 },
pro: { limit: 100, windowMs: 60000 },
enterprise: { limit: 1000, windowMs: 60000 }
};
function tieredLimiter(req, res, next) {
const tier = req.user?.tier || "free";
const config = tierConfig[tier];
const key = `${tier}:${req.user?.id || req.ip}`;
const count = incrementCount(key, config.windowMs);
if (count > config.limit) {
return res.status(429).json({
error: "Rate limit exceeded",
tier,
limit: config.limit
});
}
next();
}
Expected behavior: Free users get 10 req/min, pro users get 100 req/min, enterprise users get 1000 req/min.
Cost-Based Rate Limiting
Not all requests are equal. A rate limiting system that accounts for request cost allocates resources more fairly.
class CostBasedLimiter {
constructor() {
this.costs = {
"GET /api/files": 1,
"GET /api/files/:id": 1,
"POST /api/files": 5,
"POST /api/convert": 10,
"POST /api/batch-convert": 50
};
this.budgets = new Map();
}
getCost(req) {
const route = `${req.method} ${req.route?.path || req.path}`;
return this.costs[route] || 1;
}
middleware(budget, windowMs) {
return (req, res, next) => {
const key = req.user?.id || req.ip;
const cost = this.getCost(req);
const now = Date.now();
if (!this.budgets.has(key)) {
this.budgets.set(key, { balance: budget, lastRefill: now });
}
const account = this.budgets.get(key);
const elapsed = now - account.lastRefill;
const refill = Math.floor(elapsed / windowMs) * budget;
account.balance = Math.min(budget, account.balance + refill);
account.lastRefill = now;
if (account.balance < cost) {
return res.status(429).json({
error: "Insufficient budget",
cost,
balance: account.balance,
budget
});
}
account.balance -= cost;
req.budgetRemaining = account.balance;
next();
};
}
}
Adaptive Rate Limiting
Adaptive rate limiting adjusts limits based on current system load, CPU usage, or error rates.
class AdaptiveLimiter {
constructor(redis) {
this.redis = redis;
this.cpuThresholds = [
{ cpu: 0.5, multiplier: 1.0 },
{ cpu: 0.7, multiplier: 0.75 },
{ cpu: 0.85, multiplier: 0.5 },
{ cpu: 0.95, multiplier: 0.25 }
];
}
async getMultiplier() {
const cpuUsage = await this.getCpuUsage();
for (const threshold of this.cpuThresholds) {
if (cpuUsage < threshold.cpu) {
return threshold.multiplier;
}
}
return 0.1;
}
async middleware(baseLimit) {
const multiplier = await this.getMultiplier();
const effectiveLimit = Math.max(1, Math.floor(baseLimit * multiplier));
return (req, res, next) => {
// Apply rate limiting with effectiveLimit
next();
};
}
getCpuUsage() {
const cpus = require("os").cpus();
let totalIdle = 0;
let totalTick = 0;
for (const cpu of cpus) {
for (const type in cpu.times) {
totalTick += cpu.times[type];
}
totalIdle += cpu.times.idle;
}
return 1 - totalIdle / totalTick;
}
}
Multi-Dimensional Rate Limiting
Combine multiple dimensions (IP, user ID, endpoint, HTTP method) into composite rate limits.
class MultiDimensionalLimiter {
constructor() {
this.dimensions = {
"global": { key: () => "global", limit: 10000, windowMs: 60000 },
"per-ip": { key: (req) => `ip:${req.ip}`, limit: 100, windowMs: 60000 },
"per-user": { key: (req) => `user:${req.user?.id}`, limit: 500, windowMs: 60000 },
"per-endpoint": {
key: (req) => `ep:${req.method}:${req.path}`,
limit: 50,
windowMs: 60000
}
};
}
middleware() {
return (req, res, next) => {
for (const [name, dim] of Object.entries(this.dimensions)) {
const key = dim.key(req);
if (!key) continue;
const count = this.getCount(key, dim.windowMs);
if (count > dim.limit) {
return res.status(429).json({
error: "Rate limit exceeded",
dimension: name,
limit: dim.limit
});
}
}
next();
};
}
}
Common Mistakes
Not normalizing costs across different endpoints -- If listing files costs 1 and converting costs 100, document the cost model clearly.
Adaptive limiting causing feedback loops -- If limits tighten under load, clients retry aggressively, worsening the load. Add backoff.
Too many dimensions creating complexity -- Each dimension adds Redis calls. Cache dimension checks and keep them minimal.
Not testing tier limits thoroughly -- Edge cases at tier boundaries (upgrading mid-window, expiring trials) need careful handling.
Forgetting to clean up per-user budgets -- Cost-based budgets accumulate. Reset budgets periodically.
Practice Questions
Why use cost-based rate limiting instead of simple request counting? Different operations have different resource costs. Cost-based limiting allocates resources proportionally.
How does adaptive rate limiting respond to high CPU? It reduces rate limits, giving the server breathing room to recover from load spikes.
What is the challenge with multi-dimensional rate limiting? Each dimension requires a separate counter, multiplying storage and latency overhead.
Challenge: Implement a rate limiter that gives loyal users higher limits based on account age.
function loyaltyLimiter(req, res, next) {
const accountAge = Date.now() - new Date(req.user?.createdAt || Date.now());
const bonus = Math.floor(accountAge / (30 * 24 * 60 * 60 * 1000)) * 10;
const baseLimit = 100;
const limit = baseLimit + Math.min(bonus, 500);
}
FAQ
Mini Project
Build an advanced rate limiter with tiered limits, cost-based accounting, and multi-dimensional enforcement.
class AdvancedRateLimiter {
constructor(redis) {
this.redis = redis;
}
async check(req, user) {
const checks = [
{ key: "global", limit: 10000, windowMs: 60000 },
{ key: `ip:${req.ip}`, limit: 100, windowMs: 60000 },
];
if (user) {
const tier = user.tier || "free";
const tierLimits = { free: 10, pro: 100, enterprise: 1000 };
checks.push({
key: `user:${user.id}`,
limit: tierLimits[tier],
windowMs: 60000
});
}
const costs = { read: 1, write: 5, convert: 10 };
const cost = costs[req.resourceType] || 1;
for (const check of checks) {
const windowKey = `${check.key}:${Math.floor(Date.now() / check.windowMs)}`;
const used = await this.redis.incrby(windowKey, cost);
if (used === cost) await this.redis.expire(windowKey, 120);
if (used > check.limit) {
return { allowed: false, dimension: check.key, limit: check.limit };
}
}
return { allowed: true };
}
}
What's Next
Now that you understand advanced rate limiting, explore testing rate limiting implementations. Then learn about rate limiting performance optimization.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro