Redis goes down, API goes down
Your rate limiter keeps its counters in Redis. Redis goes down for a minute. The database is fine, the servers are fine. What happens to your API?
In 30 seconds
- The problem
- Redis goes down and every endpoint returns 500, including ones that only read from a healthy database.
- Why it happens
- Every request asks the limiter first, and the limiter awaits Redis with no plan for Redis being gone. The error bubbles up as a 500.
- The fix
- An explicit failure policy per endpoint: cheap reads fall back to a stricter per-server limit, costly actions return 503. Plus a short timeout on the Redis call.
- In an interview
- A rate limiter must never be the reason the API is down. Decide per endpoint what happens when it can't answer: cheap reads fail open to a local limit, costly actions fail closed with a 503, and the limiter call times out fast.
Why it matters
The limiter itself is right: atomic, with a TTL. It passes every test, because in tests Redis is always up.
In our run, 50 shoppers each sent a catalog read and an OTP request on two servers. With Redis down, the naive limiter failed all 100 with 500. With the per-endpoint policy, all 50 catalog reads were served on a local limit and all 50 OTP sends got a 503, so no SMS went out without a real limit.
Words you need
- Rate limit
- A cap on how many requests one user can make in a period. Past it, the API answers 429 Too Many Requests.
- Fail open
- When the limiter itself breaks, let traffic through instead of blocking it, so a limiter outage does not become an API outage.
- Cache
- A fast copy of data kept close by, here in Redis, so most reads skip the database.
- Load balancer
- The component in front of several identical servers that hands each incoming request to one of them, so no single server takes all the traffic.
Picture it
Two endpoints behind one limiter: a catalog read that only touches the database, and an OTP send that costs an SMS. Redis goes down.
Await Redis, no plan B
- GET /catalog → limiter
- Redis call throws
- Error handler returns 500
- POST /otp/send: same
- Database was healthy all along
100 of 100 requests fail. The limiter is now an off switch for the API.
Per-endpoint policy
- Redis call times out fast
- GET /catalog: count locally at limit ÷ servers
- Served, still rate-limited
- POST /otp/send: 503, retry later
- No SMS sent without a real limit
Catalog stays up, OTP stays safe, nothing 500s.
The fixes people try
No try/catch
Failsawait redis.incr(key)Redis down means every request 500s, on every endpoint behind the limiter.
Fail closed everywhere
Failscatch { return 429 }Same outage with a nicer status code. And 429 lies: the user didn't exceed anything.
Fail open everywhere
Half a fixcatch { next() }The API stays up, but with no limit at all. A script can burn your SMS budget on the OTP endpoint in a minute.
Per-endpoint policy
Workscatalog → local limit; OTP → 503Availability where it's cheap, safety where it's expensive.
Circuit breaker on top
Worksafter N failures, skip Redis for 10sStops every request paying the timeout while Redis is down. Goes straight to the fallback.
The fix and its cost
Write the policy down per endpoint before the outage. Cheap, safe reads fail open, but not to zero protection: they count in each server's memory, with the limit divided by the number of servers, so all servers together stay under the real limit. A scraper during the outage still hits a wall.
Costly or abusable actions fail closed: OTP, login, payments. They return 503, not 429, because the user did nothing wrong. Your limiter is unavailable, and 503 tells the client to retry later.
Give the Redis call a short timeout. Many clients have no command timeout by default, so an outage makes every request hang instead of throwing, and the policy never runs.
What it costs
Interviewers also listen for: 429 vs 503
Check yourself
1Why does the catalog fail when only Redis is down?Show answerHide
The limiter runs before the handler and awaits Redis. Its error becomes a 500 before the catalog code ever runs.
2Why not just fail open everywhere?Show answerHide
Some endpoints cost money or are abuse targets, like OTP sends. With no limit, one script can do real damage during the outage.
3Why divide the limit by the number of servers in the fallback?Show answerHide
Each server only sees its own share of traffic. Dividing keeps the total across all servers under the real limit.
The code
limiter.bug.ts
Example code. The tested version, with a proof test against real Postgres, lands in the repo.
Star the repo for the runnable version
Each trap in this world ships with the bug, the fix and a proof test that fires concurrent requests at a real database.
Remember
Your limiter protects the API. Make sure it can't take the API down. Decide per endpoint: local limit or 503.