InterviewKata
System design · Technology guide T04 · Builds on Module 08

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.

A 3-minute conversation: Sam asks, Priya explains
0:00 / 2:52

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.

JobWhere it sitsDecides byExamples
L4 load balancerIn front of anything that speaks TCP or UDPIP addresses and ports, per connectionAWS NLB, Google's passthrough Network LB, Azure Load Balancer, HAProxy in TCP mode
L7 load balancer / reverse proxyIn front of web serversEach HTTP request: host, path, headersAWS ALB, Google's Application LB, NGINX, HAProxy, Envoy
API gatewayThe front door to many servicesYour API: caller, quota, route, versionKong, Apigee, AWS API Gateway, Azure API Management
Service meshBetween your own servicesService identity, retries, policiesIstio, 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

Proxy · open source and commercial (NGINX Plus)

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.

Proxy · open source

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.

Proxy · open source (CNCF)

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.

Managed · AWS

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.

Managed · Google Cloud

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.

Managed · Azure

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.

API gateway · open source and commercial

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.

API gateway · managed

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.

Service mesh · open source (CNCF)

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.

Service mesh · open source (CNCF)

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.

Ingress NGINX is retiredThe Kubernetes project ended best-effort maintenance of the community Ingress NGINX controller in March 2026: no more releases or security fixes. The Ingress API itself remains, but is feature-frozen; the recommended direction is Gateway API, implemented by Envoy Gateway, Istio, Kong, NGINX Gateway Fabric, the cloud providers' controllers and others.

4. Differences at a glance

OptionLayersConfigured byStrengthRuns where
NGINXL7 (and L4 stream)Config file, reloadWeb server plus proxyAnywhere
HAProxyL4 and L7Config file, runtime APIBalancing features and speedAnywhere
EnvoyL4 and L7xDS APIs from a control planeDynamic, rich observabilityAnywhere; inside meshes and gateways
AWS ALB / NLBL7 / L4AWS APIsManaged, integrates with AWSAWS regions
Google Cloud LBL7 and L4, regional or globalGoogle Cloud APIsGlobal anycast IPGoogle's edge and regions
Azure Front DoorL7, globalAzure APIsEdge, CDN and WAF in oneMicrosoft's edge
KongL7 gatewayAdmin API, declarative configPluginsAnywhere
AWS API Gateway / Apigee / APIML7 gatewayCloud APIsKeys, quotas, portalsTheir clouds
Istio / LinkerdL4 and L7 between servicesKubernetes resourcesmTLS and policy without code changesKubernetes

5. When to choose which, and why

  1. 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.
  2. 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).
  3. 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.
  4. Many services behind one public API, with keys and quotas? Add an API gateway. Keep business logic out of it.
  5. 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

Two-sentence answers
  • "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

L4 balancing for gRPCOne long-lived HTTP/2 connection pins all its requests to one backend. Balance per request at L7, or on the client.
Business logic in the gatewayA gateway that knows what an order is becomes a bottleneck every team must change. Keep it to cross-cutting rules.
One balancer, one zoneSelf-run proxies need at least two, across zones, with failover in front; managed balancers do this for you.
A mesh for five servicesThe operational cost arrives before the benefit. Start with libraries and a good gateway.
Staying on retired softwareIngress NGINX gets no security fixes after March 2026; plan a move to a Gateway API implementation.

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?

L4 balances connections, not the requests inside them; balance gRPC at L7 or on the client.

Outside developers call your API with keys and monthly quotas. What belongs in front?

Keys, quotas and usage plans are a gateway's job.

What did Istio's ambient mode change?

ztunnel handles L4 per node; waypoints add L7 where needed.

Your clusters use the community Ingress NGINX controller. What's the concern in 2026?

Maintenance ended in March 2026; the Ingress API remains but is frozen.

10. Sources and further reading

Product facts checked on 3 November 2026. Versions, quotas and support status change; check before relying on them.

SourceUsed for
Envoy: supported load balancersAlgorithms, least request with power of two choices, slow start
Envoy: outlier detectionPassive ejection, growing ejection times, maximum ejection percentage
Amazon API Gateway quotasAccount throttle and burst
Istio: ambient mode reaches general availabilityztunnel and waypoints, v1.24
Linkerd releasesEdge and stable releases, latest version
Ingress NGINX retirement: what you need to knowEnd of maintenance in March 2026, Gateway API
Module 08: Load balancing and scalingAlgorithms, health checks, latency-aware balancing