SysDesignPrep.com
System design interview question

Design an API Gateway

One front door for public and partner APIs: authenticate, limit and route 1 M requests a second to 300 services, adding under 10 ms.

Last updated 2026-10-26. Difficulty: hard. Patterns: api-gateway, authentication, control-plane, quotas, multi-tenancy. Reported at Amazon, Google, Microsoft, Stripe, Netflix, Uber.

Walk through a strong candidate's answer, turn by turn.

The interviewer asks, the candidate answers and draws, and you press Next. Skip to any stage or key moment, and ask the AI Mentor anything along the way.

Or sit it in the style of Amazon · Google · Microsoft · Stripe, run as that company’s round is reported to go.

The short answer

How to design API Gateway in a system design interview, in five steps:

  1. Scope it. One front door for every public and partner API, authenticate every request, authorise by scope and plan, rate limits and quotas per key, validate and transform requests cheaply, protect backends and each other, revoke credentials in seconds, observe everything per key, self-service config for 300 teams.
  2. Set the targets. Throughput: 1 M req/s peak · 3 regions; Added latency: p99 < 10 ms · p50 ~1 ms; Availability of the data plane: 99.99 %; Revocation delay: p99 < 5 s · worst case 15 min.
  3. Do the numbers. Requests per day: ~35 B · 1 M/s peak; Gateway proxies: ~25 per region · run 30 × 3; CPU per request for the checks: ~0.1 ms.
  4. Draw the design. L4 edge, Gateway proxies, Backend services, Rate limit counters, Authorization server, API key service, Key and plan store, Gateway control plane, Route and policy repo.
  5. Go deep where it is hard.
    • Self-service config without a bottleneck → Team-owned config, compiled and validated centrally, rolled out in stages.
    • Authentication without a network call → Local checks: JWTs against pushed signing keys, API keys against a pushed table of hashes, revocations pushed.
    • Quotas and rate limits are different systems → Rate limits as synced local buckets; quotas counted from the usage stream, enforced by pushed flags, reconciled.
    • When a backend slows down → Per-route timeouts and deadlines, per-cluster in-flight caps, idempotent-only retries within a 10 % budget, circuit breakers.
    • Noisy neighbours and shedding by priority → Per-key and per-app limits, separate partner and public pools, priority classes shed in order, edge drops for floods.
    • Validating and transforming bodies → Stream by default; validate small bodies on routes that opt in; rewrite headers, paths and protocols only.

Functional requirements

  • One front door for every public and partner API. Route by host, method, path and version to ~300 services. /v1 and /v2 live side by side, and a team can send 5 % of a route to a new backend version, assigned by key so each partner sees one version.
  • Authenticate every request. API keys for partner servers, OAuth 2.0 access tokens (JWTs) for apps acting for our users. Bad credentials are rejected before any backend sees the request, and backends get a verified identity, never the credential itself.
  • Authorise by scope and plan. Each route declares the scopes it needs and the API product it belongs to, which the caller’s plan must include. Object-level checks (may this tenant read order 123?) stay in the service.
  • Rate limits and quotas per key. Per-second limits with bursts to protect capacity, and monthly quotas that enforce plans and feed billing. They look alike and are built as two different systems.
  • Validate and transform requests cheaply. Size limits, schema validation from each team’s OpenAPI spec, header and path rewrites, REST to gRPC transcoding. Bodies stream by default; only small ones are buffered.
  • Protect backends and each other. Per-route timeouts, retries only where safe, circuit breakers, and load shedding by priority, so a slow service or a public traffic spike does not reach partner traffic.
  • Revoke credentials in seconds. A leaked key or a compromised app stops working on every proxy within seconds, without a lookup on every request.
  • Observe everything per key. Calls, errors and latency per key and route, for us and for the developer; sampled logs and traces by request id. All of it off the request path.
  • Self-service config for 300 teams. Teams change their own routes and policies without a ticket to the gateway team. Changes are validated, rolled out in stages and reverted in one step.
  • Out of scope. The identity service itself (login, consent, token issuance), the portal UI, invoicing, DDoS scrubbing and WAF rules, GraphQL federation and backends for frontends, and service-to-service traffic, which the mesh handles.

Non-functional requirements

  • Throughput (1 M req/s peak · 3 regions). Average ~400 k/s, ~35 B requests a day. Each region is sized for 500 k/s so that two can carry the peak when one is lost.
  • Added latency (p99 < 10 ms · p50 ~1 ms). This drives the hardest trade-off in the design. A remote call costs 5–10 ms at p99, so nothing on the hot path may call the network except the backend, and revocation speed and quota exactness have to be won some other way.
  • Availability of the data plane (99.99 %). Every API is behind the gateway, so its availability caps theirs. The control plane, the auth server and the key service may all be down without stopping traffic.
  • Revocation delay (p99 < 5 s · worst case 15 min). Pushed to every proxy in seconds; if every push path failed, a revoked token still dies when it expires.
  • Counting accuracy (billing exact · limits ±10 %). Billed usage must have no lost or double-counted request after reconciliation. Quota enforcement may lag ~20 s; rate limits are protective and may be approximate.
  • Tenant isolation (partner p99 unchanged). One key, one app or one slow backend must not move the latency or errors of anyone else, above all partners during a public spike.
  • Config safety (one team’s blast radius). A bad change can touch only its own team’s routes, reaches a few proxies for a few minutes before it is caught, and reverts in seconds.
  • Credential hygiene (hashes only · never logged). API keys are stored as hashes, credentials are never logged or forwarded, and identity headers sent by clients are stripped.

Back-of-envelope estimates

  • Requests per day: ~35 B · 1 M/s peak. Peak 1 M/s, average about 400 k/s (peaks run ~2.5× the daily mean). 400 k × 86,400 ≈ 35 B requests a day. Every one is authenticated, limited, counted and logged, so the cost of each check per request is what the design is about.
  • Gateway proxies: ~25 per region · run 30 × 3. Three regions, each sized so two can carry the peak: 1 M ÷ 2 = 500 k/s per region. At 20 k req/s per proxy (32 cores at ~60 %, TLS mostly resumed, with auth, limits, validation and logging) that is 25. Run 30 per region, 90 in all: 12 in the partner pool and 18 in the public pool.
  • CPU per request for the checks: ~0.1 ms. Route match ~10 µs, an ES256 JWT signature check ~50 µs (an API key hash and lookup is ~2 µs), token buckets ~1 µs, validating a 2 KB body ~50 µs: about 0.1 ms. At 1 M/s the JWT checks alone are ~50 cores, under 2 % of the fleet’s 2,880. A network call costs ~0.5 ms at p50 and 5–10 ms at p99, so one remote auth or quota call per request would spend the whole p99 budget.
  • API key table: 100 k keys × ~200 B ≈ 20 MB. A key record is the SHA-256 hash (32 B) plus key id, app, tenant, plan, scopes, priority and status: ~200 B. 100 k × 200 B = 20 MB, small enough to push the whole table to every proxy and never look a key up over the network. 1 M keys would still be only 200 MB; around 100 M, a cache in front of a lookup service becomes the better design.
  • Revocation deny list: ~100 live entries. Access tokens live 15 min, so a revoked token only needs denying until it would have expired anyway. At ~10 k revocations a day: 10 k ÷ 96 fifteen-minute windows ≈ 100 live entries. Revoking a whole app or user is one entry, (client_id or sub, issued before T). A few KB, pushed to every proxy.
  • Token refresh load: ~11 k/s at 15 min. 10 M active user sessions, each refreshing once per token lifetime: 10 M ÷ 900 s ≈ 11 k/s at the authorization server. 5-minute tokens triple it to 33 k/s; 1-hour tokens cut it to 2.8 k/s but leave a revoked token working for up to an hour if a push is missed. Token lifetime is where revocation delay is traded against auth server load.
  • Usage data: ~10 TB/day raw · ~250 GB/day aggregated. A 300 B access record × 400 k/s ≈ 120 MB/s ≈ 10 TB/day. Billing and quotas need counts, not records, so each proxy sums per (key, route, status class) over 10 s windows: ~3 k rows per window per proxy, ~27 k rows/s across 90 proxies, ~250 GB a day. Full records are kept only for errors and a 1 % sample of successes.
  • Gateway config: ~5 k routes · ~10 MB · ~200 changes/day. 300 services × ~17 operations ≈ 5 k routes. ~1 KB of match, auth, limits, timeouts and retries per route plus ~1 KB of compiled schema ≈ 10 MB, plus the 20 MB key table. One team’s change is ~30 KB, so a delta to 90 proxies is ~3 MB, and a booting proxy loads all 30 MB in about a second. Pushing config is not the hard part; deciding what is safe to push is.

Components

  • API clients (partner servers · apps): Partner servers calling with API keys (about 40 % of traffic), third-party apps calling on behalf of our users with OAuth access tokens, and our own mobile apps. Partners use their own hostname, which lands them on a separate proxy pool. Every response carries one error format, rate-limit and quota headers, and a request id, which is what clients quote when something goes wrong.
  • Developer portal (keys · plans · usage): Where developers register apps, create and rotate API keys, pick a plan and watch their usage, errors and quota. A new key is shown exactly once, and only after every proxy has acknowledged it, so the first call with it never fails. Usage pages read the usage store, not the gateway.
  • L4 edge (anycast · one VIP per pool): The L4 tier from the load balancer design: anycast addresses, ECMP and consistent hashing of connections onto gateway proxies. Each pool has its own VIP, so partner and public traffic are separated before anything is decrypted. It is also the cheapest place to drop a flood, and the control plane can push temporary drop rules for one abusive source to it.
  • Gateway proxies (Envoy-style · stateless): 90 proxies, 30 per region in two pools (12 partner, 18 public), each handling ~20 k requests a second. They terminate TLS, verify JWTs and API keys, check scopes and plans, take tokens from local buckets, validate small bodies, route by host, method, path and version, and forward with identity headers over mTLS. Every decision uses data held in memory and pushed by the control plane, so the only network hop they add is the one to the backend.
  • Backend services (300 services · mTLS): About 300 services owned by their own teams, each publishing an OpenAPI spec that the gateway compiles into routes and validators. They receive the verified identity in x-auth-* headers, accepted only from the gateway’s mTLS identity, and never the raw credential. Object-level authorisation, such as whether this tenant may read order 123, stays here.
  • Rate limit counters (Redis · synced every 500 ms): A Redis cluster per region holding each key’s global token-bucket state: the component from the rate limiter design, used in its local mode. Proxies never call it per request; every 500 ms each proxy makes one pipelined call that reports what it consumed and returns its share of every limit, weighted by its share of that key’s traffic. If it is unreachable, proxies keep their last shares.
  • Authorization server (OAuth 2.0 · ES256 JWTs): The identity team’s OAuth 2.0 and OpenID Connect server; the gateway consumes it but does not own it. It issues 15-minute access tokens signed with ES256, publishes each new signing key a day before using it, and sends revocations (one token, a user or a whole app) into the control plane’s fast lane. Short tokens are the revocation backstop, and they cost it ~11 k refreshes a second.
  • API key service (issue · rotate · revoke): Generates keys of the form ak_live_ plus 32 random base62 characters and a checksum, stores only their SHA-256 hash, and lets a key have a successor so partners rotate without downtime. Every change (a new key, a revocation, a plan or scope change) is committed, then published to the control plane’s fast lane. It also takes leaked-key reports from secret scanners and revokes those keys automatically.
  • Key and plan store (Postgres · hashes only): Source of truth for apps, tenants, plans, scopes and API keys: about 100 k keys at ~200 B each, 20 MB. The raw key is never stored, so a leaked backup is useless to an attacker. Proxies hold a pushed copy of the key table, so this store is off the request path entirely.
  • Gateway control plane (compile · stage · fast lane): Compiles every team’s routes, policies and schemas, plus the platform’s plans, into one versioned snapshot, rejects anything that fails validation, and rolls changes out in stages, reverting on regressions. A second, unstaged fast lane carries signing keys, key changes, deny entries and over-quota flags to every proxy in seconds. It is never on the request path: if it disappears, proxies serve their last accepted snapshot.
  • Route and policy repo (one directory per team): Versioned config with one directory per service: its routes, limits, timeouts, retry rules and OpenAPI schemas, changed through that team’s own reviewed commits. The platform owns plans, scopes, global policy and the registry of which team owns which host and path prefix. About 5 k routes and ~200 changes a day.
  • Usage stream (Kafka · 10 s windows): Kafka topics keyed by key id, carrying each proxy’s 10 s usage windows tagged with (proxy, sequence), per-route metrics, sampled access logs (every error, 1 % of successes) and sampled spans. Proxies spool to local disk first, so a slow Kafka delays usage but never loses it. Nothing on the request path waits for it.
  • Quota service (durable period totals): Consumes usage windows, drops duplicate batches by (proxy, sequence), and keeps per-key, per-product totals for the billing period in a durable store. It warns developers at 80 % and 100 % and, for hard-capped plans, publishes an over-quota flag through the fast lane. Its totals trail reality by ~15 s, which is why enforcement is eventual and billing is reconciled.
  • Usage store (columnar · per key per minute): A columnar store (ClickHouse or Druid style) holding calls, status classes and latency per key, route and minute for 13 months, plus the raw per-proxy windows for reconciliation. It serves the portal’s analytics, support’s investigations and the billing export: ~250 GB a day of aggregated rows instead of 10 TB of request logs.
  • Usage reconciler (hourly + at period close): Recounts each period from the per-proxy windows, checks that every proxy’s sequence numbers are contiguous, asks proxies to re-send missing windows from their spools, and compares totals with each proxy’s own request counters. Only then is a period’s usage final and exported to billing. Differences go back to the quota service as corrections.

User flows

  1. A call is authenticated, limited and routed. The hot path. Every check happens in the proxy’s memory, so the only network hop the gateway adds is the one to the backend, and the backend receives a verified identity instead of a credential.
    1. A third-party app holds a 15-minute access token for one of our users, issued by the authorization server. The user consented through the authorization code flow with PKCE, which the identity service owns, and the app refreshes the token every 15 minutes. The token is a JWT signed with ES256 that carries the user, the app’s client_id and the scopes granted. Fifteen minutes is the revocation backstop if every push fails.
    2. The request reaches a gateway proxy in the public pool through the L4 edge. The hostname picks the pool: api.example.com is served by the public pool and partners.example.com by the partner pool, each behind its own VIP, so the L4 tier keeps them apart without looking inside TLS. The proxy terminates TLS, as the L7 tier for these hostnames, and deletes any x-auth-* headers the client sent, so nobody can claim an identity the gateway did not verify.
    3. The proxy verifies the token locally and checks the deny list. Algorithm pinned to ES256, the kid looked up in the signing keys the control plane pushed, signature checked in ~50 µs, then issuer, audience and expiry with 30 s of clock leeway, then the deny list by jti, sub and client_id. An API key instead is checked for format and checksum, hashed with SHA-256 and looked up in the pushed key table in ~2 µs. No network call either way.
    4. The proxy matches the route and checks the scope and the plan. Routes are compiled into a lookup by host, then method and longest path prefix, then headers such as a version header: ~10 µs. This route needs orders:write and belongs to the orders API product, which the app’s plan must include. A missing scope is a 403 insufficient_scope; whether this user may order for this account is the orders service’s decision, not the gateway’s.
    5. The proxy takes tokens from its local buckets and counts the call. Three buckets, each a microsecond in memory: per (user, app), per app across all its users, and the plan’s write class. None is checked over the network; the proxy’s share of each limit is refreshed in the background. The call is also added to the current 10 s usage window for (app, orders-api).
    6. Every 500 ms the proxy reports what it consumed and gets back its share of each limit. One pipelined call carries this proxy’s consumption for every key it saw since the last sync and returns each key’s global remaining tokens and this proxy’s share, weighted by its recent fraction of that key’s traffic. That is the rate limiter design in its local mode, chosen because the gateway cannot afford a network call per request. If Redis is unreachable, proxies keep their last shares.
    7. The proxy validates the body, rewrites the path and forwards with identity headers and a deadline. The 1.8 KB body is under the route’s 64 KB limit, so it is buffered and checked against the compiled CreateOrder schema in ~50 µs. The request goes on a pooled HTTP/2 connection with mTLS, and backends accept x-auth-* headers only from the gateway’s mTLS identity. The deadline header lets the service give up when the 2 s route timeout is nearly spent.
    8. The proxy returns the response with limit headers and ships the record off the hot path. Headers report the most restrictive bucket. The call is in the 10 s usage window, which is spooled to disk and shipped when it closes; errors are always logged in full and successes at 1 %. Spans are sampled at 1 % unless this key is flagged for debugging, when every request is traced.
  2. A team canaries a new backend version. The self-service path. A team changes only its own routes, the control plane compiles and validates the change with everyone else’s, and the rollout is staged so a mistake reaches a few proxies for a few minutes. Partners are assigned to the canary by key, so each sees one version consistently.
    1. The orders team merges a change that sends 5 % of /v2/orders to a new backend version. The change lives in the orders directory and is reviewed by the orders team alone; nobody on the gateway team is in the loop. The split is by hash of the key or app, not per request, so one partner does not see responses alternate between versions. Partners on an exclusion list never join a canary.
    2. The control plane compiles every team’s config into a new snapshot and runs the validators. Checks: the schema; the orders team owns api.example.com/v2/orders; no other route overlaps; auth is required unless the route is marked public with a security review; the scopes exist; the timeout is within 30 s; regexes are RE2, so matching stays linear; the new cluster exists and has healthy endpoints. A failure stops here with a message to the team, and nothing reaches a proxy.
    3. Canary proxies load the new snapshot first and run synthetic checks. Two proxies per region, serving synthetic traffic and a little real traffic, get the delta first. A proxy that cannot load it NACKs and keeps the previous version, which stops the rollout. The push mechanics are the load balancer’s control plane; what is new here is that each stage watches only the routes that changed.
    4. The rollout widens to 10 % of proxies, one region, then all, comparing the orders routes on the new and old versions. At each stage the control plane compares the 5xx rate and p99 of the orders routes on proxies running 18233 against those still on 18232, from the per-route metrics in the usage stream. A regression re-pushes 18232 automatically. The whole rollout takes about 15 minutes.
    5. Each proxy sends a caller to the canary when the hash of its key or app falls in the first 5 of 100 buckets. Every proxy computes the same hash, so the assignment holds whichever proxy a request lands on. Metrics are tagged with the backend version, so the team compares v2-next against v2 on the same mix of partners.
    6. The team steps the split to 25, 50 and 100 %, each a new version that takes the same staged path. Going forward is staged; going back is not. A rollback re-pushes a version that already ran everywhere, so it goes to every proxy at once and takes seconds.
    7. If the control plane fails mid-rollout, proxies keep serving the version they last accepted. Proxies keep their snapshot in memory and on local disk, so a restart comes back on the same version. The rollout pauses with some proxies on the old version and some on the new, both of which passed validation. Traffic is unaffected; only further changes wait.
  3. Search slows down at evening peak. The scale-breaking case. One backend slows under peak load, clients start retrying, and the public pool runs hot. Timeouts, per-cluster caps, retry budgets, a breaker and priority shedding each take part of the blow, and partner traffic never notices.
    1. At peak, a bad search deploy pushes search latency from 80 ms to 3 s. Each public-pool proxy sends search ~2 k requests a second. At 80 ms that is ~160 in flight; at 3 s it would be 6 k per proxy, each holding buffers and an upstream stream on a proxy that also serves orders, catalogue and payments. Without bounds, one slow service would take every route on the proxy down with it.
    2. The route’s timeout cuts each search off at 1 s and tells search to stop working on it. The proxy sends the remaining deadline with the request, so search can abandon work for callers that are already gone instead of finishing it. The client gets a 504 with the request id, and since a search is a GET it may retry with backoff.
    3. Search’s in-flight cap fills, and extra searches fail in microseconds. Each proxy allows a cluster about 3× its normal concurrency, ~500 for search. Past that, new searches get an immediate 503 with Retry-After instead of queueing, so search can hold at most 500 requests’ worth of memory and streams on any proxy, and orders on the same proxy keep theirs.
    4. The proxy adds no retries: search’s retry budget is spent. Searches are GETs, so a retry on another host would be safe, but retries are capped at 10 % of a cluster’s requests and that budget went in the first seconds. Retrying every failure would multiply the load on search exactly when it can least take it.
    5. Badly written clients retry at once, and per-key and per-app buckets reject the worst of them locally. Public traffic climbs ~30 % as apps retry without backoff. Each key and app is held to its plan’s rate and the excess gets 429s from local buckets, but a rejection still costs TLS and header parsing, about a fifth of a full request, so the public pool runs hotter.
    6. The search circuit breaker opens, and searches fail fast for 10 s at a time. More than half of at least 100 requests in 10 s failed, so each proxy stops calling search and answers 503 at once. After 10 s it lets 5 % through: if they succeed it closes, if not it waits another 10 s. Search gets room to recover instead of meeting the full load the moment it improves.
    7. Public-pool CPU passes 75 %, and proxies shed by priority: sheddable traffic first, then free. Bulk exports, analytics and requests marked as client retries go first, then free-tier traffic, each shed fraction ramping up in steps rather than all at once. Paid traffic in the public pool is shed only if that is not enough. The order was decided with the product team in advance.
    8. The partner pool sees none of it: different hostname, different proxies. Partner traffic arrives on its own VIP at its own 12 proxies per region, so neither the retries nor the shedding reach it. Partners calling search see the open breaker like everyone else, but orders, payments and catalogue calls keep their normal latency.
  4. A leaked API key is revoked in seconds. The security failure path. A live key turns up in a public repository, and it must stop working on all 90 proxies within seconds, even though no proxy looks anything up per request. The same lane revokes OAuth tokens.
    1. A secret scanner finds a live key in a public repository and reports it. The ak_live_ prefix and checksum make our keys easy to match with few false positives, which is why GitHub’s secret scanning partner programme can find them and post them to us within minutes of a push. Reports are signed by the scanner, and the endpoint accepts nothing else.
    2. The key service hashes the key, finds its record and marks it revoked. The store only knows hashes, so the reported key is hashed with SHA-256 to find it. The revocation records who reported it and why, and the key’s owner is emailed with where it was found.
    3. The change goes into the control plane’s fast lane. The fast lane is unstaged on purpose. A revocation can only make one credential stop working, so its worst case is one wrongly blocked key, while staging it like a route change would leave a leaked key working for 15 minutes.
    4. Every proxy applies the change and acknowledges it. Typically about a second to all 90 proxies, under 5 s at p99. The key service also writes a signed snapshot of the key table that proxies poll every 60 s, so revocation still lands within a minute if the control plane’s streams are down.
    5. The next request with the key is refused with a 401 by whichever proxy it reaches. The proxy hashes the key, finds it revoked in memory and answers without calling anything. Whoever copied the key from the repository gets nothing, and so does the partner’s own integration, which is why the portal makes rotation painless.
    6. The developer creates a replacement, and the portal shows it only after every proxy has it. The key service commits the new key, pushes it through the fast lane and returns once the control plane reports every proxy has acknowledged that version, typically in 1–2 s. The first call with the new key therefore never fails. For planned rotation a key can have a successor, and both work for 24 hours.
    7. OAuth tokens are revoked through the same lane, as deny entries that expire with the tokens. One token is denied by its jti, a user or a whole app by (sub or client_id, issued before T), and each entry is dropped 15 minutes later, when every token it covers has expired anyway. That is why the list stays around 100 entries. If every push failed, the 15-minute lifetime is the backstop.
  5. A key reaches its monthly quota. The asynchronous path. Usage is counted in the proxies, shipped in windows, deduplicated and summed, and an over-quota flag comes back through the fast lane about 20 s after the line is crossed. A reconciler makes the count exact for billing.
    1. Each proxy counts calls per (key, product) in 10 s windows and spools every closed window to disk before shipping it. Counting is an in-memory increment on the hot path; everything else happens after the window closes. The spool means a slow or unavailable Kafka delays usage but never loses it, and each batch carries the proxy id and a sequence number, so duplicates and gaps can both be found later.
    2. The quota service drops duplicate batches and adds each window to the key’s total for the period. Proxies and Kafka retry, so the same batch can arrive twice; the (proxy, seq) pair makes adding it idempotent. Totals live in a durable store keyed by (key, product, period). They trail reality by ~15 s: the window, plus shipping, plus processing.
    3. The same stream is rolled up per key, route and minute into the usage store. This is what developers see in the portal and what support queries: calls, error classes and latency per key and route per minute, kept for 13 months. The per-proxy windows are kept too, for reconciliation.
    4. At 80 % the developer is warned; at 100 % on a hard-capped plan the quota service publishes an over-quota flag. Plans with billed overage are never blocked; they only accrue. For a hard cap the flag goes through the fast lane like a revocation, because it only restricts one key. It carries the period end, so proxies lift it without another message.
    5. Proxies refuse that key’s calls to that product with a 429 quota_exceeded until the period resets. The same status as a rate limit, but a different body and headers, because the client must react differently: wait a second, or stop until the period resets. From crossing 100 % to the first refusal is ~20 s, so a key at 1 k calls a second overshoots by ~20 k calls, 0.2 % of its quota; those are counted but not billed.
    6. The reconciler recounts the period from per-proxy windows and chases any gaps in the sequence numbers. Every proxy’s sequence numbers must be contiguous. A gap means a batch never arrived, so the reconciler asks that proxy to re-send it from its spool, and each proxy’s totals are compared with its own request counters. Only then is the period’s usage final and exported to billing.
    7. Any remaining difference goes back to the quota service, so live totals match the final count. Re-sent batches flow through the stream like any other and are counted once by the (proxy, seq) check. The reconciler’s corrections cover the rest, such as a window a consumer bug dropped, so a key near its cap is judged on the number it will be billed on.

Deep dives

Self-service config without a bottleneck

Three hundred teams change their routes and policies every week. How do they do it without filing a ticket with the gateway team, and without one team’s mistake breaking everyone’s API?

A gateway owned by one team becomes a queue for 300: every new endpoint, timeout change or canary waits on that team, and the usual reaction is that teams route around the gateway, losing central auth and limits. But letting 300 teams edit one shared config is how a typo in one team’s host match blackholes another team’s API on all 90 proxies at once. About 200 route changes land a day; at a 1 % bad-change rate, that is two incidents a day unless something catches them.

The way out is config with ownership. Each team owns a namespace of hosts and path prefixes and can only change routes inside it; the platform compiles every team’s files into one versioned snapshot and refuses anything that fails validation; and proxies only ever run a version that compiled and loaded. A bad change should be stopped before it ships, contained to its owner’s routes if it ships anyway, and reversible in seconds.

  • Team-owned config, compiled and validated centrally, rolled out in stages chosen
  • A central gateway team reviews and applies every change rejected
  • Each team runs its own gateway situational: a team with a protocol or latency need the shared gateway cannot meet, such as long-lived streaming, or a regulated tenant that needs its own deployment
  • Custom plugin code per team in the shared proxy situational: a few platform-reviewed plugins, or sandboxed Wasm filters with CPU and memory limits, for needs declarative config cannot express

The answer: Each service has a directory in the config repo with its routes, policies and OpenAPI schemas, owned by its team; the platform owns plans, scopes, global policy and the registry of which team owns which host and path prefix. The control plane compiles everything into one versioned snapshot and rejects a change that fails any check: schema, ownership of every host and prefix, no overlapping routes, auth on every route not explicitly marked public, scopes that exist, timeouts within the 30 s platform maximum, RE2 regexes so matching stays linear, and the compiled size. A change that compiles goes to two canary proxies per region running synthetic checks, then 10 % of proxies, one region and everywhere over ~15 minutes, comparing 5xx and latency on the changed routes between proxies on the new and old versions; a regression stops it and re-pushes the previous version. Signing keys, key changes, deny entries and quota flags take a separate fast lane with no staging, because each can only ever affect one credential. Proxies keep the last snapshot they accepted on local disk and serve it if the control plane is gone. The push mechanics (versioned deltas, ACK and NACK) are the load balancer’s control plane; Kong’s hybrid mode splits its gateway the same way, with data plane nodes that keep serving their cached config when the control plane is unreachable.

Two teams both claim /v2/orders. What happens?

The compiler refuses whichever change arrives second and names the owner of the prefix. Ownership is registered once per host and path prefix in a platform-owned file, so a new claim needs the current owner’s approval. Without this check the outcome would depend on route order, and a team could silently capture another team’s traffic, which is a security problem as well as an outage.

The control plane is down for two hours. What still works?

All traffic, on the last accepted snapshot: routes, the key table, signing keys and deny lists stay in memory and on disk. Route changes stop, which is an inconvenience. Revocation matters more, so it does not depend on the control plane alone: the key service and the authorization server each write a signed snapshot of their state to object storage that proxies poll every 60 s, so a leaked key still dies within about a minute.

A change passes validation but is wrong: the team points /v2/orders at its staging cluster. How far does it get?

To the canary proxies and the 10 % stage, where synthetic checks and the 5xx comparison on that team’s routes catch it within minutes, and the rollout reverts itself. Even if it reached every proxy, only the orders routes would be affected, because the change could only touch the orders namespace. Rollback re-pushes the previous version to every proxy at once: seconds, not another 15-minute rollout.

Why not let teams write plugins? Kong and Envoy both support them.

Because a shared proxy runs every team’s code in one process: one slow regex or memory leak stalls traffic for all 300 services. Declarative config covers almost everything teams ask for: routes, auth, limits, timeouts, header and path rewrites, schema validation. For the rest the platform offers a few reviewed plugins, or Wasm filters with CPU and memory limits on routes that opt in, never arbitrary code merged 200 times a day.

How do you know a proxy is running the config it should?

Every proxy reports the snapshot it runs, ACKs each version it loaded and NACKs any it rejected. Dashboards show versions per proxy and the time from merge to every proxy acknowledging; alert on any NACK and on any proxy several versions behind its peers. A proxy whose own stream broke while its peers are current takes itself out of rotation after a minute, but when every proxy is behind because the control plane is down, they all keep serving.

Authentication without a network call

Why not just ask the auth service whether each token or key is valid? It is the only way to know about a revocation immediately.

Every one of 1 M requests a second carries a credential. Checking each with the authorization server (token introspection) or a key service adds a round trip, ~0.5 ms at p50 and several milliseconds at p99, and makes that service a dependency of every API: 1 M requests a second of load on it, and when it is down, every API is down. But a signed token that is only checked locally keeps working after it is revoked, until it expires, and a stolen partner key has to stop working in seconds, not hours.

The way through is to separate the common case from the rare one. Validity is mostly a property of the credential itself: a JWT’s signature, issuer, audience and expiry can be checked with a public key, and an API key is valid if its hash is in a table small enough to hold in memory. Revocation is rare, about 10 k a day, so it can be pushed to every proxy as a small list instead of being asked about on every request. Then the hot path is local, and revocation speed is how fast a push reaches 90 proxies.

  • Local checks: JWTs against pushed signing keys, API keys against a pushed table of hashes, revocations pushed chosen
  • Introspect every token and look up every key rejected
  • Introspect, then cache the answer for 60 s situational: opaque tokens from an external identity provider that issues no JWTs; the gateway can exchange each for an internal token on first use (the phantom token pattern)
  • Long-lived JWTs with no revocation list rejected

The answer: Access tokens are JWTs signed with ES256 by the authorization server, with a 15-minute lifetime and iss, aud, sub, client_id, scope, exp and jti. Proxies pin the algorithm, find the kid among signing keys pushed by the control plane, verify the signature and check issuer, audience and expiry with 30 s of clock leeway; an unknown kid is a 401, never a fetch, so a forged kid cannot make proxies call anyone, and the authorization server publishes each new key a day before signing with it. API keys look like ak_live_ plus 32 random base62 characters and a checksum: a malformed key is rejected from its format alone, and the rest is hashed with SHA-256 and looked up in the pushed 20 MB key table. A fast hash is safe because the key has ~190 random bits; bcrypt would cost ~100 ms per request for nothing. Revocations go through the control plane’s fast lane, by jti for one token, by (sub or client_id, issued before T) for a user or an app, and as status changes for keys, reaching every proxy at p99 under 5 s, with a polled snapshot in object storage every 60 s as the second path. If both fail, tokens still die within 15 minutes, which is why the lifetime is short; the price is ~11 k refreshes a second at the authorization server.

Why hash the API keys at all, if the table lives in our own memory?

Because the table also lives in the key store, its backups and every proxy, and any of them can leak. With only SHA-256 hashes stored, a leaked table gives an attacker nothing they can send. The raw key exists once, when the portal shows it to the developer, and after that only its first few characters are shown so people can tell keys apart. For the same reason proxies never log the Authorization or X-Api-Key header.

A developer creates a key and calls the API one second later. Does it work?

Yes, because the portal does not hand the key over until it is live: the key service commits it, the fast lane pushes it, and the create call returns once every proxy has acknowledged that version, typically in one to two seconds. An unreachable proxy does not block it forever: after 5 s the key is returned and that proxy is marked stale, and a proxy that stays behind its peers leaves rotation. Two seconds at creation is far cheaper than a lookup path on every request.

How do you rotate the authorization server’s signing key without failing requests?

Publish the new public key a day before signing with it, so every proxy has it long before the first token carrying its kid. Switch signing to the new key, keep the old one for at least the 15-minute token lifetime, then remove it. Because unknown kids are rejected rather than fetched, the order matters: announce, wait, sign, wait, retire.

A partner’s plan is downgraded and loses the orders API. How fast does that take effect?

Entitlements tied to an API key or an app’s plan are in the pushed key table, so a plan change reaches every proxy in seconds. Scopes inside an OAuth token stay as issued until the token expires, at most 15 minutes, unless we also push an (client_id, issued before T) entry, which forces a refresh that returns the new scopes. For a downgrade 15 minutes is usually fine; for an incident we push the entry.

At 10 M or 100 M keys, does pushing the table still work?

1 M keys is 200 MB per proxy, still fine to push with no lookups. Around 10 M to 100 M, typically when every end user gets a personal token, memory and boot time push towards a cache: proxies keep recently used keys for 10 minutes, look up misses at a regional key service with a negative cache for unknown hashes, and keep the pushed change events to evict revoked keys. The checksum in the key format matters more then, because it filters random keys before they cost a lookup.

Quotas and rate limits are different systems

A plan says 100 requests a second and 10 million a month. Why not enforce both with the same counters?

They look alike, a count per key against a limit, but they answer different questions. The rate limit protects capacity over the next second: it decides in microseconds on every request, it is fine to be 10 % off, and losing its state costs a second of leniency. The monthly quota is a contract and an invoice: the count has to be exact by the end of the month, survive proxy crashes, Redis failovers and deploys, and be explainable to a customer disputing a bill. Enforcing the quota can lag, though: 20 s of overshoot at 1 k requests a second is 20 k calls, 0.2 % of a 10 M quota.

So they need opposite things. Rate limits need speed and tolerate loss; quotas need durability and exactness and tolerate delay. One mechanism forced to do both is either a durable write per request (1 M a second, milliseconds each) or a month of billing data in a cache that a failover can wipe. AWS API Gateway’s usage plans separate them too, a token-bucket throttle and a per-period quota, and AWS documents both as best effort rather than hard limits.

  • Rate limits as synced local buckets; quotas counted from the usage stream, enforced by pushed flags, reconciled chosen
  • One shared Redis counter per key for both rejected
  • A durable decrement of the quota on every request rejected
  • Quota leases: each proxy takes a block of the remaining quota and spends it locally situational: prepaid credits or hard caps where any overshoot costs real money, such as calls a downstream provider bills us for

The answer: Rate limits reuse the rate limiter design in its local mode: each proxy keeps token buckets per (key, route class), plus a ceiling per OAuth app, and every 500 ms makes one pipelined call to a regional Redis that reports what it consumed and returns its share of each limit, weighted by its recent share of that key’s traffic. A key new to a proxy starts with an even share; if Redis is unreachable, proxies keep their last shares, which fails open within a bound. Quotas are counted from usage, never from limiter state: each proxy sums calls per (key, product) over 10 s windows, appends each closed window to a local disk spool and ships it to Kafka with (proxy id, sequence number). The quota service drops duplicate batches by that pair, keeps per-key totals for the period in a durable store, warns at 80 % and 100 %, and for hard-capped plans publishes an over-quota flag that proxies enforce with a 429 quota_exceeded until the period resets. A reconciler recounts each period from the windows, chases sequence gaps from the spools, compares against each proxy’s own counters and finalises billable usage; the ~0.2 % overshoot on a hard cap is counted but not billed.

A proxy crashes with 8 seconds of uncounted usage in memory. Is that money lost?

Up to 10 s of one proxy’s counts, at most ~200 k calls at peak against 35 B a day: real but small, and bounded by the window. Windows that had already closed are on the spool and are shipped when the proxy comes back, or the reconciler sees the gap and pulls them. If the bound mattered more, 1 s windows would cut it tenfold at ten times the rows. We do not bill what we cannot count, so the error favours the customer.

Why ±10 % on rate limits? Can you make it tighter?

The error comes from shares being up to 500 ms stale: when a key’s traffic moves between proxies, some hold share they are not using while others run out. Syncing every 100 ms cuts the error at five times the Redis traffic, and the rate limiter’s exact mode, one Redis call per request, removes it at the cost of a network call on the hot path. For a protective limit, 10 % is the right trade.

The billing period ends at midnight UTC. What happens at 00:00:00?

The quota service rolls each key to a new period and clears over-quota flags, and the flags already carry their own expiry, so proxies lift them even if that message is late. A window that straddles midnight is split by request timestamp, so each call lands in exactly one period. Plans anchored to a customer’s billing date work the same way with a per-key period start; the dangerous version is computing periods in local time, which creates 23- and 25-hour days.

A customer disputes their invoice: “we never made 12 million calls”. How do you answer?

From the usage store: calls per day, per route and per key, with status classes, plus the sampled access logs for spot checks of individual requests. Because counts come from deduplicated, sequence-checked windows that the reconciler compared with each proxy’s own counters, we can show the figure is complete and not double counted. Usually the answer is a forgotten integration or a retry loop on 5xx, both visible per route.

A partner says 5 % of their calls fail. How do they, and you, see why?

The portal reads the usage store: calls, status classes and latency per key and route per minute, so the partner can see whether the failures are their 4xx or our 5xx and on which route. Individual requests are found by the request id on every response, in the sampled logs that keep every error. For a hard case, support switches on full tracing for that one key for an hour through the fast lane; otherwise spans are sampled at 1 %, which at 400 k requests a second is still 4 k traces a second.

When a backend slows down

Search latency goes from 80 ms to 3 seconds. Why doesn’t that take down the orders API too, on the same proxies?

A proxy holds a request open for as long as the backend takes. At 2 k searches a second per proxy, 80 ms means ~160 searches in flight; at 3 s it is 6 k, each holding buffers and an upstream stream, and the proxy’s memory, connections and event loop are shared with every other route. Clients make it worse: on timeouts they retry, and a gateway that also retries every failure multiplies the load on a backend that is already drowning.

The answer is to bound everything per backend: how long a request may take, how many may be in flight, how many retries may be added, and when to stop calling at all. Then a slow service fails fast on its own routes and everything else on the proxy is untouched. Which failures are safe to retry depends on the method, so the gateway has to know which calls are idempotent.

  • Per-route timeouts and deadlines, per-cluster in-flight caps, idempotent-only retries within a 10 % budget, circuit breakers chosen
  • Retry every failure up to 3 times rejected
  • Queue requests at the gateway until the backend recovers rejected: never at a synchronous gateway; slow work belongs behind a 202 and an operation id
  • Adaptive concurrency limits per cluster situational: clusters whose healthy latency varies widely, where a fixed cap is either too tight or too loose; Envoy has an adaptive concurrency filter and Netflix open-sourced a concurrency-limits library

The answer: Every route has a timeout set by its team within a 30 s platform maximum, 1 s for search and 2 s for creating an order, and the proxy sends the remaining deadline in a header so the backend can abandon work the caller has stopped waiting for. Each cluster has a per-proxy in-flight cap of about three times its normal concurrency (requests a second × typical latency), ~500 for search, and requests beyond it get an immediate 503 with Retry-After, so search can never hold the memory and connections that orders needs. Retries happen once, on a different host, only for connect failures, resets before any response and 503s that say the request was not processed, and only for GET, HEAD, PUT and DELETE, or a POST with an Idempotency-Key on a route that declares its service honours one; retries are capped at 10 % of a cluster’s requests. A circuit breaker per cluster on each proxy opens when more than half of at least 100 requests in 10 s fail or time out, answers 503 at once for 10 s, then lets 5 % through to probe. Ejecting single bad hosts inside a cluster is the load balancer’s outlier detection, reused as is.

Why does the in-flight cap matter if you already have a timeout?

A timeout bounds how long each request is held, not how many are held. At 2 k searches a second and a 1 s timeout, a dead search cluster still occupies 2 k requests per proxy at every moment, most of them doomed. The cap of ~500 turns the rest into instant 503s. Together they bound a slow cluster’s footprint on a proxy whatever happens downstream.

A partner’s POST /v2/orders times out at the gateway. Did the order get created?

Maybe, and the gateway cannot know, which is exactly why it did not retry. It returns a 504 with the request id. The partner retries with the same Idempotency-Key, and the orders service either returns the original result or creates the order once. Only routes whose service implements idempotency keys let the gateway retry a POST itself, and even then only for failures before any response.

The breaker on search opens. What does a client see, and how does it close?

A 503 with Retry-After and an error code saying the service is unavailable, in under a millisecond, from every proxy whose breaker opened. Each proxy decides on its own, so proxies that still see search succeeding keep sending to it. After 10 s a breaker lets 5 % of requests through; if they succeed it closes, if not it stays open another 10 s, so search does not meet the full load the instant it recovers.

Client, gateway and service all retry. How do you stop retries multiplying?

Retry at one layer per hop, and budget it. The gateway retries at most once and within 10 % of traffic, services do the same through their mesh, and our SDKs back off with jitter and honour Retry-After. The gateway marks a retried request with a header so the backend does not retry it again, and it counts client retries by Idempotency-Key, which is how we spot a client library with a bad retry loop.

How do you choose a route’s timeout?

From that route’s own latency, not a default: a few times its p99.9, within the platform maximum. Search at a p99.9 of 300 ms gets 1 s; an export that takes minutes is not a synchronous route at all and returns 202 with an operation id. The control plane warns when a route’s timeout is far above its observed latency, because a 30 s timeout on an 80 ms route is a 30 s pile-up waiting to happen.

Noisy neighbours and shedding by priority

A free-tier developer’s batch job loops and sends 200,000 requests a second. How do your paying partners not notice?

Per-key rate limits keep the key from reaching any backend, but rejecting still costs: every request is TLS to decrypt and headers to parse before the proxy knows whose it is, about a fifth of a full request. 200 k rejections a second is the work of ~40 k real requests, two public-pool proxies’ worth, on proxies shared with everyone else. And overload is not only one key: a slow backend, or many keys spiking together each under its own limit, can exceed capacity with no limit broken.

Isolation therefore comes in layers: limits per key and per app so one credential has a ceiling, separate proxy pools so partner traffic never shares a proxy with the open internet, and a priority on every request so that under overload the least important traffic goes first and partners last. Stripe has written about keeping capacity for critical requests by shedding non-critical ones first, and Netflix about prioritised load shedding in Zuul.

  • Per-key and per-app limits, separate partner and public pools, priority classes shed in order, edge drops for floods chosen
  • Per-key rate limits only rejected
  • A dedicated gateway deployment per large tenant situational: contracts or regulation that require a tenant’s traffic to be physically isolated, or a few tenants big enough to justify their own fleet
  • Autoscale the proxies rejected

The answer: Limits per key, plus a ceiling per OAuth app (client_id), because a popular app’s users each have their own token but share one bug. Partners and enterprise plans use their own hostname on a separate pool, 12 proxies per region against 18 for the public pool, so public traffic never reaches partner proxies. Every request gets a priority from its plan and route: critical (partner and enterprise writes), standard (paid), free, and sheddable (bulk exports, analytics, requests marked as retries). When a pool’s CPU passes 75 % or a backend cluster signals overload, proxies shed sheddable traffic first, then free, then standard, with a 503 and Retry-After, ramping the shed fraction rather than flipping it. A key rejected more than 90 % of the time for a minute at over 10 k requests a second is reported to the control plane, which pushes a 15-minute drop rule for its source addresses to the L4 edge, where dropping a packet costs almost nothing; that is for floods from a few servers, never for an app whose traffic comes from millions of phones behind shared carrier addresses. Expensive routes such as search take several tokens per call, so a heavy endpoint cannot hide inside a request count.

Why separate pools by hostname rather than by API key?

Because the L4 edge in front of the proxies sees addresses and ports, not keys, and the key is inside TLS. A hostname with its own VIP is the earliest point where partner traffic can be told apart. Partners get the partner hostname at onboarding, and the public hostname refuses partner keys after a migration period, so nobody ends up on the wrong pool.

Isn’t shedding free-tier traffic just breaking the product for free users?

Only during overload, and the alternative is worse: without priorities every request slows down together until everything times out, free and paid alike. Shedding part of the free traffic for a few minutes keeps the product working for everyone else and keeps free users’ retries from deepening the hole. The order is a product decision written down in advance, not improvised in an incident.

How does the gateway know a backend is overloaded before it starts timing out?

Three signals: the cluster’s in-flight count approaching its cap, latency rising against its normal p99, and the backend saying so with a 503 and an overload header when its own concurrency limiter fires. Any of them starts priority shedding for that cluster’s routes only, so an overloaded search sheds free search traffic without touching orders.

A critical-class partner spikes within its limits. Can it hurt other partners?

Within the partner pool, yes, if the spike is big enough, which is why partner limits are sized against pool capacity: the largest few partners’ limits together must fit in the pool’s headroom. Beyond that, per-cluster caps protect each backend, and the very largest partners can get their own priority within the critical class or, by contract, their own deployment.

How would you test that partner traffic survives a public spike?

Game days. In a staging copy, replay recorded public traffic at several times its peak; in production, move a region’s public traffic onto fewer proxies until shedding starts. Partner p99 and errors should not move, and the shed counters should climb class by class in the right order. If partner latency moves, something is shared that should not be.

Validating and transforming bodies

Teams want the gateway to validate every request against their schema and to reshape payloads for old API versions. How much body work should a gateway do?

Most gateway work looks at headers: a few hundred bytes, parsed in microseconds. Bodies are different. To validate or transform a JSON body the proxy has to buffer all of it, and a proxy has ~1,000 requests in flight at once. With 10 KB bodies that is 10 MB; let 100 clients upload 100 MB files through a buffering route and it is 10 GB, and the proxy dies for every route on it. Transformation is also where business logic creeps in: mapping v1 fields onto v2 is a statement about what the fields mean.

So bodies stream through by default, and the proxy only does what it can bound: size limits enforced while streaming, schema validation on routes that opt in and only under a buffer limit, and rewrites of headers and paths, which cost nothing. Anything that needs to understand the payload belongs in the service or in a backend for frontend behind the gateway.

  • Stream by default; validate small bodies on routes that opt in; rewrite headers, paths and protocols only chosen
  • Full transformation templates in the gateway situational: a managed gateway in front of systems you cannot change, such as a legacy SOAP backend; AWS API Gateway’s REST APIs use mapping templates for this
  • No body inspection at the gateway situational: streaming or very high-throughput routes where the service validates anyway and every microsecond counts
  • Buffer and validate every body rejected

The answer: Bodies stream by default under a size limit per route, 64 KB for most JSON routes and more only where declared, enforced from Content-Length and again by counting bytes as they arrive, so an oversized upload is cut off with a 413 without being buffered. Routes that opt in get JSON Schema validation compiled from the team’s OpenAPI spec when the snapshot is built, run only when the body fits under the route’s 1 MB buffer limit; a 2 KB body validates in ~50 µs and a failure returns a 400 naming the bad fields. Transformations are limited to what needs no understanding of the payload: header and path rewrites (/v2/orders to /orders), identity and deadline headers, compression, and JSON to gRPC transcoding for gRPC backends, which Envoy has a filter for. Responses always stream. Mapping old API versions onto new ones stays in the service: Stripe, for example, pins each account to a dated API version and keeps the per-version changes in its API code, not at its edge.

Why validate at the gateway at all if the service validates too?

To reject garbage before it costs a backend hop, and to give every API the same 400 format with field-level errors, generated from the same OpenAPI spec as the docs. It is a filter, not a guarantee: services still validate, because they are also called from inside. Routes opt in, so a team with a hot, simple endpoint can skip it.

A client sends Content-Length: 1000 and then streams 10 GB.

The proxy counts bytes as they arrive and resets the stream at the route’s limit, whatever the header said; with chunked encoding there may be no header at all. The client gets a 413 if it is still listening, and the connection is closed. Header checks are a fast path, the byte count is the real limit.

How does the gateway serve /v1 and /v2 of the same API?

As separate routes, usually to separate clusters, or to one service with the version passed through, owned by the same team. The gateway routes by path prefix, or by a version header for APIs with dated versions, and adds a Deprecation header with a sunset date to v1 responses. Usage per key per version from the usage store tells the team which partners still call v1 before they turn it off.

A response is 50 MB. Does the proxy buffer it?

No. Responses stream to the client as the backend sends them, with flow control, so memory per request stays at a few buffers. Anything that would need the whole response, such as rewriting its fields, is not done at the gateway, and compression streams too.

The theory behind it

  • API gateways and backends for frontends: What an API gateway does at the edge of a system: routing, authentication, rate limiting, request transformation and aggregation, the backend-for-frontend pattern, GraphQL gateways, and how to keep the gateway from becoming a bottleneck or a monolith.
  • JWT vs session cookies: Two ways to keep users logged in: server-side sessions with cookies versus self-contained JSON Web Tokens. How each works, revocation, size, security pitfalls, refresh tokens, storage in browsers and mobile apps, and the hybrid most large systems use.
  • Rate limiting and resilience: Token bucket, leaky bucket, fixed and sliding windows, distributed rate limiters, plus the resilience toolkit: timeouts, retries with backoff and jitter, circuit breakers, bulkheads, load shedding and graceful degradation.

Related

Report an issue

Something wrong with this question? A number that does not add up, a step whose answer key looks off, a typo. It goes straight to the author with the page you are on.

Watch a strong candidate answer thisTurn by turn, with the design drawn as they talkWalk through →Mock interview