Rate limits
🎯You'll be able to
Choose and implement a rate limit that protects the API, return 429 with useful headers, and share counts across instances.
🏠The everyday picture
A ration shop gives each card holder their monthly quota, however many times they come back. The shopkeeper's register, not the customer's word, keeps the count.
📘What it really is
-
Rate limiting caps how many requests a caller can make in a time window. It slows down password guessing, scraping, bots and runaway scripts, and protects the database from overload.
-
Who is the caller? Prefer the signed-in identity (client + user from the token); fall back to the IP address for anonymous routes (IPs can be shared by a whole office or mobile network, so set those limits higher).
-
Algorithms:
- fixed window: count per minute (
rl:<who>:<minute>); simple, but allows a burst at the boundary; - sliding window: counts over the last 60 seconds; smoother;
-
token bucket: a bucket refills steadily; each request takes a token; allows short bursts up to the bucket size.
-
Share the count across all API instances in Redis (
INCRplusEXPIRE, in oneMULTI). Counting in each process's memory lets callers multiply their limit by the number of servers. -
Respond
429 Too Many RequestswithRetry-After, and tell callers their budget (RateLimit-Limit,RateLimit-Remaining,RateLimit-Reset). -
Decide what happens if Redis is down: usually fail open (allow requests) for ordinary routes, but fail closed (refuse) for sensitive ones like sign-in attempts.
-
Limits are one layer; also use stricter limits on expensive or sensitive routes (OTP sending, login, search).
📝A worked example
Input: test customer 3 makes 41 requests to GET /v1/me/orders within one minute; the limit is 40 (set by RATE_LIMIT=40 in the
tests).
Steps in the finished api/rate-limit.js: key rl:u:tiffin-web:cust-3:<minute number>; MULTI INCR key; EXPIRE key 65; EXEC; if the
count is over 40, reply 429.
Expected output: requests 1 to 40 get 200 with RateLimit-Remaining counting down; request 41 gets 429 with Retry-After: <seconds
to the next minute>; customer 2's request at the same moment gets 200. This is Project 1 test 12.
🌍Where it's used
- Login and OTP routes, against guessing and SMS cost attacks.
- Public APIs, with different limits per plan.
- Search and export, which are expensive per request.
📝Case scenario
An app sends a one-time password by SMS on every "Send OTP" request, with no limit. A bot calls it 200,000 times overnight for random numbers, and the SMS bill arrives in lakhs of rupees. A limit per phone number and per IP, plus a daily cap, would have stopped it after a handful of messages.
🖱️Try it
Against the starter kit, call GET /v1/me/orders?limit=1 50 times in a loop with one token (for i in $(seq 50); do curl ...; done).
Count the status codes. Then write which key, window and limit you'd use for this route, the menu route, and a future "Send OTP" route.
Success: the starter allows all 50, and your plan gives a different limit for each route with a reason.
⚠️Common mistakes
- Counting in process memory. Sign: limits that multiply with the number of servers, and reset on deploy. Fix: count in Redis.
- 429 without
Retry-After. Sign: clients retry instantly in a loop. Fix: tell them when to try again. - One limit for everything. Sign: login and OTP get the same generous budget as reading a menu. Fix: stricter limits on sensitive and costly routes.
🛠️For your project
Project 1 test 12: request 41 within a minute gets 429 with Retry-After, and another customer isn't affected.
✅Quick check
Why count requests in Redis rather than in each server's memory?
Show the answer
So all servers share one count; otherwise each server gives the full limit separately.
Should rate limiting fail open or closed when Redis is down?
Show the answer
It depends: usually open for ordinary routes, closed for sensitive ones such as login attempts.
