Design an API gateway
The interviewer asks
“Design an API gateway that sits in front of hundreds of microservices. It should handle routing, authentication, rate limiting and observability for both public and internal APIs.”
What a strong answer looks like
A strong answer separates the data plane (a fleet of stateless proxies on the hot path) from the control plane (where routes, plugins and keys are configured and then pushed to the proxies). On the request path the gateway terminates TLS, matches a route, runs a short plugin chain (auth, rate limit, transforms), and forwards to a healthy upstream instance with timeouts, retries and circuit breakers. The deep dives: keeping added latency under ~5 ms p99 by never calling a database per request (local JWT validation, cached API keys, local-plus-global rate limits), safe config propagation, and resilience to slow upstreams. Mention multi-tenancy, observability (trace IDs, per-route metrics) and why the gateway must not become a dumping ground for business logic.
Concepts to mention
1 Requirements
Functional
- Route requests by host, path, method and headers to the right backend service and version
- Authenticate callers: JWT/OAuth2 for users, API keys or mTLS for partners and services
- Rate limit and enforce quotas per API key, user, IP and route
- Apply timeouts, retries and circuit breaking per route
- Transform requests and responses (header injection, request IDs, simple REST to gRPC mapping)
- Emit access logs, metrics and traces for every request
- Let teams change routes and policies through a self-serve control plane without restarts
Non-functional
- Added latency: < 2 ms p50, < 5 ms p99 over the backend time
- Throughput: 500k requests/s peak (festive sale), linearly scalable by adding nodes
- Availability: 99.99%; the gateway must be more available than any service behind it
- Config changes visible on all nodes within ~10 s
- Rate-limit accuracy within ~5% of the configured limit across the fleet
- No single point of failure across 3 availability zones
Out of scope (say it out loud)
- Full service mesh for east-west traffic (discuss briefly)
- Developer portal and API monetisation UI
- Business logic and data aggregation (belongs in BFF services)
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
ANYhttps://api.example.com/{service}/{path} | Client traffic proxied to the matched upstream |
POST/admin/v1/routes | Create a route: match rules, upstream, timeouts, retries, plugins |
PUT/admin/v1/routes/{id} | Update a route; creates a new config version |
POST/admin/v1/consumers/{id}/keys | Issue an API key with plan and quota |
gRPCxDS / ConfigStream(node_id, last_version) | Gateway nodes stream config snapshots from the control plane |
GET/healthz | Load balancer health check for each gateway node |
4 Data model
routesPostgres (control plane)route_id (PK)hostspath_patternmethodsupstream_idtimeout_msretry_policyplugins (JSONB)versionowner_teamSource of truth with audit history. Compiled into an immutable snapshot (trie / radix tree for paths) and pushed to nodes.
upstreamsService registry (Consul / Kubernetes endpoints)upstream_idservice_nameendpoints[]healthweightsEndpoints change often; gateway watches the registry instead of polling.
api_keysPostgres + local in-memory cachekey_hash (PK)consumer_idplanscopesstatuscreated_atStore only a hash of the key. Nodes cache key → consumer for ~60 s; revocations are pushed as events.
rate_limit_countersRedis Clusterrl:{consumer}:{route}:{window} → countTTL = windowSharded by consumer. Used for global limits; nodes also keep local token buckets.
access_logsKafka → ClickHouse / S3tsrequest_idroute_idconsumer_idstatuslatency_msupstream_latency_msbytesColumnar store for per-route dashboards and per-consumer usage billing.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Proxy an authenticated request
- 1Client calls POST /orders over HTTPS; the L4 load balancer picks a gateway node in the nearest zone
- 2Gateway terminates TLS, assigns a request ID and trace ID (or keeps the incoming ones)
- 3Route matching on host + path in an in-memory radix tree selects route 'orders-create' (microseconds)
- 4Auth plugin validates the JWT signature locally using cached JWKS keys and checks expiry, audience and scopes; no network call
- 5Rate-limit plugin checks the local token bucket for (user, route); global counters are synced to Redis in the background
- 6Gateway picks a healthy Orders instance (least-request load balancing, outlier ejection) and forwards with a 2 s timeout and the user ID in a trusted header
- 7Response returns; gateway records latency metrics, writes an access log line to a local buffer that ships to Kafka
Roll out a route change
- 1Team updates the route via the admin API; control plane validates it (schema, conflicts, ownership) and stores version N+1
- 2Control plane compiles a new snapshot and pushes it to 5% of gateway nodes (canary)
- 3Automatic checks compare error rate and latency of canary nodes vs the rest for a few minutes
- 4If healthy, the snapshot rolls to all nodes; each node swaps config atomically without dropping connections
- 5If unhealthy, the control plane pushes version N back; nodes also keep last-known-good config locally
7 Deep dives
Keeping the hot path fast: no database per request
The gateway adds latency to every API call, so the rule is: nothing on the request path waits on a remote store unless it must. Routes are compiled into an in-memory radix tree, so matching is microseconds. JWTs are validated locally: the gateway caches the identity provider's public keys (JWKS), refreshes them every few minutes and on an unknown key ID, and checks signature, expiry and scopes. Opaque API keys are hashed and looked up in a local cache backed by Postgres, with a TTL of ~60 s and push-based revocation events for fast blocking. Plugins run in a fixed order and must be cheap; anything slow (fraud scoring, content scanning) belongs in a service, not in the gateway. Use an event-loop proxy (Envoy, NGINX/OpenResty) with keep-alive connection pools to upstreams so TCP and TLS handshakes are not paid per request. With this, the gateway's own cost stays ~1-2 ms at p50. Measure it separately: log total latency and upstream latency, and alert on the difference.
Distributed rate limiting across 50 nodes
A limit like '1,000 requests/min per API key' is global, but traffic for one key spreads across 50 nodes. Options: (1) Call Redis on every request with an atomic Lua script (sliding-window or token bucket). Accurate, but it adds ~0.5-1 ms and a Redis dependency to every call; at 500k req/s that is a big Redis cluster. (2) Pure local limits: divide the limit by node count. No network cost, but uneven load balancing makes it unfair. (3) Hybrid, which most large gateways use: each node keeps a local token bucket and syncs its consumption to Redis every ~100 ms or every N requests, getting back the global count and adjusting its local share. Accuracy is within a few percent, and Redis load drops by 10-100x. Use sliding-window counters for fairness at window edges. If Redis is unreachable, fail open with local-only limits (protect availability) for normal APIs, but fail closed for abuse-sensitive routes like OTP or login. Return 429 with Retry-After and X-RateLimit-Remaining headers.
Resilience toward upstreams
The gateway sees every service failure first, and it must not amplify it. Each route has a timeout lower than the client's (for example 2 s for the gateway vs 5 s on the app), so a slow service frees gateway resources. Retries only for idempotent methods (GET, or POST with an Idempotency-Key), at most 1-2 attempts, with jittered backoff and a retry budget (retries ≤ 10-20% of requests) so retries cannot triple the load during an outage. Outlier detection ejects instances that return consecutive 5xx. Circuit breakers cap concurrent requests and pending queue per upstream: when Payments is slow, its connection pool fills and new Payments requests fail fast with 503, while Orders traffic is unaffected. This is bulkheading per upstream. For a festive sale, priority-based load shedding drops low-value traffic (recommendations) before checkout. Hedged requests can cut tail latency for idempotent reads but double load, so use them only for a few critical paths.
Control plane vs data plane
Split the system so the data plane keeps working when the control plane is down. The control plane owns the config database, validation, RBAC (which team may change which routes), audit logs, and compilation of snapshots. Data-plane nodes subscribe over a streaming API (Envoy's xDS is the standard) and get versioned, complete snapshots, not individual diffs, so a node that reconnects simply takes the latest version. Nodes keep the last applied config on disk; if the control plane is unreachable they keep serving with it. Changes roll out progressively with health gates. Validation catches route conflicts (two teams claiming /v1/users) and dangerous settings (timeout 0, retries 10). This split is also how you offer the gateway as a platform: hundreds of teams self-serve routes without a central team editing NGINX files and doing reloads.
8 Trade-offs
One central gateway vs per-domain gateways and BFFs
One gateway gives one place for auth and limits but becomes a shared bottleneck in process and blast radius. Larger companies run several gateway clusters (public, partner, internal) built on one platform, plus BFF services per client type for aggregation. Keep business logic out of the gateway either way.
Build on Envoy/NGINX vs managed gateway
Managed (AWS API Gateway, Apigee) is fast to start and includes keys and quotas, but costs per request (expensive at 500k req/s), adds latency and limits customisation. Self-hosted Envoy or Kong gives control and low cost at scale but needs a platform team. Choose managed for small scale, self-hosted at large scale.
Fail open vs fail closed
If the rate limiter or auth key cache is unavailable, failing open keeps the business running but removes protection; failing closed protects backends but turns a small dependency failure into a full outage. Decide per route: auth fails closed (with cached keys as a buffer), most rate limits fail open.
9 Bottlenecks
TLS handshakes during a traffic spike
A sale start brings millions of new connections; full TLS handshakes are CPU heavy. Use TLS 1.3, session resumption, ECDSA certificates, keep-alive, and pre-scale gateway nodes before known events instead of relying on reactive autoscaling.
Redis hot key for a big partner
One very large API consumer makes its rate-limit key hot on one Redis shard. Use the hybrid local-bucket approach so the key is touched only on sync, or split the key into N sub-keys and sum them.
10 Failure handling
A gateway node crashes
The L4 load balancer health check removes it within seconds. Nodes are stateless (state is in config snapshots and Redis), so others take the load. Keep N+2 capacity per zone.
Control plane outage
Gateways keep serving with their last-known-good snapshot. Only config changes are blocked. Emergency changes (block an abusive key) can go through a small, separately deployed denylist channel.
Upstream service melts down
Timeouts, circuit breakers and per-upstream connection limits isolate it. The gateway returns fast 503s for that route and protects its own threads and memory so other routes are unaffected.
11 What impresses
- Stating a latency budget for the gateway itself and how to measure it
- Hybrid local + global rate limiting with clear accuracy vs cost reasoning
- Retry budgets and idempotency rules so retries do not amplify outages
- Explaining xDS-style versioned snapshots and last-known-good config
- Per-route decisions on fail open vs fail closed
- Keeping business logic out of the gateway and using BFFs for aggregation
12 Mistakes
- Calling the auth service or a database on every request
- A single gateway instance or a gateway with in-memory sessions (not stateless)
- Retrying all failed requests, including non-idempotent POSTs, with no limit
- Putting aggregation and business rules in the gateway until it is a monolith
- Hand-editing config files and reloading nodes without validation or canaries
- Forgetting observability: no request IDs or per-route metrics
13 Follow-ups
Lessons this design uses
Practice questions
- Your company is putting an API gateway in front of 60 microservices. What should the gateway do, what should it not do, and where do backend-for-frontend layers fit?
- The mobile app gives up on a request after 3 seconds, but your backend keeps working on it for 15 seconds, calling five services. Why is that a problem, and how do you fix it?
- When would you put an L4 load balancer in front of your services, and when an L7 one? Give an example of each.
- You have 80 microservices inside a Kubernetes cluster. Today any pod can call any service's internal API without authentication because 'it is inside the VPC'. Why is that risky, and how would you secure service-to-service calls?
- You are launching a public developer API for merchants. Design the API key system: generation, storage, scopes, rotation and leak handling.
- Your payments API is hit by a DDoS attack: first a 400 Gbps UDP flood, then a slow, distributed HTTP flood of valid-looking requests at 2 million RPS. Walk me through a layered defence architecture.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.