Stop 20 · System DesignTopics · 15/15

Designing a Rate Limiter

Part of the Backend Developer Roadmap

In short

Interviewers love 'design a rate limiter' because it starts simple and gets hard fast: many servers, shared counters, race conditions, and what to do when Redis itself is down.

Another classic system design question — designing a rate limiter that works correctly and efficiently across multiple distributed server instances, not just a single process.

The purple blob with a tally clicker lets a few people through a metro turnstile and stops the next.

Think of it like this

The metro turnstile guard

During rush hour, a guard at the metro gate lets in a set number of people per minute, counting with a clicker. All gates report to one control room, so you can't dodge the limit by switching gates. Your rate limiter is that guard, and Redis is the control room.

Watch it happen

A shared counter in Redis

Client
API server
Redis

GET /search (key k_7)

1/7A request arrives with API key k_7. The limit is 100 requests per minute per key.

The big ideas

What you need to know

Shared state

With many API servers, counters must live in a shared store like Redis, or each server allows the full limit.

Atomic operations

INCR is atomic, so two servers can't both read 99 and write 100. Use Lua scripts for multi-step logic.

Limit by what matters

Per API key, user, IP or endpoint. A login endpoint needs much stricter limits than a product list.

Decide the failure mode

If Redis is down, fail open (allow) or fail closed (block)? Often open for reads, closed for logins.

See it in code

Walk through the code

js
1const LIMIT = 100;2 3export async function rateLimit(req, res, next) {4  const key = req.get("x-api-key") ?? req.ip;5  const minute = Math.floor(Date.now() / 60_000);6  const redisKey = `rl:${key}:${minute}`;7 8  const [count] = await redis.multi().incr(redisKey).expire(redisKey, 60).exec();9  res.set("RateLimit-Limit", String(LIMIT));10  res.set("RateLimit-Remaining", String(Math.max(0, LIMIT - Number(count))));11  if (Number(count) > LIMIT) {12    res.set("Retry-After", String(60 - (Math.floor(Date.now() / 1000) % 60)));13    return res.status(429).json({ error: "Too many requests" });14  }15  next();16}

1/4Identify the client and build a counter key for the current minute.

This is a fixed window, the simplest design. Interviews usually move on to sliding windows or a token bucket kept in Redis with a Lua script.

Save yourself the pain

Common mistakes

Oops

Keeping counters in each server's memory behind a load balancer.

Do this instead

With 4 servers, a client effectively gets 4 times the limit. Use a shared store.

Oops

Doing GET, checking in code, then SET, as separate calls.

Do this instead

Two requests can read the same count at once and both pass. Use INCR or a Lua script so it's atomic.

Your mission

A distributed limiter

~40 min

Quick check

Why use Redis INCR instead of GET followed by SET for the counter?

Go deeper

  1. 1Design how a rate limiter's counters would be shared across multiple server instances using Redis.
  2. 2Decide which algorithm (token bucket, sliding window) fits the requirements given and why.
  3. 3Consider what happens to your rate limiter if Redis itself becomes unavailable.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge