Skip to content

Anatomy of a High-Performance API Gateway

Core Concept LearningAugust 3, 20269 min read

Every request into a microservices backend crosses the same narrow point before it reaches any business logic: the API gateway. Under light load, almost any gateway configuration works, which is exactly why the real design mistakes — an unbounded retry policy, a rate limiter that resets under multi-instance deployment, a health check that doesn't match how the upstream actually fails — stay invisible until the one day traffic spikes or an upstream service degrades, and the gateway amplifies the problem instead of containing it.

This guide dissects one concrete request — a checkout API call that has to pass TLS termination, routing, authentication, rate limiting, and load-balanced retries before it reaches the order service — and uses it to compare three real gateway approaches: NGINX's static, event-loop model; Envoy's dynamically configured control-plane model; and a serverless front door built on Cloud Functions. Each owns the same responsibilities differently, and the differences show up exactly at the failure cases naive comparisons skip. For the load-balancing layer underneath this, see load balancing: L4 vs. L7; for the rate-limiting mechanism, see rate limiting algorithms and NGINX as an edge gateway.

Layers of a request passing through an API gateway
Layers of a request passing through an API gateway

What a gateway actually owns

An API gateway is not just "a reverse proxy with extra features" — it's the layer that owns a specific, ordered set of cross-cutting concerns so individual services don't each reimplement them: TLS termination (so services behind it can speak plain HTTP internally), request routing (mapping a path or host to the right upstream service), authentication/authorization enforcement at the edge, rate limiting per client, and load-balanced retries across healthy upstream instances. Getting the order right matters — routing has to happen before rate limiting can apply the right per-route limit, and rate limiting has to happen before the request reaches an upstream that would otherwise absorb load it never should have received.

The checkout request makes this concrete: TLS terminates at the gateway, so the certificate and cipher configuration live in exactly one place instead of every service; the path /api/checkout routes to the order service's upstream pool; a per-API-key token-bucket check runs before the request is forwarded; and if the order service's first instance is slow or down, the gateway retries against a healthy instance rather than the client seeing a raw connection failure.

Quick reference

  • TLS termination at the gateway centralizes certificate rotation and cipher policy — services behind it can use plain HTTP or mTLS on an internal network instead of each managing public certs.
  • Routing decisions (path, host, header-based) must resolve before any per-route policy (rate limit, auth scope) can apply correctly.
  • A gateway that terminates TLS becomes a single point of failure for every service behind it — its own availability and scaling become a shared dependency for the whole backend.
  • Authentication at the edge (validating a token before the request reaches any service) reduces the blast radius of a service that forgets to check auth — but doesn't replace per-service authorization for what an authenticated caller is allowed to do.

Remember this

A gateway's value is centralizing cross-cutting concerns in one correctly ordered pipeline — get the order wrong (rate limit after routing, auth after forwarding) and each concern silently stops doing its job for some fraction of requests.

NGINX and Envoy: static config vs. a live control plane

NGINX's core strength is architectural simplicity: an event-loop worker model that handles enormous concurrent connection counts with very low per-request overhead, configured through a static file that's reloaded (not hot-swapped mid-request) when routes or upstreams change. That simplicity is also its constraint — adding or removing an upstream instance dynamically (as a Kubernetes deployment scales pods up and down) requires either a config reload or an external mechanism (like nginx-ingress watching Kubernetes and regenerating config) rather than a built-in service-discovery integration.

Envoy was built for exactly that dynamic case: it separates the data plane (the proxy handling traffic) from a control plane that pushes configuration updates over the xDS API in real time, so upstream endpoints, routing rules, and even rate-limit policy can change without a reload or restart. That buys live reconfiguration in a fast-scaling microservices environment, at the cost of running (or adopting) a control plane and a meaningfully larger operational surface than a static NGINX config file.

Where NGINX, Envoy, and a serverless gateway fit
Where NGINX, Envoy, and a serverless gateway fit

Quick reference

  • NGINX config changes require a reload (nginx -s reload) — connections in flight are typically drained gracefully, but this is a distinct operational step from Envoy's live xDS push.
  • Envoy's xDS protocol (CDS/EDS/RDS/LDS) lets a control plane update clusters, endpoints, routes, and listeners independently and incrementally, without restarting the proxy process.
  • NGINX's simplicity is an operational advantage for a stable, slow-changing topology; Envoy's dynamic model earns its complexity in a topology that changes frequently (autoscaling, canary rollouts, service mesh).
  • Both support similar core primitives (routing, TLS, rate limiting via modules/filters) — the real differentiator is how configuration changes propagate, not raw per-request performance for a static workload.

Remember this

Choosing NGINX vs. Envoy is a question about how often your upstream topology changes and who needs to change it — a static, infrequently-changing backend fits NGINX's simplicity; a fast-scaling, dynamically discovered backend needs Envoy's live control-plane model.

The retry storm: one request, cascading failure

Trace the checkout request when the order service's primary instance starts timing out under load: the gateway's retry policy resends the request to another instance. That's correct behavior for one request — the problem appears when every gateway worker independently makes the same decision for every request hitting the slow instance simultaneously, multiplying load onto the remaining healthy instances at exactly the moment they're least able to absorb it. The trigger is a single degraded upstream instance; the symptom is the entire order-service pool becoming overloaded within seconds; the root cause is an unbounded retry policy with no shared budget across the gateway's concurrent requests.

The fix is a retry budget: cap retries as a percentage of total request volume (Envoy's retry budget mechanism, or an equivalent token-bucket-style limiter shared across workers) so retries can't exceed, say, 20% of primary traffic regardless of how many individual requests are failing. Combined with a circuit breaker that stops sending new requests to an instance failing above a threshold, this converts a retry storm into a bounded, contained degradation instead of a cascading outage.

One request hitting an unhealthy upstream — the gateway's retry budget
One request hitting an unhealthy upstream — the gateway's retry budget

Quick reference

  • A retry budget bounds total retry volume as a percentage of primary traffic — without one, retry volume scales with the number of failing requests, not a fixed cap.
  • Outlier detection (ejecting a consistently failing instance from the pool temporarily) stops the bleeding faster than retries alone, which keep sending fresh requests into a known-bad instance.
  • Exponential backoff on retries (not immediate retry) reduces the chance that a retry arrives at the same overloaded instance during its worst moment.
  • Test retry-storm behavior deliberately with a load test that fails one upstream instance under real concurrent traffic — this failure mode rarely appears in a single-request manual test.
Per-request retry, no shared budget
1# NGINX upstream retry — each worker retries independently,2# with no cap on total retry volume across the pool.3upstream order_service {4  server order-1:8080 max_fails=3 fail_timeout=10s;5  server order-2:8080 max_fails=3 fail_timeout=10s;6}7 8location /api/checkout {9  proxy_pass http://order_service;10  proxy_next_upstream error timeout http_502 http_503;11  proxy_next_upstream_tries 3; # per-request cap, no pool-wide budget12}
Envoy retry policy with a shared retry budget + circuit breaker
1# Envoy route config (illustrative — check current Envoy docs for2# exact field names/versions before deploying).3route:4  retry_policy:5    retry_on: "5xx,connect-failure,reset"6    num_retries: 27    retry_back_off:8      base_interval: 0.05s9    retry_budget:10      budget_percent: { value: 20.0 } # retries capped at 20% of primary traffic11outlier_detection:12  consecutive_5xx: 513  interval: 10s14  base_ejection_time: 30s15  # An instance failing 5 consecutive requests is ejected from the pool16  # for 30s — new requests stop going to it instead of retrying into it.17# Break it: remove retry_budget and outlier_detection, then simulate one18# slow upstream instance under load — expected: retries alone multiply19# load onto the remaining instances until they degrade too.

Remember this

A retry policy without a shared budget doesn't recover from a slow upstream — it amplifies the failure onto every other instance in the pool, which is why a retry budget and circuit breaker are not optional hardening, they're the mechanism that makes retries safe at all.

Choosing a gateway for the workload

The decision rule: choose NGINX when the backend topology is relatively stable, the team wants operational simplicity, and per-request overhead at very high connection counts is the dominant concern. Choose Envoy when the topology changes frequently (autoscaling, service mesh, canary deployments) and you need live configuration updates plus rich per-request observability (detailed L7 metrics, distributed tracing integration) that static config makes harder to get. Choose a serverless front door (a gateway built on Cloud Functions or equivalent) when request volume is bursty and low enough that cold-start tail latency is acceptable, and you'd rather not operate gateway infrastructure at all — the trade is cost-per-request and a latency profile you don't fully control.

Whichever gateway you choose, the retry budget and circuit breaker from the previous section apply regardless of vendor — they are the mechanism, not a feature specific to one product. Verify your chosen gateway actually supports a shared retry budget (not just a per-request retry count) before depending on it in production.

Quick reference

  • Match gateway choice to topology change frequency, not to a general reputation for "performance" — both NGINX and Envoy handle high request volume well; the differentiator is configuration dynamism.
  • For a serverless front door, measure p95/p99 latency under your real traffic pattern before committing — cold-start tail latency is the real cost, not the average.
  • Verify retry-budget and circuit-breaker support exists and is configured correctly in whichever gateway you pick — the retry-storm failure mode applies to any gateway with unbounded retries, not just NGINX.
  • Re-run your retry-storm load test after any gateway config change — a config that looked safe on review can silently regress a retry budget setting during a routine update.

Remember this

Gateway choice is a topology and operations decision, not a raw-performance contest — and the retry-storm protection (budget plus circuit breaker) is a requirement for any of them, not a differentiator between them.

Key takeaway

Configure the checkout gateway end to end: TLS termination, path-based routing to the order-service pool, a per-API-key rate limit, and — critically — a retry policy with an explicit shared budget and outlier detection/circuit breaking. Verify success under normal load: requests route correctly and a single slow response retries once against a healthy instance.

Then break it deliberately: load-test with one order-service instance artificially slowed (add a fixed delay) while the others stay healthy, and watch whether your retry policy contains the failure to that instance or amplifies it onto the healthy ones. Pass criterion: the slowed instance gets ejected from the pool (or retries stay within your configured budget), overall checkout latency for the pool stays bounded, and you can see the ejection or budget-limiting event in your gateway's logs or metrics.

Share:

Related Articles

Modern microservice architectures require an API Gateway at the edge to handle incoming client traffic, enforce authenti

Read

Rate Limiting is a critical defense mechanism for production APIs, protecting downstream microservices from traffic spik

Read

System design interviews evaluate a candidate's ability to architect scalable, resilient, and cost-effective distributed

Read

Keep learning

Follow a structured path or browse all courses to go deeper.