Rate Limiting Your REST API with HTTP 429
Rate limiting protects both sides of an API. Token buckets, the standard 429 headers, and what well-behaved clients should do with Retry-After.
Netbay Infrastructure Team
Netbay Engineering
On this page
Rate limiting is usually described as server protection, but it is better understood as a contract: the server promises a fair share and the client promises to respect the signals. Done well, limits protect the database, keep noisy neighbors from starving everyone else, and give well-behaved clients a clean vocabulary for waiting. Done badly, they are arbitrary 429s with no headers and clients that hammer anyway out of confusion. This post covers the model, the headers, and the client behavior.
The token bucket model
The token bucket is the mental model that survives contact with reality. Imagine a bucket that holds, say, fifty tokens. Each request spends one. The bucket refills at a steady rate — twenty tokens per second — so sustained traffic is throttled to the refill rate while short bursts can spend the accumulated surplus. A client that keeps a mostly full bucket gets fast, bursty work; a client that floods hits the refill ceiling. Compared to a naive fixed window — which lets traffic surge to the limit at every boundary — the token bucket smooths the average and still tolerates bursts.
The wire contract
Rate limit state belongs in headers the client can read without parsing your body. On successful requests, send the classic trio:
- X-RateLimit-Limit — the maximum requests the client is allowed per window.
- X-RateLimit-Remaining — how many are left right now.
- X-RateLimit-Reset — seconds, or a timestamp, until the window resets.
When the client is over, respond 429 Too Many Requests and include Retry-After with the number of seconds to wait. Every client that respects Retry-After turns your throttling into scheduling instead of failure, which is exactly the outcome you want. A 429 is retryable — it means "later", not "never" — and unlike a 5xx it does not say anything is broken, so a client that sees 429 should back off, not panic.
# nginx: per-IP rate limit with a burst allowance
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
server {
location /api/ {
limit_req zone=api burst=40 nodelay;
limit_req_status 429;
add_header X-RateLimit-Limit 20 always;
add_header X-RateLimit-Remaining $limit_req_status always;
}
}HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 30
{"error":{"code":"rate_limited","message":"Slow down; retry after 30s.","details":[]}}Client behavior that keeps you on the good side
The client side of the contract matters as much as the headers:
- Read Retry-After and wait that long. A client that sleeps the exact value spreads its own load evenly.
- Use exponential backoff for 5xx retries, stepping 1s, 2s, 4s, capped — and add jitter so a fleet of clients does not retry in lockstep.
- Treat 429 and 5xx as retryable; treat every other 4xx as permanent and do not retry it at all.
- Shed or queue work rather than failing it: a batch client can hold jobs in a queue and drain them as the quota refills.
async function callWithBackoff(url, headers, { retries = 4, base = 1000 } = {}) {
for (let attempt = 1; attempt <= retries; attempt++) {
const res = await fetch(url, { headers });
if (res.status !== 429 && res.status < 500) return res; // permanent, stop
const retryAfter = Number(res.headers.get("Retry-After"));
const wait = retryAfter || (base * 2 ** (attempt - 1));
await new Promise(r => setTimeout(r, wait));
}
throw new Error("still rate limited after " + retries + " attempts");
}Takeaway
Rate limiting only works if the signals are readable. Advertise the quota on every response, answer excess with 429 plus Retry-After, and teach your own clients to treat that as a schedule, not a failure. Then throttling protects your database without punishing the people doing it right.
A VPS control plane like the one at netbayhosts.in is exactly where sane limits pay off: predictably paced provisioning calls keep everyone's deployments fast instead of letting the loudest script win.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance