Rate Limiting Headers — Complete Implementation Guide
In this tutorial, you will learn about Rate Limiting Headers. We cover key concepts, practical examples, and best practices to help you master this topic.
Rate limiting HTTP headers communicate rate limit status to clients, telling them how many requests remain, when limits reset, and how long to wait after exceeding the limit.
What You'll Learn
By the end of this tutorial, you will implement standard rate limit headers, understand the two common header conventions, and help clients self-regulate their request rates.
Why It Matters
Clients cannot respect rate limits they cannot see. Proper headers reduce 429 errors by helping clients pace their requests intelligently.
Real-World Use
DodaTech's APIs return rate limit headers on every response, enabling client SDKs to automatically throttle requests and display usage information to developers.
Rate Limiting Headers Learning Path
flowchart LR
A[Distributed Limiting] --> B[Rate Limit Headers]
B --> C[X-RateLimit vs Standard]
B --> D[Retry-After]
B --> E{You Are Here}
style E fill:#f90,color:#fff
Standard Rate Limit Headers
The IETF standard (draft) defines three headers that communicate rate limit state to clients.
const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
const limiter = rateLimit({
windowMs: 60000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many requests" }
});
app.use(limiter);
app.get("/", (req, res) => {
res.json({ data: "success" });
});
app.listen(3000);
Expected response headers:
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 1722189060
Legacy Headers Pattern
Many APIs use the X-RateLimit-* prefix convention popularized by GitHub and Twitter APIs.
function legacyHeaders(req, res, next) {
const limit = 100;
const remaining = getRemaining(req.ip);
const reset = Math.floor(Date.now() / 1000) + 60;
res.setHeader("X-RateLimit-Limit", limit);
res.setHeader("X-RateLimit-Remaining", Math.max(0, remaining));
res.setHeader("X-RateLimit-Reset", reset);
next();
}
Expected response headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1722189060
Retry-After Header
When a client exceeds the rate limit, the Retry-After header tells them how long to wait before retrying. It is critical for client-side backoff.
async function rateLimitWithRetryAfter(req, res, next) {
const key = req.ip;
const windowMs = 60000;
const max = 100;
const count = await getCount(key);
res.setHeader("RateLimit-Limit", max);
res.setHeader("RateLimit-Remaining", Math.max(0, max - count));
if (count > max) {
const oldestTimestamp = await getOldest(key);
const retryAfter = Math.ceil((oldestTimestamp + windowMs - Date.now()) / 1000);
res.setHeader("Retry-After", retryAfter);
res.setHeader("RateLimit-Reset", Math.ceil((oldestTimestamp + windowMs) / 1000));
return res.status(429).json({
error: "Rate limit exceeded",
retryAfter
});
}
next();
}
Expected 429 response headers:
RateLimit-Limit: 100
RateLimit-Remaining: 0
Retry-After: 45
RateLimit-Reset: 1722189105
Content-Type: application/json
{"error": "Rate limit exceeded", "retryAfter": 45}
Dynamic Header Values
Limits can vary per user tier or endpoint. Headers should reflect the actual limit for the current request context.
function dynamicHeaders(req, res, next) {
const tier = req.user?.tier || "free";
const limits = {
free: { limit: 10, window: 60000 },
pro: { limit: 100, window: 60000 },
enterprise: { limit: 10000, window: 60000 }
};
const config = limits[tier];
const count = getCount(req.user?.id || req.ip);
res.setHeader("RateLimit-Limit", config.limit);
res.setHeader("RateLimit-Remaining", Math.max(0, config.limit - count));
res.setHeader("RateLimit-Policy", `${config.limit}/min`);
res.setHeader("X-RateLimit-Tier", tier);
next();
}
Expected headers for a pro user:
RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Policy: 100/min
X-RateLimit-Tier: pro
Common Mistakes
Not setting headers on 429 responses -- Clients need rate limit info even on error responses to know when to retry.
Using Unix timestamps in milliseconds -- Most clients expect seconds. Use
Math.floor(Date.now() / 1000)for Unix timestamps.Inconsistent header naming -- Mixing
X-RateLimit-LimitandRateLimit-Limitconfuses clients. Pick one convention.Not documenting header semantics -- Clients need to know what each header means. Document your rate limit header convention.
Setting Reset to current window end instead of oldest request expiry -- Clients need to know when a slot opens, not when the window ends.
Practice Questions
What is the difference between RateLimit-Reset and Retry-After? RateLimit-Reset indicates when the rate limit window resets. Retry-After indicates how many seconds to wait.
Why should errors also include rate limit headers? Clients may read error responses programmatically and need header data to implement backoff logic.
What header tells a client how many requests they have left? RateLimit-Remaining (or X-RateLimit-Remaining for legacy headers).
Challenge: Implement a middleware that parses rate limit headers and adjusts client request rate.
// Client-side: parse RateLimit-Remaining and slow down when low
function clientThrottle(headers) {
const remaining = parseInt(headers["ratelimit-remaining"]);
if (remaining < 10) {
return Math.max(100, headers["ratelimit-reset"] * 1000 - Date.now());
}
return 0;
}
FAQ
Mini Project
Build a middleware that sets comprehensive rate limit headers, supports both standard and legacy formats, and provides accurate Retry-After timing.
const express = require("express");
const app = express();
const store = new Map();
function rateLimitHeaders(opts = {}) {
const max = opts.max || 100;
const windowMs = opts.windowMs || 60000;
return (req, res, next) => {
const key = opts.keyGenerator ? opts.keyGenerator(req) : req.ip;
const now = Date.now();
if (!store.has(key)) store.set(key, []);
const timestamps = store.get(key).filter(t => now - t < windowMs);
const remaining = Math.max(0, max - timestamps.length);
res.setHeader("RateLimit-Limit", max);
res.setHeader("RateLimit-Remaining", remaining);
res.setHeader("X-RateLimit-Limit", max);
res.setHeader("X-RateLimit-Remaining", remaining);
if (remaining === 0) {
const oldest = timestamps[0];
const reset = Math.ceil((oldest + windowMs) / 1000);
const retryAfter = Math.ceil((oldest + windowMs - now) / 1000);
res.setHeader("RateLimit-Reset", reset);
res.setHeader("Retry-After", retryAfter);
return res.status(429).json({ error: "Rate limit exceeded" });
}
timestamps.push(now);
store.set(key, timestamps);
next();
};
}
app.use(rateLimitHeaders({ max: 60 }));
app.get("/", (req, res) => res.json({ ok: true }));
app.listen(3000);
What's Next
Now that you understand rate limit headers, explore advanced rate limiting patterns and strategies. Then learn about testing rate limiting implementations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro