Design a Rate Limiter & API Gateway
- Design a rate limiter inside an API gateway that many replicas share
- Pick a token bucket over a shared counter, so every replica enforces one limit
- Handle the failure modes IBM pushes on: fail-open vs fail-closed, timeouts, breakers, backoff, bulkheads
Understanding the problem
Design a rate limiter that lives inside an API gateway sitting in front of many microservices. Every request carries an API key. The limiter reads which tier the key belongs to and decides whether the client has budget left. Then it forwards the request to the right service, or rejects it with an HTTP 429 and a Retry-After header. It has to add almost no latency and stay correct even though the gateway runs as many replicas at once. And it has to keep serving sensibly when the piece it depends on, or one of the services behind it, falls over.
IBM reaches for this design once a candidate is senior enough to get a system-design round at all, and the choice is deliberate. IBM's world is hybrid cloud and microservices. An enterprise runs a few hundred services across its own data center and a public cloud, with an API gateway as the front door. The rate limiter is the part of that gateway that decides who gets through and who waits. Nothing here runs at consumer-firehose scale. Strip it down and you have a counter per client that must stay correct while a fleet of gateway replicas hits it at once, and must stay up when something underneath it fails. IBM cares far more about how gracefully the design degrades than about how many billions of requests it can survive.
The round usually starts small, a limiter for one service, and then grows under you, with something like two hundred services behind the gateway, then twenty gateway replicas, then Redis going down. At some point expect the interviewer to drop from the architecture straight into code and ask for the token-bucket check, or the class that holds the counter. That drop from high-level design to low-level implementation inside one question is common here, so know the refill math well enough to write it cold.
Functional requirements
Keep the core small and let the interviewer pull more out of you. A client calls the gateway with an API key. The gateway resolves which tier that key belongs to, checks whether the client has budget left, and either forwards the request to the right service or rejects it. A rejection is an HTTP 429 with a Retry-After header telling the caller how long to wait. Two requirements carry the weight. The limit holds even though many gateway replicas serve the same client. And the rejection is honest: Retry-After reflects when a token actually frees up.
In scope
- Per-client and per-API-key limits, resolved from the key the caller presents on every request
- Multiple tiers (free, silver, gold, enterprise), each mapping to its own rate and burst size
- Return HTTP 429 with a Retry-After header, plus X-RateLimit headers so a client can self-throttle before it trips
- One shared limit across all gateway replicas, so the client sees its true rate no matter which replica answers
- Fail-open or fail-closed behavior when the rate-limit store is unreachable, chosen on purpose
Below the line (name it, then move on)
- Authentication itself. The gateway verifies the key; issuing and rotating keys is a separate identity system
- Per-URL or per-parameter quotas beyond the tier limit. We limit by client and tier; fine-grained WAF rules are their own layer
- Billing and usage metering. We count for admission control, not for invoicing, though the counts can feed it
- The business logic of the two hundred services behind the gateway. We admit or reject; what a service does is its own concern
- A single global limit spanning regions. We keep the counter regional here and note multi-region as a later refinement
Non-functional requirements
Put a number on each target. A rate limiter is judged on a few concrete promises, and added latency comes first. The check sits on the hot path of every request, so the budget is one to two milliseconds at p99, which by itself rules out heavy work per call. Correctness across the fleet is next. A client that bought, say, one hundred requests a second should see one hundred. One hundred times the replica count is a bug. Availability is where the design earns its grade. The limiter must never be what takes the gateway down. If the shared counter is slow or unreachable, the gateway keeps serving under a fallback you picked on purpose, and it does not fail every request.
One piece of slack is worth pocketing now. The count can be slightly approximate. An enterprise client on a hundred-a-second plan will not open a ticket because the gateway let through a hundred and three during a one-second spike, or held it to ninety-eight. That few percent pays off later. It's what lets you keep a network round-trip off the hot path, with each replica serving from a small local reservation and reconciling with the shared counter in the background.
- ~50K/s aggregate request rate. Peak traffic across all clients through the gateway; enterprise scale, not a consumer firehose. Illustrative teaching figure.
- ~20 gateway replicas. The gateway runs as a horizontal fleet behind a load balancer, so any replica can serve any client, which is exactly why the counter has to be shared.
- 1-2 ms added latency budget (p99). The check runs on every request, so it has to be near free; this budget is what rules out a network hop per call.
- 429 + Retry-After the rejection contract. An over-limit request gets a 429 and a Retry-After the caller can trust, so a well-behaved client backs off instead of hammering.
Where the cost falls
Sketch the load before the architecture. One asymmetry shapes the whole design. Admitting a request is cheap. Doing it correctly across twenty replicas, on the hot path, without a round-trip per call, is not. The figures are illustrative, so derive them out loud. A good interviewer redoes the arithmetic with you. Take a silver-tier client on six thousand requests a minute. Six thousand divided by sixty is one hundred requests a second. Now picture the gateway as twenty replicas behind a load balancer that sprays that client's calls evenly. If each replica keeps its own local counter set to the full hundred a second, the client can actually push twenty times a hundred, two thousand a second. Every replica thinks it is fine. The limit is only real if the twenty replicas share one view of the count. That single fact forces most of the design.
- The check: on every request. the limit decision runs inline before routing, up to ~50,000 times a second in aggregate, so it lives on a one-to-two millisecond budget and cannot afford heavy work per call.
- The shared count: one view across replicas. twenty replicas each holding a local full-limit counter overshoots by 20x. The count must be shared, and that sharing is where the round-trip cost and the hot-key risk live.
- The limiter down: must not sink the gateway. the counter store can blip. If the gateway fails every request when it does, the limiter has become a single point of failure for two hundred services at once.
- A downstream down: one bad service, not all. one of the ~200 backends can hang. Without isolation, its slow calls pile up and consume every gateway thread, so a single sick service starves the other 199.
Two lines in that grid set the agenda. Sharing the count makes the limit correct, and it also tempts you into a network round-trip per request that blows the latency budget. Keeping the limiter and the downstreams from taking the gateway down is the enterprise-resilience half. IBM leans on that half hardest. So the design has to hold three properties at once. The check stays cheap. The count is shared but not perfectly exact. And a failure anywhere underneath degrades to something sensible instead of a cascade.
The set-up
Core entities
List the moving parts before you draw an arrow. There are only three. The whole limiter is a little state per client plus the policy that governs it.
Client and API key
The unit the limit applies to. Every request carries a key, and the gateway resolves it to a client identity and a tier. The key is the counter's partition: one bucket per key, so one noisy client can never spend another client's budget.
Tier policy
The rule for a tier: a sustained rate in tokens per second and a burst, the bucket's capacity, meaning how many requests may arrive at once after a quiet spell. Free through enterprise are four rows in a small config table the gateway reads, so changing a limit is a config edit, not a redeploy.
Counter (the bucket)
The live state per key: how many tokens are left and when it last refilled. Two numbers. This is what lives in the shared store, gets read and decremented on every request, and is the thing the entire distributed-counting deep dive is about.
The interviewer will ask where each piece lives, so pin the stores next. The counter lives in a fast in-memory store, Redis being the usual pick, keyed by the client key so each bucket is one entry. Redis is right here because the counter is touched on every request and has to answer in well under a millisecond, which a disk-backed database cannot promise. The Tier policy lives in ordinary config, cached in each gateway replica and refreshed occasionally, because it changes rarely and is read constantly. The gateway itself holds no durable state. Any replica can serve any request, and losing one just means the load balancer routes around it.
API
The external surface is thin. A rate limiter is mostly one decision applied to every call. Do write down the internal atomic check against the counter store, though, since the whole correctness story lives in that one operation. You'll refer to these by name later.
Two of these carry the design. The proxied call is the product, and an honest 429 is what makes clients trust the limiter instead of routing around it. If Retry-After doesn't reflect when a token actually frees up, clients ignore it and retry immediately, which defeats the point of a limiter. The internal allow check is the other one. It turns twenty independent replicas into one correct limit, and making it atomic is most of the job. The deep dives either make that check more correct or make it cheaper.
High-level design
Follow one request from a client to a backend. An API clients call arrives at the API gateway carrying an API key. The gateway resolves the key to a client and a tier by reading the Tier policy it keeps cached. It runs the atomic budget check against the Rate-limit store, and on a pass forwards the request to the right one of the Microservices behind it. On a fail it returns a 429 straight back to the caller without ever touching a backend. The diagram colors each box by role: blue for the API clients, violet for the API gateway and the Microservices it fronts, and green for the two stores it reads.
1) The request arrives and the key is resolved. An API clients call reaches the API gateway with an API key in the Authorization header. The gateway verifies the key and looks up its tier in the Tier policy, which it keeps cached, so this step adds no network hop. Now it knows the client identity and which bucket size applies. Anything slow stays off the hot path on purpose. No per-request database read, no call out to an identity service. Either one would blow the latency budget on every request.
2) The gateway checks the budget. With the tier in hand, the gateway runs the atomic allow check against the Rate-limit store. That one operation refills the client's token bucket based on how much time has passed, tries to remove a token, and returns whether the request is allowed along with how many tokens remain. The limit is only correct if this is atomic across the fleet. That is the first place the design gets pushed, and the second deep dive owns it.
3) An admitted request is routed. If a token was available, the gateway forwards the request to the right one of the Microservices, adds the X-RateLimit headers to the response, and streams the answer back. The common case stays cheap: a cached tier lookup, one fast counter check, then a proxy. What gets punted for now is a slow or dead downstream. One hanging backend must not take the gateway with it, and the fourth deep dive deals with that.
4) A rejected request gets an honest 429. If the bucket was empty, the gateway never touches a backend. It returns a 429 with a Retry-After computed from the bucket's refill rate, the time until the next token appears, plus the X-RateLimit headers. A good client reads Retry-After and waits. A bad one retries immediately and gets another 429, which costs the backends nothing because the reject happens at the gateway. Which counting algorithm makes that Retry-After trustworthy is the first deep dive.
5) When the machinery underneath blips. Everything above assumes the Rate-limit store answers quickly and the Microservices are healthy. Neither is guaranteed. The store can be briefly unreachable, and any of the two hundred backends can hang. In those moments the gateway can fail open to a local fallback when the store blips, trip a circuit breaker on a sick service, and isolate its threads. That is the enterprise-resilience core, and the last deep dive covers all of it.
Technology choices
A box on the diagram is just a label until you name a real tool, say why that kind of tool fits, and know how it breaks under this load. Put a popular pick in each slot. The follow-ups will be about how each one fails.
- Rate-limit store is an in-memory key-value store: Redis, or a compatible managed one. In-memory is right because the counter is read and written on every request and has to answer in well under a millisecond, which a disk-backed database cannot. The primitive that matters is a Lua script, which Redis runs atomically. It refills the bucket, spends a token, and writes the new state in one indivisible step, so two gateway replicas hitting the same key can never both take the last token. The failure mode to name is Redis as a hot single node at fifty thousand checks a second. The fixes are sharding the keyspace by client key and, in the next dive, serving most requests from a local reservation so the store is not touched on every call.
- API gateway is a real gateway, not something you hand-roll: Envoy or NGINX, or a managed API gateway on top of one. This kind of tool exists precisely to do auth, routing, rate limiting, and request and response transforms in one place, so two hundred services do not each reimplement them. Rate limiting is a first-class feature in both, backed by exactly the token-bucket and shared-counter machinery here. The failure mode is the gateway itself becoming a bottleneck or a single point of failure. The answer is the horizontal fleet behind a load balancer, which is also why the counter has to be shared.
- The resilience layer is a library, not a diagram box: Resilience4j, or Envoy's own circuit breaking, for the breakers, timeouts, and bulkheads that keep a sick downstream contained. They are configuration on the gateway rather than new services. The failure they answer is a slow backend consuming every gateway thread, and the mechanism is a per-downstream connection pool plus a breaker that fails fast once error rates cross a threshold. The fourth deep dive is where this earns its place.
- For the estate itself, a service mesh such as Istio on OpenShift is where a lot of enterprise IBM deployments live. The mesh gives you sidecar rate limiting for east-west traffic between services, while the gateway handles north-south traffic from outside. Name the split honestly: the gateway is the front door for external clients, the mesh handles service-to-service limits inside, and both lean on the same counting ideas.
Potential deep dives
A rate-limiter round usually pushes on four corners: the counting algorithm, keeping the count correct across a fleet, where the limiter sits in the gateway, and what happens when something underneath fails. IBM spends the most time on the last one. It gets the most room below.
1) Which counting algorithm actually holds the line?
Start with the counting. The algorithm you pick decides how fair the limit feels and how honest the Retry-After is. Anchor on a silver client at six thousand requests a minute, which is one hundred a second. Every algorithm answers the same question: given the requests that already arrived, is this next one allowed? The naive answers have real flaws. Walk from the worst to the best, because that is the reasoning the interviewer wants to watch.
Approach
Chop time into fixed one-minute windows and keep a count per client per window. Increment on each request, reject once the count passes the tier limit, and reset to zero when the clock ticks into the next minute. In Redis it is a single INCR on a key like client:123:minute:2026-07-18T12:00 with an EXPIRE so old windows clean themselves up. It is the least code, one counter and one comparison, and for a client whose traffic is smooth it enforces the limit exactly.
Challenges
It breaks at the window boundary, and the break is a factor of two. A client can send its full six thousand in the last second of one minute and another full six thousand in the first second of the next. Twelve thousand requests land in a span of about two seconds, and every window count stayed legal. The limit says one hundred a second. The boundary quietly lets through something closer to two hundred for that moment. For a downstream you were trying to protect, that burst is exactly the spike the limit existed to prevent.
Approach
Keep two counters, the current minute and the previous one, and estimate the rate over the trailing sixty seconds by weighting the previous window by how much of it still overlaps. Fifteen seconds into a new minute, you are twenty-five percent through it, so you count the current window in full plus seventy-five percent of the previous one. Say the previous minute saw six thousand requests and the current one has seen a thousand. The estimate is one thousand plus six thousand times zero point seven five, or five thousand five hundred, so the request is allowed under the six-thousand limit. It is still just two counters per client, so it stays cheap, and it smooths the boundary the fixed window fell over.
Challenges
The estimate assumes the previous window's requests were spread evenly, which they were not, so it is approximate. A burst that landed all in the last second of the previous minute is treated as if it were smeared across the whole minute. The error is small in practice and biased toward the limit rather than wildly past it, which is why it makes a fine default. What it still does not give you is a real burst allowance. It smooths the rate, but it has no notion of letting a client that has been quiet spend a little saved-up capacity at once, which is often exactly the behavior an API wants.
Sliding-window counters are already cheap, so the jump to the last tier is about the missing burst. Real API clients are bursty on purpose. A batch job wakes up, fires a hundred calls, and goes quiet for a minute, and its average stays well under the limit the whole time. A pure rate limit punishes that pattern anyway. You want a limiter that says yes to a short burst as long as the sustained rate holds. A token bucket has exactly that shape, and it is the practical default real gateways ship.
Approach
Model each client as a bucket that holds up to burst tokens and refills at rate tokens a second. Every request removes one token, and an empty bucket means a 429. For the silver client, set burst to one hundred and rate to one hundred a second. It sustains a hundred a second forever. After a quiet spell it can fire up to a hundred at once, which is the burst real clients need. The state is two numbers per key, the token count and the last refill time. On each request the check does tokens = min(burst, tokens + (now - last_refill) * rate), then if tokens >= 1 it decrements and allows, otherwise it rejects. Retry-After falls straight out of the math. The time until one token accrues is one divided by the rate, so an empty silver bucket says come back in about ten milliseconds. A spent per-minute quota says wait until the next refill.
Challenges
The knobs are real and you tune them to the client. A large burst is friendly to legitimate spikes but lets a client hit a fragile downstream harder in one instant. Burst size trades user-friendliness against how much of a shock the backend can absorb. The state per key is tiny, but it is state a fleet of replicas has to share and update atomically. Two replicas that both read three tokens, both spend one, and both write two have together let through two requests while only decrementing one token. That atomic-update problem across replicas is what the next dive covers.
Token bucket wins because it separates two things the others blur together: a sustained rate and a one-time burst. Refill at the rate, cap at the burst, spend a token per request, and a 429 with a Retry-After of one-over-rate falls out for free. Fixed windows are cheaper but leak a double-rate spike across the boundary; sliding-window counters fix that but offer no burst allowance. The catch sets up everything next. The bucket is two numbers per client, and a whole fleet of gateway replicas must read and update them without two replicas ever spending the same token.
2) How do twenty gateway replicas share one limit?
A single gateway makes the token bucket trivial: the two numbers live in local memory and every request updates them in order. Reality is a fleet. The gateway runs as roughly twenty replicas behind a load balancer, and a client's requests spray across all of them with no affinity. Say each replica keeps its own local bucket sized to the full hundred a second. The client can push a hundred through each of twenty replicas at once, two thousand a second, and every replica still reads itself as under the limit. So the count has to be shared. Sharing it correctly inside a one-to-two millisecond budget is the hard part.
Approach
Let each replica keep the token bucket in its own memory, no coordination at all. It is the fastest possible check, pure local memory and zero network, and if you only ever ran one replica it would be perfect. Teams reach for it because it is trivially cheap and it feels like it should be close enough. It is not.
Challenges
It overshoots by the replica count, and the replica count is exactly the number you scale up under load. Twenty replicas means twenty times the intended limit gets through; forty replicas means forty times. The one number a rate limit is supposed to guarantee is off by an order of magnitude, and it drifts further out precisely when traffic grows and you add replicas. You could divide each replica's limit by the fleet size, but the fleet size changes as replicas autoscale. And a client whose requests land unevenly still gets throttled early on a busy replica while others sit idle. Local-only cannot enforce a shared number.
Approach
Move the bucket into one shared store all replicas read. Redis holds the token count and last-refill time per client key, and every replica runs the same atomic check against it. The atomicity is non-negotiable, so the whole refill-and-spend runs as a single Lua script on Redis. It reads the two numbers, adds any tokens accrued since the last refill, and spends one if it can, all indivisibly. Two replicas hitting the same key are serialized, so they cannot both take the last token. Now the twenty replicas share one true count, and the limit is exactly right.
Challenges
Every request now pays a network round-trip to Redis before it can be admitted. Same-availability-zone that is roughly half a millisecond, which fits the budget but eats most of it. Redis now also sees the full aggregate rate, up to fifty thousand checks a second, on the hot path of every call. Redis can handle that on one node, but the node becomes a single point of failure and a hot spot, with a popular client's key as one entry every replica contends on. You have traded a correctness bug for a latency cost and a dependency that, if it stalls, stalls every request. Making that dependency safe is the fourth dive; making it cheap is the rest of this one.
Approach
Stop hitting Redis on every request by letting each replica reserve a small batch of tokens and serve from it locally. Instead of spending one token in Redis per call, a replica claims, say, ten tokens at once. It decrements its local reservation for the next ten requests and only goes back to Redis when the reservation runs low. Most requests are then a local-memory check with no network at all, and Redis traffic drops by roughly the batch size, from fifty thousand a second toward five thousand. Shard the Redis keyspace by client key across several nodes so no single node carries every check, which also spreads the hot-key load. The shared counter stays the source of truth, and the reservations are just a lease of capacity handed out ahead of time.
Challenges
Reservations make the limit approximate at the margin. If all twenty replicas are holding up to ten un-spent reserved tokens for a client, the true admitted count can run two hundred over the strict limit for a moment. So you size the batch against how much slack the tier tolerates: small batches for a tight limit, larger for a loose one. There is also waste: a replica that reserved ten tokens and then goes idle has stranded that capacity until it expires, so reservations need a short time-to-live so unused ones flow back. This is the exactness-for-latency trade the non-functional requirements banked up front, spent on purpose.
The limit is only real if the replicas share one count, and it is only fast if they mostly do not look at it. A shared token bucket in Redis, updated by one atomic Lua script, makes the count exactly right but puts a round-trip on every request. Handing each replica a small reserved batch of tokens moves almost every check back into local memory and cuts store traffic by the batch size. The price is a bounded overshoot, replicas times batch size at the worst moment, and you can afford it only because you banked a few percent of slack back in the requirements. Shard the keyspace by client key so no one node is the hot spot.
3) Where in the gateway does the limiter belong?
Now zoom out from the counter to the pipeline around it. An API gateway does several jobs in sequence, and the order changes what the limiter can do. A gateway authenticates the caller, resolves its identity and tier, applies the rate limit, routes to a backend, and transforms the request and response along the way. Put the limiter in the wrong place in that chain and it either cannot see the tier it needs or wastes work on requests it should have dropped for almost nothing. The enterprise version of the question is sharper still: with two hundred services behind the gateway, do you centralize the limit or let each service run its own?
Approach
Skip the gateway's involvement and have every microservice enforce its own limit. Each of the two hundred services checks the caller's budget before doing work. It feels decentralized and autonomous, and for a handful of services owned by one team it can even be manageable.
Challenges
It is two hundred copies of the same logic, and they drift. Every team reimplements the token bucket, picks its own store, and reads the tiers slightly differently. A client's effective limit then depends on which service it hits, and nobody can reason about the policy globally. Worse, a request that should be rejected still travels all the way to a backend before being turned away, so the limit protects nothing upstream and every service pays to reject traffic. Changing a tier means a coordinated change across two hundred codebases. Centralizing the decision is the entire reason the gateway exists.
Approach
Centralize the limit in the gateway so there is one policy and one place to change it. The gateway sits in front of all two hundred services and applies the token bucket to every request before routing. This is the right instinct, one front door and one limiter, and it is where most designs land first.
Challenges
The trap is ordering. If the gateway rate-limits before it authenticates and resolves the tier, it does not yet know which bucket the request belongs to. It can only apply a crude anonymous limit. An attacker with no valid key can still consume that shared anonymous budget and squeeze real clients out. You also cannot offer per-tier limits, the reason the tier model exists, until identity is resolved. Limiting first sounds efficient, but it limits on information the gateway does not have yet.
Approach
Order the chain so each stage runs on the cheapest sufficient information. First, reject the obviously bad at the edge for almost nothing: malformed requests, oversized bodies, missing keys. Next, authenticate the key and resolve the client and tier, since the limiter needs the tier to pick the bucket. Then run the per-key token bucket against the shared store, and only an admitted request proceeds to routing and the backend. A 429 is returned right here, before any backend is touched, with Retry-After from the bucket. For traffic between services inside the estate, the same limiting runs in a sidecar in the service mesh, so the gateway governs north-south traffic from outside while the mesh governs east-west traffic within.
Challenges
Resolving identity before limiting means the auth step is now on the hot path too, so it has to be cheap. That is why the gateway verifies the key locally, from a cached key it can validate without a network call, not by phoning an identity service per request. The split between the edge gateway and the mesh sidecars also means the tier policy has to reach both consistently. The same config that feeds the gateway must feed the sidecars, or the two disagree about a client's limit. Ordering it this way costs something, but a per-tier limit only works in this order.
4) What happens when the limiter or a downstream is down?
The whole round was building toward this dive. Two things underneath the gateway can fail, and they fail differently. The rate-limit store can go slow or unreachable, and any of the two hundred backends can hang or start erroring. Either way the gateway keeps serving the healthy traffic it can and contains the damage. The patterns that do that are the enterprise-resilience vocabulary, and the interviewer wants to hear them used correctly.
Take the rate-limit store first: Redis is briefly unreachable, or slow enough that waiting on it would blow the latency budget. The gateway has two honest options, and picking one is a real decision. Fail open means that if the limiter does not answer within a tight timeout, the gateway allows the request. Availability is preserved, but the rate limit is gone exactly when the store is struggling, so a flood could pour through at the worst moment. Fail closed means that if the limiter is unreachable, the gateway rejects. The backends stay protected, but a brief Redis blip becomes a total outage where every request gets a 429, which is often a worse failure than the one you were guarding against.
Put a tight timeout on the Redis call, a few milliseconds, so a slow store never adds latency to every request. Then fail open, but not to no limit at all: fall back to the per-replica local token bucket at a conservative fraction of the real limit. When the shared store is gone, each replica still enforces something, so you neither drop all traffic nor remove all protection, and when Redis recovers the replicas resume the shared count. Which way you lean depends on what the limit protects. A limit guarding a fragile paid downstream or a hard cost ceiling leans toward fail-closed or a strict local fallback, while pure abuse-prevention on a robust service leans open. Say which, and say why.
Sizing that local fallback is its own small decision. Divide the real limit by the replica count and you are back to the exact math that made local-only wrong, so lean lower. Give each replica a fraction that, summed across the fleet, stays comfortably under the true limit. You would rather under-admit for the few seconds Redis is away than let the flood through. When the store returns, the replicas drop the fallback and resume the shared count on their next check. The fallback is a safety floor for a few bad seconds.
Now the other failure. A request was admitted, the gateway routes it, and the backend hangs. This is where one sick service among two hundred can quietly take down the gateway itself, because a gateway thread waiting on a dead backend is a thread that serves no one else. A gateway has only so many threads. Four patterns, layered, keep one bad service contained.
Approach
Route the request and wait for the backend to answer, however long that takes, and if it errors, retry immediately, maybe a couple of times, to paper over a blip. It is the obvious happy-path code, and when every backend is healthy it works, and the retries genuinely rescue the occasional transient error.
Challenges
A single hung backend takes the gateway down with it. Every request to that service parks a gateway thread waiting on a response that never comes. At enterprise request rates the gateway runs out of threads in seconds. Calls to the other one hundred and ninety-nine healthy services start failing too, because there is nothing left to serve them. The immediate retries make it worse: the moment a struggling service shows any life, every gateway retries at once and knocks it straight back down. One sick service becomes a full gateway outage, which is the exact cascade the enterprise cares about preventing.
Approach
Give every downstream call a deadline, a few hundred milliseconds or whatever the service's real latency plus headroom is, so a hung backend releases the gateway thread instead of parking it forever. Then wrap each downstream in a circuit breaker that tracks its recent error and timeout rate. Once that crosses a threshold, the breaker opens and fails fast, returning a 503 immediately for a cooldown period without even attempting the call. After the cooldown, let a single probe request through, half-open, and close the breaker again only if it succeeds. Now a dead service is detected quickly and the gateway stops throwing threads at it.
Challenges
Timeouts and a breaker contain the sick service, but two gaps remain. Retries, if you keep them, still fire in lockstep: when the breaker half-opens or the service recovers, every gateway retries at the same instant and re-synchronizes the herd that knocked it over. And the breaker is per downstream, but the threads are shared. Before the breaker trips, calls to the sick service can already have consumed enough of the shared thread pool to hurt the healthy ones. You have stopped the permanent cascade but not the thundering herd or the shared-resource contention.
Approach
Close the two gaps. Retry only idempotent requests, and space the retries with exponential backoff plus random jitter. Without jitter, every gateway retries at two seconds, then four, then eight, in unison. With it, each picks a randomized delay inside the window, and the load on the recovering service spreads out instead of arriving as a wall. Cap the retries so a hopeless call gives up. Then add bulkheads, giving each downstream its own isolated connection and thread pool, sized to that service. A hang in one service can then only exhaust its own pool, and the other one hundred and ninety-nine keep their capacity. The gateway degrades service by service instead of all at once.
Challenges
The isolation costs resources. A separate pool per downstream means more idle connections held across two hundred services. Size pools to real traffic, and share one pool across a group of low-traffic services instead of giving each its own. Jittered backoff also means a retried request takes longer to finally succeed or fail, so you set the caps and windows against how long the caller upstream is willing to wait. Tuning varies, but the shape is settled. Contain each downstream, fail fast when it is sick, and never let retries synchronize.
At IBM, partial failure is the heart of this design, and it splits cleanly. When the shared limiter is down, fail open onto a conservative local bucket, so a store blip degrades the limit instead of deleting the gateway. When a downstream is down, four moves contain it. A timeout releases the thread, a circuit breaker fails fast, jittered backoff keeps retries from synchronizing, and a bulkhead per service keeps one sick backend from starving the other hundred and ninety-nine. One rule ties it together. No single failure underneath the gateway gets to become a total outage above it. Degrade service by service, never all at once.
What is expected at each level
There's a lot of surface here, and your level decides how much of it you cover before anyone asks. Read the rungs as a gradient: mid gets a working limiter, senior owns the shared counter and the fail-open decision, and staff carries the full partial-failure story and the operability around it.
Mid-level engineer
Favor breadth, call it eighty to twenty. Reach one coherent design: a gateway in front of the services, a token bucket per client key, tiers mapped to bucket sizes, and a clean 429 with Retry-After. Get the token-bucket check itself onto the board, since this interviewer often drops from the diagram into the code, so be ready to write tokens = min(burst, tokens + elapsed * rate) and explain it. The phase that earns the pass is the fleet question. When they ask what happens with twenty replicas, reason toward a shared counter rather than a local one, even if you need a nudge to get there. A working limiter whose algorithm and 429 contract you can write out clears this rung. Expect the interviewer to fill in the distributed and failure details.
Senior engineer
Now depth matters, roughly sixty to forty, and this is the rung the round is tuned to. Raise the hard facts yourself: a local-per-replica counter overshoots by the fleet size, so the count must be shared and updated atomically, and the shared store must not become a single point of failure. Reach the token-bucket-in-Redis answer with an atomic Lua script, and then earn the local-reservation optimization by naming the round-trip cost that forces it. Make the fail-open versus fail-closed call out loud. Defend which way you lean, and why. You should find the twenty-times overshoot and the limiter-as-single-point-of-failure problems on your own and answer each concretely. Clearing this rung means a near-finished limiter whose distributed correctness and its main failure mode you surfaced without being asked.
Staff and beyond
Depth dominates across the whole picture, roughly forty to sixty. Own the partial-failure story end to end. The limiter fails open onto a conservative local bucket. The two hundred downstreams get timeouts, circuit breakers, jittered backoff, and bulkheads, each explained by the specific cascade it prevents. Carry the ordered gateway pipeline, the north-south gateway versus east-west mesh split, and the sharding and hot-key handling for the counter store. Show the arithmetic that takes a naive twenty-times overshoot back down to a bounded few-percent slack. Know how an operator changes a tier, watches for a client pinned at its limit, and sees a breaker open. You set the direction. The interviewer only nudges. The bar here is holding the cheap check, the shared count, the gateway ordering, and the failure containment as four distinct levers, and keeping added latency and graceful degradation intact through every refinement.
Key Takeaways
- ✓A rate limiter behind an API gateway is a small amount of state per client, a token bucket of two numbers, applied on the hot path of every request. Lead with the token bucket: refill at the tier's rate, cap at its burst, spend a token per call, and a 429 with an honest Retry-After of one-over-rate falls out for free.
- ✓The limit is only correct if the gateway replicas share one count. A local counter per replica overshoots by the fleet size, so the bucket lives in a shared store updated by one atomic operation. Handing each replica a small reserved batch of tokens moves almost every check back into local memory. You trade a bounded, deliberate overshoot for staying off a network round-trip per request.
- ✓Order the gateway pipeline so the limit runs after identity is resolved, or per-tier limits are impossible and an unauthenticated flood can eat the anonymous budget. Centralize the policy in the gateway, not across two hundred services, and split north-south gateway limits from east-west mesh limits.
- ✓Partial failure is what IBM is really testing. Fail the limiter open onto a conservative local bucket so a store blip does not take the gateway down. Contain a sick downstream with a timeout, a circuit breaker, jittered-backoff retries, and a bulkhead per service. Then one failing backend among two hundred never becomes a total outage.
The next lesson and 8 more IBM lessons are Premium. $49 a year, less than $1 a week · Cancel anytime
IBM interview guideKnow what IBM asks before you walk in.
14 coding · 3 system design · 13 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.