Load balancers, proxies and gateways
Every design diagram has a box in front of the servers, and interviewers like to ask what's in it. Is it an L4 or L7 balancer, a reverse proxy, an API gateway, or a service mesh? NGINX, HAProxy and Envoy can each be several of these; every cloud sells its own; and on Kubernetes the community Ingress NGINX controller was retired in March 2026. This guide explains how each works, how they differ, and which to choose, with a chooser and code you can run.
- Sam What's in the box in front of the servers?
- Priya It depends. An L4 or L7 balancer, a reverse proxy, an API gateway, or a service mesh. NGINX, HAProxy and Envoy can each be several of these, and every cloud sells its own.
- Sam What are the four jobs?
- Priya An L4 balancer decides per connection, by addresses and ports. An L7 balancer or reverse proxy reads each HTTP request. An API gateway is the front door to many services, with keys, quotas and routing. And a service mesh sits between your own services, adding mutual TLS, retries and policies.
- Sam Walk me through a request.
- Priya The L4 balancer picks a gateway copy for the connection and forwards bytes. The gateway ends TLS, checks the token and quota, and routes by path. It balances each request across the service's copies. A mesh proxy adds mutual TLS and a retry on internal calls. And a copy that keeps failing gets ejected, even if its health check passes.
- Sam What are the options?
- Priya NGINX, a web server and reverse proxy. HAProxy, a dedicated, fast balancer. Envoy, configured live through APIs by a control plane. The cloud balancers: ALB and NLB on AWS, Cloud Load Balancing on Google with a global anycast IP, and Front Door on Azure. Gateways like Kong, Apigee, Azure API Management and AWS API Gateway, which throttles accounts to ten thousand requests a second by default. And meshes: Istio, whose ambient mode drops per-pod sidecars, and Linkerd. One warning: the community Ingress NGINX controller stopped getting security fixes in March twenty twenty-six; Gateway API is the way forward.
- Sam How do I choose?
- Priya Not HTTP? Use L4. Long-lived HTTP/2 or gRPC connections? Balance at L7. On a cloud, start with its managed L7 balancer. Outside developers with keys? Add a gateway. Many internal services needing mTLS? Consider a mesh, once there are enough of them.
- Priya Your turn. A gRPC client sends ten thousand requests over one connection through an L4 balancer. Where do they go?
- Priya All to one backend. L4 balances connections, not the requests inside them.
- Sam And the code?
- Priya Three clients with long-lived connections, balanced per connection and then per request, plus routing by the longest path prefix.
- Sam Common mistakes?
- Priya L4 balancing for gRPC. Business logic in the gateway. A single self-run balancer in one zone. A mesh for five services. And staying on retired software.
1. Four jobs, often one product
Module 08 teaches the balancing algorithms and health checks; this guide is about the products and where each sits.
| Job | Where it sits | Decides by | Examples |
|---|---|---|---|
| L4 load balancer | In front of anything that speaks TCP or UDP | IP addresses and ports, per connection | AWS NLB, Google's passthrough Network LB, Azure Load Balancer, HAProxy in TCP mode |
| L7 load balancer / reverse proxy | In front of web servers | Each HTTP request: host, path, headers | AWS ALB, Google's Application LB, NGINX, HAProxy, Envoy |
| API gateway | The front door to many services | Your API: caller, quota, route, version | Kong, Apigee, AWS API Gateway, Azure API Management |
| Service mesh | Between your own services | Service identity, retries, policies | Istio, Linkerd, Cilium |
2. How a request gets through
One request from a phone to an orders service, through each layer in turn.
3. The products
NGINX
An event-driven web server and reverse proxy: a few worker processes, each handling thousands of connections. Serves static files, terminates TLS, caches and balances. Configuration is a file, reloaded without dropping connections.
Best for: a web server and reverse proxy in one, for most sites.
HAProxy
A fast L4 and L7 load balancer focused on balancing: many algorithms, detailed health checks, stick tables for rate limiting and session affinity, and a runtime API for changes without reloads.
Best for: a dedicated, high-performance balancer you run yourself.
Envoy
An L7 proxy built for service-to-service traffic, configured through APIs (xDS) rather than files, so a control plane can update it live. Load balancers include weighted round robin, least request with power of two choices, ring hash and Maglev, plus outlier detection and slow start. The data plane for Istio and many gateways.
Best for: dynamic environments where a control plane programs the proxies.
ALB, NLB and Gateway LB
Elastic Load Balancing: the Application Load Balancer for HTTP routing, the Network Load Balancer for TCP and UDP with static IPs and very high connection rates, and the Gateway Load Balancer for inserting firewalls.
Best for: ALB for web apps and APIs on AWS; NLB for non-HTTP protocols.
Cloud Load Balancing
Application Load Balancers (L7) and Network Load Balancers (L4), each regional or global. The global external balancers use one anycast IP served from Google's edge, so users connect to the nearest point of presence.
Best for: one global IP in front of backends in several regions.
Front Door, Application Gateway, Load Balancer
Azure Front Door is a global L7 entry point with CDN and web application firewall; Application Gateway is a regional L7 balancer; Azure Load Balancer is regional L4.
Best for: Front Door for global web apps; Application Gateway inside a region.
Kong
An API gateway built on NGINX and OpenResty, extended by plugins for authentication, rate limiting, transformations and logging. Runs anywhere, including as a Kubernetes ingress.
Best for: a self-run gateway with many ready-made plugins.
AWS API Gateway, Apigee, Azure API Management
Managed gateways with keys, quotas, usage plans and developer portals. AWS API Gateway throttles each account by default to 10,000 requests a second per region, with bursts from a token bucket of up to 5,000 requests. Apigee and Azure API Management focus on API products, analytics and monetisation.
Best for: public APIs that need keys, quotas and a developer portal.
Istio
Gives services mutual TLS, retries, traffic splitting and policies without code changes. Its ambient mode, generally available since v1.24 (November 2024), replaces a sidecar per pod with a shared per-node proxy (ztunnel) for L4 and optional waypoint proxies for L7.
Best for: many services needing mTLS and traffic policy, with a team to run it.
Linkerd
A smaller mesh with its own lightweight Rust proxy, known for simple operation. Since February 2024 the project publishes edge releases only; stable releases come from vendors such as Buoyant. Linkerd 2.20 was announced in June 2026.
Best for: mTLS and golden metrics with less to operate than Istio.
4. Differences at a glance
| Option | Layers | Configured by | Strength | Runs where |
|---|---|---|---|---|
| NGINX | L7 (and L4 stream) | Config file, reload | Web server plus proxy | Anywhere |
| HAProxy | L4 and L7 | Config file, runtime API | Balancing features and speed | Anywhere |
| Envoy | L4 and L7 | xDS APIs from a control plane | Dynamic, rich observability | Anywhere; inside meshes and gateways |
| AWS ALB / NLB | L7 / L4 | AWS APIs | Managed, integrates with AWS | AWS regions |
| Google Cloud LB | L7 and L4, regional or global | Google Cloud APIs | Global anycast IP | Google's edge and regions |
| Azure Front Door | L7, global | Azure APIs | Edge, CDN and WAF in one | Microsoft's edge |
| Kong | L7 gateway | Admin API, declarative config | Plugins | Anywhere |
| AWS API Gateway / Apigee / APIM | L7 gateway | Cloud APIs | Keys, quotas, portals | Their clouds |
| Istio / Linkerd | L4 and L7 between services | Kubernetes resources | mTLS and policy without code changes | Kubernetes |
5. When to choose which, and why
- Is it HTTP? If not (databases, MQTT, game servers, raw TCP), you need L4: NLB, Google's Network LB, Azure Load Balancer or HAProxy in TCP mode.
- Long-lived connections carrying many requests (HTTP/2, gRPC)? Balance at L7, or the first connection's backend gets all its requests (the demo below shows this).
- On a cloud, start with its managed L7 balancer (ALB, Google's Application LB, Front Door or Application Gateway). Run NGINX, HAProxy or Envoy yourself when you need control the managed one doesn't give.
- Many services behind one public API, with keys and quotas? Add an API gateway. Keep business logic out of it.
- Many internal services needing mTLS, retries and traffic splitting? Consider a mesh, once there are enough services to justify running one; a handful of services can use client libraries instead.
Try it: choose for your requirements
6. L4 vs L7 balancing, in code
Three clients each hold one long-lived connection: 60, 30 and 10 requests. An L4 balancer picks a backend per connection, so one backend gets 60 requests and another 10. An L7 proxy picks a backend per request and splits them almost evenly. The last lines show L7 routing by the longest matching path prefix. The program was run in all three languages; each prints the output below it.
BACKENDS = ["api-1", "api-2", "api-3"]
# Three clients, each holding one long-lived HTTP/2 or gRPC connection that carries many requests.
CONNECTIONS = [("mobile-gateway", 60), ("web-tier", 30), ("batch-job", 10)]
def l4_round_robin():
"""An L4 balancer picks a backend per connection, then forwards bytes; it never sees the requests inside."""
load = {b: 0 for b in BACKENDS}
for n, (_, requests) in enumerate(CONNECTIONS):
load[BACKENDS[n % len(BACKENDS)]] += requests
return load
def l7_round_robin():
"""An L7 proxy ends the connection, reads each request and picks a backend per request."""
load = {b: 0 for b in BACKENDS}
n = 0
for _, requests in CONNECTIONS:
for _ in range(requests):
load[BACKENDS[n % len(BACKENDS)]] += 1
n += 1
return load
ROUTES = [("/api/orders", "orders"), ("/api/", "api"), ("/static/", "cdn")]
def route(path):
"""L7 routing: the longest matching path prefix wins, as in NGINX locations, Envoy routes or a gateway."""
best = max((prefix for prefix, _ in ROUTES if path.startswith(prefix)), key=len, default=None)
return dict(ROUTES)[best] if best else "404"
def show(name, load):
print(f"{name}: " + ", ".join(f"{b}={load[b]}" for b in BACKENDS))
show("L4, per connection", l4_round_robin())
show("L7, per request", l7_round_robin())
for path in ["/api/orders/81", "/api/users/7", "/static/logo.svg", "/admin"]:
print(f"route {path} -> {route(path)}")
const BACKENDS = ["api-1", "api-2", "api-3"];
// Three clients, each holding one long-lived HTTP/2 or gRPC connection that carries many requests.
const CONNECTIONS: [string, number][] = [["mobile-gateway", 60], ["web-tier", 30], ["batch-job", 10]];
// An L4 balancer picks a backend per connection, then forwards bytes; it never sees the requests inside.
function l4RoundRobin(): Map<string, number> {
const load = new Map(BACKENDS.map((b) => [b, 0]));
CONNECTIONS.forEach(([, requests], n) => {
const b = BACKENDS[n % BACKENDS.length];
load.set(b, load.get(b)! + requests);
});
return load;
}
// An L7 proxy ends the connection, reads each request and picks a backend per request.
function l7RoundRobin(): Map<string, number> {
const load = new Map(BACKENDS.map((b) => [b, 0]));
let n = 0;
for (const [, requests] of CONNECTIONS) {
for (let i = 0; i < requests; i++) {
const b = BACKENDS[n % BACKENDS.length];
load.set(b, load.get(b)! + 1);
n++;
}
}
return load;
}
const ROUTES: [string, string][] = [["/api/orders", "orders"], ["/api/", "api"], ["/static/", "cdn"]];
// L7 routing: the longest matching path prefix wins, as in NGINX locations, Envoy routes or a gateway.
function route(path: string): string {
let best: [string, string] | null = null;
for (const r of ROUTES) if (path.startsWith(r[0]) && (!best || r[0].length > best[0].length)) best = r;
return best ? best[1] : "404";
}
function show(name: string, load: Map<string, number>): void {
console.log(`${name}: ` + BACKENDS.map((b) => `${b}=${load.get(b)}`).join(", "));
}
show("L4, per connection", l4RoundRobin());
show("L7, per request", l7RoundRobin());
for (const path of ["/api/orders/81", "/api/users/7", "/static/logo.svg", "/admin"]) {
console.log(`route ${path} -> ${route(path)}`);
}import java.util.*;
public class Main {
static final List<String> BACKENDS = List.of("api-1", "api-2", "api-3");
// Three clients, each holding one long-lived HTTP/2 or gRPC connection that carries many requests.
static final int[] REQUESTS_PER_CONNECTION = {60, 30, 10}; // mobile-gateway, web-tier, batch-job
// An L4 balancer picks a backend per connection, then forwards bytes; it never sees the requests inside.
static Map<String, Integer> l4RoundRobin() {
Map<String, Integer> load = new LinkedHashMap<>();
BACKENDS.forEach(b -> load.put(b, 0));
for (int n = 0; n < REQUESTS_PER_CONNECTION.length; n++) {
load.merge(BACKENDS.get(n % BACKENDS.size()), REQUESTS_PER_CONNECTION[n], Integer::sum);
}
return load;
}
// An L7 proxy ends the connection, reads each request and picks a backend per request.
static Map<String, Integer> l7RoundRobin() {
Map<String, Integer> load = new LinkedHashMap<>();
BACKENDS.forEach(b -> load.put(b, 0));
int n = 0;
for (int requests : REQUESTS_PER_CONNECTION) {
for (int i = 0; i < requests; i++) load.merge(BACKENDS.get(n++ % BACKENDS.size()), 1, Integer::sum);
}
return load;
}
static final String[][] ROUTES = {{"/api/orders", "orders"}, {"/api/", "api"}, {"/static/", "cdn"}};
// L7 routing: the longest matching path prefix wins, as in NGINX locations, Envoy routes or a gateway.
static String route(String path) {
String[] best = null;
for (String[] r : ROUTES) if (path.startsWith(r[0]) && (best == null || r[0].length() > best[0].length())) best = r;
return best == null ? "404" : best[1];
}
static void show(String name, Map<String, Integer> load) {
StringJoiner j = new StringJoiner(", ");
load.forEach((b, v) -> j.add(b + "=" + v));
System.out.println(name + ": " + j);
}
public static void main(String[] args) {
show("L4, per connection", l4RoundRobin());
show("L7, per request", l7RoundRobin());
for (String path : List.of("/api/orders/81", "/api/users/7", "/static/logo.svg", "/admin")) {
System.out.println("route " + path + " -> " + route(path));
}
}
}L4, per connection: api-1=60, api-2=30, api-3=10
L7, per request: api-1=34, api-2=33, api-3=33
route /api/orders/81 -> orders
route /api/users/7 -> api
route /static/logo.svg -> cdn
route /admin -> 404
7. Say it in the interview
- "L4 or L7?" L7 for HTTP and gRPC, because it balances each request, routes by path and terminates TLS; L4 for other protocols or when I need raw connection throughput and static IPs. Often both: L4 in front of a fleet of L7 proxies.
- "Load balancer or API gateway?" A load balancer spreads traffic over copies of one service; a gateway is the front door to many services, applying authentication, quotas and routing once. Public APIs with keys get a gateway.
- "Do you need a service mesh?" Only with many services that need mTLS, retries and traffic splitting consistently. Istio's ambient mode lowers the cost by dropping per-pod sidecars, but it's still a system to run.
8. Common mistakes
9. Check yourself
A gRPC client keeps one HTTP/2 connection open and sends 10,000 requests through an L4 load balancer to five backends. Where do they go?
Outside developers call your API with keys and monthly quotas. What belongs in front?
What did Istio's ambient mode change?
Your clusters use the community Ingress NGINX controller. What's the concern in 2026?
10. Sources and further reading
Product facts checked on 3 November 2026. Versions, quotas and support status change; check before relying on them.
| Source | Used for |
|---|---|
| Envoy: supported load balancers | Algorithms, least request with power of two choices, slow start |
| Envoy: outlier detection | Passive ejection, growing ejection times, maximum ejection percentage |
| Amazon API Gateway quotas | Account throttle and burst |
| Istio: ambient mode reaches general availability | ztunnel and waypoints, v1.24 |
| Linkerd releases | Edge and stable releases, latest version |
| Ingress NGINX retirement: what you need to know | End of maintenance in March 2026, Gateway API |
| Module 08: Load balancing and scaling | Algorithms, health checks, latency-aware balancing |