Putting an API gateway at the front door
Open in the interactive app: practice, quiz, mock interview →In one minute
- An API gateway is a programmable reverse proxy at the edge of your services: it terminates client connections, decides who the caller is, picks an upstream, and enforces the rules every API should obey.
- Every gateway product splits into a data plane (the proxies that touch every request) and a control plane (the store and API that compile routes, keys and policies and push them to the proxies).
- The gateway's real value is a uniform edge contract: one place that strips spoofed headers, attaches a verified identity, sets a deadline, and shifts traffic between versions.
- Its main risks are the same as any shared choke point: a bad config push breaks every API at once, and a retry or timeout policy set wrong multiplies load during an incident.
How it works
Data plane and control plane
The data plane is a fleet of identical proxy nodes (Envoy, NGINX, HAProxy, or a managed service's hidden fleet). Each node holds a compiled copy of the configuration in memory and never asks anyone a question on the hot path if it can avoid it. The control plane owns the source of truth: routes, upstream clusters, API keys, plugin settings. Kong stores this in Postgres or declarative YAML; Envoy-based gateways such as Envoy Gateway, Contour and Istio's ingress translate Kubernetes objects into Envoy config and stream it out.
The two halves fail differently. If the control plane dies, proxies keep serving the last good config: you lose the ability to change things, not to serve. If the data plane dies, everything stops.
The configuration model
A GatewayClass names the implementation, owned by the platform team. A Gateway declares listeners (ports, hostnames, TLS certificates), owned by whoever runs the edge. An HTTPRoute attaches to a Gateway and says which paths, headers and methods go to which backends with what weights, and it is owned by the service team. Platform teams own the door; product teams own the rooms behind it. Gateway, GatewayClass and HTTPRoute are stable, and a GRPCRoute does the same job for gRPC (Gateway API docs). This model is now the default path: the community Ingress NGINX controller was retired in March 2026 with no further releases or security fixes, and the Kubernetes project points users to Gateway API (Kubernetes blog).
The edge contract
Azure's architecture guide splits gateway work into three patterns: routing one endpoint to many services, aggregation of several backend calls into one response, and offloading cross-cutting work such as TLS, authentication and throttling (Azure Architecture Center). Offloading is what makes a gateway worth having. What it gives you, beyond routing, is a set of promises every service can rely on:
- Header hygiene. Anything the gateway will assert on the caller's behalf, such as
x-user-id,x-tenant-idor a client certificate subject, is deleted from the incoming request before the verified value is added. Otherwise any client can forge an identity by sending the header itself. - Identity attachment. The gateway validates the credential (a JWT signature against cached keys, an API key lookup, an mTLS certificate) and forwards a small verified identity, so services do not each parse tokens.
- Deadline propagation. The gateway picks a total time budget for the request and forwards the remaining budget, so services stop working on requests the client has already given up on.
- Traffic shifting. Weighted routes send a stable slice of users to a canary. The slice is chosen by hashing a stable key, usually the user or session, so one user does not bounce between versions on every click.
The problem shapes
A gateway is the overload shape (it must shed and limit load before it reaches services) combined with trust but verify (it turns an untrusted request into a verified one). The same shape appears in an LLM gateway that fronts several model providers: one door, a verified tenant, a token budget, and a weighted route to a cheaper model for a slice of traffic.
Down the stack
Layer 1The contract
For each request the gateway promises: a request whose credential fails verification never reaches an upstream; internal identity headers seen by an upstream were set by the gateway, never by the client; the upstream receives a deadline no larger than the route's configured timeout; the same user hashes to the same side of a weighted split while the weight is unchanged; and the request either gets an upstream response or a gateway-generated status (401, 404, 429, 502, 503, 504) within the route timeout. For configuration the promise is weaker: a change becomes visible on each node within seconds, nodes may disagree during that window, and a config that a node rejects leaves that node on its previous version.
Layer 2The mechanism
Envoy receives config through xDS, a set of gRPC streams: LDS for listeners, RDS for route tables, CDS for clusters, EDS for endpoints. Each DiscoveryResponse carries a version_info and a nonce. The proxy replies with a DiscoveryRequest echoing the nonce; if it accepted the update it echoes the new version (an ACK), and if validation failed it repeats the old version with an error_detail (a NACK). That is how a control plane learns that half the fleet rejected a route. An ACK only means the resources passed validation, not that traffic is already flowing through them (Envoy xDS protocol). Delta xDS sends only changed resources, which matters once you have tens of thousands of routes.
On the request path, the HTTP connection manager runs filters in order, typically jwt_authn, ext_authz, local_ratelimit, then router. The router enforces a route timeout (15 seconds by default, and it covers all retries) and optional per_try_timeout (Envoy route API), and tells the upstream its budget with the x-envoy-expected-rq-timeout-ms header on internal requests (Envoy router filter). Retries are capped by the cluster's circuit breaker (3 concurrent retries by default) or by an optional retry budget, whose defaults let retries be at most 20% of active requests to a cluster, with a floor of 3 concurrent retries, so a failing upstream cannot be hit with three times its normal load (Envoy circuit breakers). Weighted clusters choose a backend per request by a random draw, or by a hash of a header when you configure the policy to make it sticky.
Layer 3The machine
Envoy runs one worker thread per core, and a connection lives on one worker for its whole life, so the hot path takes no locks. New configuration is built on the main thread and published to workers through thread-local slots, so a route update is a pointer swap per worker, not a pause. Bodies are the expensive part. A gateway that buffers a request body (to verify a signature over it, or transcode it) holds it in memory; Envoy's default per_connection_buffer_limit_bytes is 1 MiB (Envoy listener API), so 5,000 concurrent 1 MiB uploads would need about 5 GB of RAM if fully buffered. Streaming avoids that, which is why body-inspecting plugins should be the exception.
Restarting a proxy without dropping connections relies on the kernel. Envoy's hot restart passes listening sockets from the old process to the new one over a Unix domain socket using SCM_RIGHTS, then the old process drains for a configurable period. NGINX does the same with a master process that forks new workers on reload while old workers finish their requests. Either way the listen socket never closes, so the kernel's accept queue keeps taking SYNs throughout.
Why it's built this way
- Every service needs the same edge rules, and implementing them in each language drifts, so the rules move into one proxy.
- That proxy is on every request, so it must answer from memory, which forces config to be compiled and pushed rather than looked up.
- Pushed config can be wrong, so the protocol carries versions and ACK or NACK, and proxies keep serving the last good version.
- A shared retry or timeout policy multiplies load across every service, since attempts multiply at each layer (Google SRE book), so retries are budgeted as a fraction of traffic rather than counted per request.
- Memory per connection is the scaling limit, so gateways stream bodies and buffer only when a plugin demands it.
Diagram
sequenceDiagram participant CP as Control plane participant GW as Gateway node participant C as Client participant S as Orders v1 participant K as Orders v2 canary CP->>GW: RDS update v42 (canary 20%) GW-->>CP: ACK v42 C->>GW: GET /orders/7, Bearer token, x-user-id spoofed GW->>GW: strip x-internal and x-user-id, verify token GW->>GW: hash(user, route) mod 100 = 88, stable GW->>S: GET /orders/7, x-internal-user, deadline 800ms S-->>GW: 200 GW-->>C: 200
Worked example
A retail API serves 30,000 requests per second at peak through 10 gateway nodes. The orders team ships v2 and asks for a 5% canary. With per-request random weighting, a user making 20 calls in a session has a 1 - 0.95^20, about 64%, chance of touching v2 at least once, and v1 and v2 disagree about a cart field. Switching to a sticky split, a hash of the user ID modulo 100 compared against 5, puts exactly the same 5% of users on v2 every time, and raising the weight to 20% only adds users; nobody who was on v2 leaves it.
Deadlines next. The mobile client times out at 2 seconds. The gateway route timeout is 1.5 seconds, and the client sends x-timeout-ms: 1200, so the gateway forwards a budget of 1,200 ms. Orders calls inventory and pricing in sequence; after 700 ms in inventory, orders knows it has 500 ms left and passes that down. Without propagation, pricing would run on its own 3 second timeout for a response nobody reads.
Retries last. During a v2 bug, 20% of orders requests fail. With a policy of 2 retries per request and no budget, failing requests triple, adding about 30,000 x 0.2 x 2 = 12,000 extra requests per second to an already sick service. With a 20% retry budget, the retries are capped at a fifth of active requests, which is roughly what the healthy fleet can absorb.
In the wild
- Google Cloud, 2025: a policy check on every API call crash-loops worldwide. A new quota-policy check in Service Control, the component that authorizes and meters Google's API requests, shipped on 29 May without a feature flag or error handling. On 12 June a policy row with blank fields replicated globally within seconds and crashed Service Control in every region, so APIs across Google Cloud and Workspace returned 503s. Tasks restarting without randomized backoff then overloaded a Spanner table, stretching recovery in us-central1 to about 2 hours 40 minutes. Google is making Service Control fail open and rolling out global data incrementally (Google Cloud).
- Cloudflare, 2019: one edge rule pushed everywhere at once. A new WAF rule with a catastrophically backtracking regex went to the whole network in seconds, because rule changes skipped staged rollout. HTTP-serving CPUs hit about 100%, and about 80% of traffic was lost for 27 minutes. Cloudflare restored a removed CPU guard, reviewed all 3,868 rules, moved toward linear-time regex engines and staged rule rollouts (Cloudflare).
- Google Cloud, 2021: a config pipeline race makes the global load balancer serve 404s. A race condition in the pipeline that pushes customer configs to Google's external proxy load balancer corrupted a file after validation had already run. Most endpoints returned 404 for about 33 minutes, taking down Cloud Run, App Engine, Cloud Functions, Apigee and Firebase routes. Google rolled back to the last good config, closed the race and added checks before configs are applied (Google Cloud).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Managed gateway (AWS API Gateway, Apigee) | No fleet to run, built-in keys, quotas and usage plans | Per-request price, payload limits, a 29-second default integration timeout that only Regional and private REST APIs can raise, at the cost of throttle quota (AWS) | Public APIs with moderate traffic and a small platform team |
| Self-hosted Envoy-based gateway | Fast, xDS-driven, rich routing and retry control | You run the control plane and the upgrade cadence | High traffic, Kubernetes, need for fine control |
| Kong or NGINX with plugins | Huge plugin ecosystem, simple to extend | Plugin code runs on the hot path; Lua and config sprawl | Many teams needing auth, keys and transforms quickly |
| Thin edge plus service mesh | Edge stays small, policy lives near services | Two systems to understand and debug | Many internal services that also need mTLS and retries |
Pitfalls in production
- Forgetting to strip identity headers. A service trusts
x-user-id, and one route bypasses the auth filter, so clients set it themselves. Strip every internal header prefix on every route by default, before any filter runs. - Timeouts that increase inward. The gateway waits 30 seconds for a service whose own timeout is 60. Make timeouts shrink as you go deeper and forward the remaining budget.
- Unbudgeted retries. Per-request retry counts turn a partial outage into a full one. Retry only idempotent methods and use a retry budget.
- Non-sticky canaries. Random per-request weighting mixes versions inside one session. Hash a stable key so each user stays on one side.
- A single config push to every node. A route with a broken regex is accepted by the control plane and NACKed or, worse, accepted by every node at once. Validate in CI, roll out to a few nodes first, and alarm on NACKs.
Leadership lens
- Write down what belongs in the gateway (identity, limits, routing, deadlines) and what does not (business rules, data joins), and review new plugins against it.
- Give the edge one owner and route ownership to service teams through self-service resources, such as
HTTPRoute, so the platform team is not a ticket queue. - Track the time from config merge to fleet-wide ACK and the NACK rate as first-class SLOs; they predict the next outage better than request latency does.
Check yourself
The control plane is down for 20 minutes. What still works and what does not?
Show answer
Proxies keep serving with their last accepted config, so traffic flows. You cannot add routes, rotate keys pushed through config, or change weights until it returns.
Why must the gateway delete
x-user-idfrom the client request even on routes that require authentication?Show answer
If the filter that sets it is skipped or misconfigured on one route, the client's own value would reach the service. Stripping first makes the safe outcome the default.
Why hash user ID and route together for a canary split rather than user ID alone?
Show answer
With the route in the hash, the same users are not the canary population for every service at once, so one unlucky group does not absorb every experiment.
How does a retry budget differ from a retry count?
Show answer
A count limits retries per request, so total retries grow with failures. A budget caps retries as a fraction of current traffic, so retry load stays bounded however many requests fail.
Sources and further study
Primary
- Envoy xDS protocol: versions, nonces, ACK and NACK, and delta updates between control plane and proxies.
- Ingress NGINX retirement (Kubernetes blog, 2025): why the community controller ended in March 2026 and where users should move.
- Amazon API Gateway: integration timeouts beyond 29 seconds (2024): the limit change and its throttle-quota trade-off.
Systems docs
- Envoy circuit breakers:
max_retries, retry budgets and their defaults. - Envoy router filter: timeout and retry headers the gateway sets and honours.
Books
- Understanding Distributed Systems (2nd ed.), Roberto Vitillo, chapter "Control planes and data planes": why the config path and the request path should fail independently.
- Release It! (2nd ed.), Michael Nygard, chapter 10 "Control Plane": the platform that configures production is itself production.
- Site Reliability Engineering, chapter 22 "Addressing Cascading Failures" (read free): deadline propagation and retry amplification across layers.
Other curricula
- Azure Architecture Center: API gateways: frames the gateway as routing, aggregation and offloading patterns, and compares reverse proxies, mesh ingress and managed services.
- donnemartin/system-design-primer: covers the gateway only as a reverse proxy (security, TLS termination, caching) and does not discuss control planes or config rollout.
Watch
- What is API Gateway? (ByteByteGo): a short tour of what gateways do.
- Reverse Proxy vs API Gateway vs Load Balancer (ByteByteGo): where the three roles overlap and where they differ.
Free resources
- Kubernetes Gateway API: the role-oriented resource model (GatewayClass, Gateway, HTTPRoute) and its rationale.
- Envoy xDS configuration API overview: how proxies receive listeners, routes and clusters from a control plane.
- Envoy HTTP routing: route matching, weighted clusters, timeouts and retry policy.
- gRPC deadlines: why and how to propagate a deadline across service hops.
- Timeouts, retries, and backoff with jitter (AWS Builders' Library): how retries amplify load and how to bound them.
- What is Amazon API Gateway?: a managed gateway's feature set and limits.