Skip to content
BytePatterns

ALB vs NLB vs Gateway Load Balancer: Layer 7, Layer 4, Layer 3

10 min readBytePatterns

ALB vs NLB vs Gateway Load Balancer: what each one can see, from HTTP host and path to TCP ports to raw IP packets, plus target groups, TLS and a toy model.

ALB vs NLB vs Gateway Load Balancer in one line: an Application Load Balancer reads the HTTP request and routes on it, a Network Load Balancer forwards TCP, UDP and TLS flows without reading them, and a Gateway Load Balancer passes every IP packet through a fleet of security appliances and back. The cleanest way to choose is to ask what each one can see, because a load balancer can only route on fields it reads. Everything below comes from the Elastic Load Balancing documentation listed at the end, as of October 2026.

TypeWhat it can seeTypical use
ALB, layer 7host, path, headers, method, query stringHTTP services and APIs, routing by URL
NLB, layer 4protocol, IP addresses and portsTCP or UDP services, static IPs, client IP
GWLB, layer 3every IP packet, on all portsfirewalls and inspection appliances

The problem it solves

All three spread traffic over healthy targets, and all three are "a load balancer" on a whiteboard. They answer different questions:

  • Which service should handle this request? That needs the URL or a header: layer 7.
  • Which server should take this connection? That needs only addresses and ports, for any protocol: layer 4.
  • Has this packet been inspected? That is not about choosing a server at all, but about putting a firewall in the path without the application noticing.

The wrong pick shows up later as a missing feature: path routing on an NLB, or a static IP on an ALB.

The intuition

An ALB terminates the connection and reads the request. It works at the seventh layer of the OSI model. An HTTPS listener uses a certificate to decrypt requests, and then listener rules, evaluated in priority order, match conditions on the host, the path, HTTP headers and methods, query parameters and source IP. Each rule forwards to a target group of instances, IP addresses or Lambda functions, with its own health check and, by default, round robin routing.

An NLB forwards flows. It works at the fourth layer, and its target groups speak TCP, UDP, TCP_UDP, TLS and QUIC. For TCP it picks a target with a flow hash over the protocol, addresses, ports and TCP sequence number, so each connection stays on one target for its lifetime, while a second connection from the same client can land elsewhere. Each enabled zone gets a static IP address, and an internet-facing NLB can take one Elastic IP per subnet. Client IP preservation depends on the target type: on by default for instance targets, off by default for IP targets over TCP and TLS.

A GWLB is a bump in the wire. It works at the third layer, listens for all IP packets across all ports, and exchanges traffic with its appliances using the GENEVE protocol on port 6081. It keeps each flow on one appliance, using the 5-tuple by default. Traffic reaches it through a Gateway Load Balancer endpoint set as the next hop in a route table, so neither the client nor the application is configured to talk to it.

Sticky sessions are a target group setting too: cookies on an ALB, source address on an NLB, where clients behind one NAT device all land on one target. For the algorithms behind "pick a server", see round robin vs least connections.

Watch it run

The lesson animation puts an Application Load Balancer in front of an Auto Scaling group. Traffic doubles, a target tracking policy adds instances, and they get traffic only once their health checks pass; then one instance stops answering and the ALB drops it at once while the group replaces it. Notice where the health check lives: on the target group, the same object an NLB uses.

EC2, Auto Scaling & Load Balancers

Step 1 of 11

Two instances in two Availability Zones behind a load balancer. The group says: min 2, max 4.

The same interactive animation as the lesson — step through it with the controls.

The code

The routing that only an ALB can do, as an illustrative CloudFormation listener rule; the references are placeholders:

OrdersRule:
  Type: AWS::ElasticLoadBalancingV2::ListenerRule
  Properties:
    ListenerArn: !Ref HttpsListener
    Priority: 10
    Conditions:
      - Field: host-header
        HostHeaderConfig:
          Values: ["api.example.com"]
      - Field: path-pattern
        PathPatternConfig:
          Values: ["/orders/*"]
    Actions:
      - Type: forward
        TargetGroupArn: !Ref OrdersTargetGroup

Next, a toy model, not Elastic Load Balancing itself. The same packet goes to three routers, and each one may only use the fields its layer can see. The ALB first:

import fnmatch
import hashlib

def flow_hash(pkt, fields):
    """Deterministic hash of the chosen header fields (toy flow hash)."""
    key = "|".join(str(pkt[f]) for f in fields)
    return int(hashlib.md5(key.encode()).hexdigest(), 16)

FIVE_TUPLE = ["protocol", "src_ip", "src_port", "dst_ip", "dst_port"]

def alb(pkt, rules, default_tg):
    """Toy model of an ALB listener: rules in priority order over the HTTP request.
    The ALB terminated TLS, so it can read host, path, headers and query."""
    req = pkt["http"]
    for priority, cond, tg in sorted(rules, key=lambda r: r[0]):
        ok = all([
            fnmatch.fnmatch(req["host"], cond.get("host", "*")),
            fnmatch.fnmatch(req["path"], cond.get("path", "*")),
            all(req["headers"].get(k) == v for k, v in cond.get("headers", {}).items()),
            all(req["query"].get(k) == v for k, v in cond.get("query", {}).items()),
        ])
        if ok:
            return tg, f"rule {priority}"
    return default_tg, "default rule"

def nlb(pkt, listeners, targets):
    """Toy model of an NLB: the listener is chosen by protocol and port,
    the target by a hash of the flow. The payload is opaque bytes."""
    tg = listeners[(pkt["protocol"], pkt["dst_port"])]
    pool = targets[tg]
    return tg, pool[flow_hash(pkt, FIVE_TUPLE) % len(pool)]

def gwlb(pkt, appliances, verdict):
    """Toy model of a GWLB: every IP packet goes to one appliance (sticky per
    5-tuple), wrapped in GENEVE on port 6081, and comes back if allowed."""
    appliance = appliances[flow_hash(pkt, FIVE_TUPLE) % len(appliances)]
    wrapped = {"outer": {"protocol": "UDP", "dst_port": 6081, "to": appliance},
               "inner": pkt}
    return appliance, verdict(wrapped["inner"]), wrapped["outer"]["dst_port"]

def visible(lb, pkt):
    """What each load balancer can base a decision on."""
    if lb == "ALB":
        return sorted(pkt["http"])          # host, path, headers, query, method
    return FIVE_TUPLE                       # NLB and GWLB: addresses, ports, protocol

def req(path, host="shop.example.com", src_port=50000, headers=None, query=None):
    return {"protocol": "TCP", "src_ip": "203.0.113.7", "src_port": src_port,
            "dst_ip": "198.51.100.10", "dst_port": 443,
            "http": {"method": "GET", "host": host, "path": path,
                     "headers": headers or {}, "query": query or {}}}

rules = [
    (10, {"host": "api.example.com", "path": "/orders/*"}, "orders-tg"),
    (20, {"path": "/images/*"}, "static-tg"),
    (30, {"headers": {"x-canary": "1"}}, "canary-tg"),
]
for p in [req("/orders/42", host="api.example.com"), req("/images/logo.png"),
          req("/cart", headers={"x-canary": "1"}), req("/cart")]:
    print("ALB", p["http"]["host"], p["http"]["path"], "->", alb(p, rules, "web-tg"))
# ALB api.example.com /orders/42 -> ('orders-tg', 'rule 10')
# ALB shop.example.com /images/logo.png -> ('static-tg', 'rule 20')
# ALB shop.example.com /cart -> ('canary-tg', 'rule 30')
# ALB shop.example.com /cart -> ('web-tg', 'default rule')

The NLB sends two requests on one connection to one target, because nothing it reads differs between them; a new connection hashes again. The GWLB decides nothing about the application: it picks an appliance, wraps the packet in GENEVE and lets the appliance's verdict stand.

listeners = {("TCP", 443): "web-tcp-tg", ("UDP", 53): "dns-tg"}
targets = {"web-tcp-tg": ["i-a", "i-b", "i-c"], "dns-tg": ["i-d", "i-e"]}

# Same connection, two different paths: the NLB cannot tell them apart.
a, b = req("/orders/42", src_port=50001), req("/images/logo.png", src_port=50001)
print("NLB", nlb(a, listeners, targets), nlb(b, listeners, targets))
print("NLB new connection:", nlb(req("/orders/42", src_port=50004), listeners, targets))
print("ALB sees:", visible("ALB", a))
print("NLB sees:", visible("NLB", a))
# NLB ('web-tcp-tg', 'i-c') ('web-tcp-tg', 'i-c')
# NLB new connection: ('web-tcp-tg', 'i-b')
# ALB sees: ['headers', 'host', 'method', 'path', 'query']
# NLB sees: ['protocol', 'src_ip', 'src_port', 'dst_ip', 'dst_port']

firewalls = ["fw-1", "fw-2", "fw-3"]
deny_telnet = lambda inner: "drop" if inner["dst_port"] == 23 else "allow"
web = req("/cart")
telnet = dict(web, dst_port=23)
print("GWLB", gwlb(web, firewalls, deny_telnet), gwlb(telnet, firewalls, deny_telnet))
# GWLB ('fw-2', 'allow', 6081) ('fw-3', 'drop', 6081)

Flow stickiness, checked on 3,000 random flows sent five times each: every flow reached a single appliance, and the hash spread the flows roughly evenly.

import random

# Flow stickiness: 3,000 random flows, each sent 5 times, must reach one appliance.
random.seed(9)
sticky, load = 0, {fw: 0 for fw in firewalls}
for _ in range(3000):
    flow = req("/", src_port=random.randint(1024, 65535))
    flow["src_ip"] = "10.0.%d.%d" % (random.randint(0, 255), random.randint(1, 254))
    picks = {gwlb(flow, firewalls, deny_telnet)[0] for _ in range(5)}
    sticky += len(picks) == 1
    load[next(iter(picks))] += 1
print(sticky, load)
# 3000 {'fw-1': 1014, 'fw-2': 968, 'fw-3': 1018}

The complexity

The costs are in what each layer has to do per request:

  • ALB parses every request and evaluates rules in priority order. In exchange it can redirect, return fixed responses, authenticate users, and split one hostname over many services.
  • NLB hashes each new flow and forwards bytes; its guide says it can handle millions of requests per second. Every path on port 443 looks the same to it.
  • GWLB adds a trip through an appliance for every flow, so throughput and latency become the appliance fleet's problem.

Where it goes wrong

  • Expecting path routing on an NLB. It routes by protocol and port. If you need a static IP and URL routing, register an ALB as the NLB's target, which the NLB supports.
  • Losing the client IP. Behind an ALB the target sees the load balancer's address and reads the client from the X-Forwarded-For header. Behind an NLB with IP targets over TCP, preservation is off by default; turn it on or use proxy protocol v2.
  • Sticky sessions hiding a state problem. If a target fails, its sessions move anyway. Keep session state off the instance.
  • Treating GWLB as an application balancer. It balances appliances. The application still needs its own ALB or NLB.

When it shows up in interviews

It usually arrives inside a design: a web tier, a game server on UDP, and a rule that all traffic is inspected. That is one of each. A short version of the comparison is question 15 in AWS interview questions, part 1; the follow-up often asks how the balancer and the Auto Scaling group share health checks, which the EC2, Auto Scaling and load balancers lesson covers. For the same split inside Kubernetes, see ClusterIP vs NodePort vs LoadBalancer vs Ingress.

How to say it in an interview

"I pick by what the balancer must read. If routing depends on the request, the host, the path or a header, I use an ALB: it terminates TLS, runs listener rules in priority order and forwards to target groups. If I need raw TCP or UDP, a static IP per zone or the client's source address, I use an NLB, which hashes each flow to a target. If every packet has to pass a firewall fleet, I use a Gateway Load Balancer, which sends traffic through the appliances over GENEVE and back, transparently."

Frequently asked questions

What is the difference between ALB and NLB?

An ALB works at layer 7: it reads HTTP requests and routes them by host, path, headers or query string to target groups. An NLB works at layer 4: it forwards TCP, UDP or TLS flows by hashing addresses and ports, offers a static IP per zone, and can preserve the client's source IP.

When should I use a Gateway Load Balancer?

Use it when traffic must pass through virtual appliances, such as firewalls or intrusion detection systems, without reconfiguring the application. It works at layer 3, keeps each flow on one appliance, exchanges packets with them over GENEVE on port 6081, and is reached through Gateway Load Balancer endpoints in route tables.

Is NLB faster than ALB?

An NLB does less per request: it hashes a flow and forwards bytes without parsing HTTP, and its guide says it can handle millions of requests per second. An ALB does more work because it reads and routes on each request. Choose on the features you need first, then measure your own latency.

Sources