Part 9 — Layer 7: Application
Job
Provide the service the user actually wanted, using a protocol that both applications understand: HTTP to place an order, DNS to find a server, the Postgres wire protocol to save it.
In our story
Three L7 protocols take part in your one click: DNS, HTTP, and the Postgres protocol.
Step A: DNS (assuming nothing is cached). The browser needs an IP for
shop.example.com:
Browser cache? miss
OS resolver cache? miss
│
▼ UDP 53 → home router 192.168.1.1 (DNS forwarder) → ISP recursive resolver
│
┌───────────────────────────────────────────────────┘
│ 1. Root server "who handles .com?" → a.gtld-servers.net ...
│ 2. .com TLD server "who handles example.com?" → ns1.example-dns.net ...
│ 3. Authoritative "shop.example.com?"
│ → CNAME shop-alb-123.region.elb.example.net
│ → A 198.51.100.20 TTL 60
▼
Browser now has 198.51.100.20 (cached for 60 s)
DNS is an L7 protocol carried over UDP (L4), falling back to TCP for large responses. Increasingly it's encrypted (DNS-over-HTTPS/TLS). A cold lookup can take 20–100 ms or more. A cached one takes 0 ms.
Step B: the HTTP request. Inside the TLS tunnel, on HTTP/2 stream 15:
:method: POST
:scheme: https
:authority: shop.example.com
:path: /api/v1/orders
content-type: application/json
content-length: 43
cookie: session=s%3A9d8f...; AWSALB=...
idempotency-key: 7f3c9a2e-1b4d-4c8e-9f00-2a6b8d1e5c43
user-agent: Mozilla/5.0 ...
accept-encoding: gzip, br
{"cartId":"c_8812","paymentMethodId":"pm_77"}
Step C: L7 routing at the load balancer. Because the ALB decrypts the traffic and parses HTTP, it can make content-aware decisions that an L4 device can't:
Rule: Host = shop.example.com AND Path = /api/v1/orders* → target group "orders-service"
Adds: X-Forwarded-For: 203.0.113.7 (your public IP, which is otherwise lost)
X-Forwarded-Proto: https
X-Amzn-Trace-Id: Root=1-6703... (distributed-tracing correlation ID)
Picks a healthy pod: 10.0.2.77:8080 (least outstanding requests)
Sends over an existing keep-alive connection, HTTP/1.1 (or HTTP/2, gRPC)
Step D: your code. The orders-service handler:
1. Authenticate: look up the session cookie (Redis) → user u_4411
2. Idempotency: have we seen key 7f3c9a2e...? No → record it
3. Business logic: price the cart, call the payment provider (another full
outbound L1–L7 journey through the NAT gateway!)
4. Persist: INSERT INTO orders ... over a pooled Postgres connection
(Postgres wire protocol, L7, over TCP 5432, often TLS)
5. Respond: 201 Created {"orderId":"o_99120371","status":"confirmed"}
Every outbound call in step D repeats the entire OSI journey on a different path. A single user click usually fans out into 5–50 internal L7 conversations.
How it works
1. HTTP's model: a stateless request/response with method, target, headers, and body. Stateless means every request carries everything the server needs (cookies, tokens), which is what lets any instance handle any request and makes horizontal scaling possible.
2. Methods and idempotency matter to the network layers below.
| Method | Safe? | Idempotent? | Can a proxy/client retry automatically? |
|---|---|---|---|
| GET, HEAD | Yes | Yes | Yes |
| PUT, DELETE | No | Yes | Yes (with care) |
| POST | No | No | No, unless you add an Idempotency-Key |
When a TCP connection drops after the request was sent but before the
response arrived, the client can't tell whether the order was created. L4 can't
answer that question. Only L7 idempotency can. That's why our request carries
an Idempotency-Key.
3. Status codes: who generated them? In a proxy chain, the same code means different things depending on which hop produced it:
| Code | From your app | From the load balancer / proxy |
|---|---|---|
| 400/401/403/404 | App logic | Rare (WAF block → 403) |
| 500 | Your code threw an exception | — |
| 502 Bad Gateway | — | The upstream sent an invalid response or closed the connection (crash, keep-alive race, protocol mismatch) |
| 503 Service Unavailable | Deliberate load shedding | No healthy targets (all failing health checks) |
| 504 Gateway Timeout | — | Upstream didn't respond in time (slow DB, deadlock, network drop) |
Incident tip: always check whether a 5xx came from the app's access log or only the LB's log. A 502/504 that appears only in LB logs means the request never got a response from your code. It's a network, timeout, or crash problem, not a logic bug.
4. The L7 infrastructure you already run:
| Component | L7 superpower |
|---|---|
| CDN | Caching by URL/headers, edge TLS termination, close to users |
| WAF | Blocks malicious payloads (SQLi, XSS) by reading the HTTP content |
| API gateway | Auth, rate limiting per API key, request transformation |
| L7 load balancer / ingress | Routing by host/path/header, retries, health checks |
| Service mesh sidecar (Envoy) | mTLS, per-request load balancing, retries, circuit breaking, telemetry |
5. HTTP versions.
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC over UDP |
| Concurrency | One request per connection at a time (browsers open about 6 connections) | Many streams on one connection | Many streams, no TCP head-of-line blocking |
| Headers | Plain text | HPACK compressed | QPACK compressed |
| Handshake (new connection) | TCP + TLS = 2 RTT | TCP + TLS = 2 RTT | 1 RTT (0-RTT on resumption) |
| Connection migration (Wi-Fi → 4G) | No (new 4-tuple = new connection) | No | Yes (connection IDs) |
Try it yourself
# DNS: the answer, the full delegation chain, and the TTL
dig shop.example.com +noall +answer
dig +trace shop.example.com
dig @1.1.1.1 shop.example.com A +short # ask a specific resolver
scutil --dns | head -20 # macOS: which resolver am I using?
# HTTP: full request/response with headers
curl -v https://shop.example.com/api/v1/health
# Force a specific IP (bypass DNS) while keeping SNI/Host correct: tests one backend/LB
curl -v --resolve shop.example.com:443:198.51.100.20 https://shop.example.com/api/v1/health
# HTTP/3, if your curl supports it
curl --http3 -sI https://shop.example.com/
# Browser: DevTools → Network → click a request → Timing tab
What breaks
| Symptom | L7 cause |
|---|---|
| "Works for me" but not for users after a DNS change | DNS TTL/caching. Old records live until they expire (and some resolvers ignore TTLs) |
NXDOMAIN / SERVFAIL |
Missing record, broken delegation, DNSSEC validation failure |
| Duplicate orders | Client or proxy retried a non-idempotent POST after a timeout |
| 503 from LB, app looks healthy | Health-check path misconfigured / returning non-200 / wrong port |
| 413 Payload Too Large / 431 headers too large | Proxy limits (body size, header size: watch for huge cookies/JWTs) |
| Real client IP is the LB's IP in logs | Not reading X-Forwarded-For (or reading it insecurely, trusting spoofed values) |
| CORS errors in the browser | Missing/incorrect Access-Control-Allow-* headers. A browser policy at L7 |
War story: "The retry storm"
A slow database caused orders-service latency to rise from 100 ms to 3 s. The
ALB timed out at 2 s and returned 504. The mobile app retried 3 times. The API
gateway also retried 2 times. Each hop's retries multiplied: one user tap
became up to 3 × 3 = 9 requests to an already overloaded database, which then
fell over completely. Fixes: retry at only one layer, use exponential
backoff with jitter, set retry budgets, require idempotency keys for POSTs, and
add a circuit breaker. Lesson: retries are an L7 policy that multiplies
load across the whole stack. Somebody has to own the end-to-end retry design.
Why it matters to you
- L7 is where business semantics live: idempotency, retries, auth, rate limits. Lower layers can't make those decisions for you.
- DNS is part of your deployment and failover system. TTLs decide how fast you can move traffic. Lower them before a migration.
- Know which hop generates each 5xx. It cuts incident time in half.
- Every L7 proxy terminates the connection. Client IP, TLS, and timeouts all restart at each hop.
Go deeper in the series
- DNS from raw bytes (
dnsquery) and DNS operations (dnsdiff): TCP/IP Chapters 18 and 55. - HTTP versions and how HTTP/3 is discovered (
protoprobe): HTTPS Chapter 14. - Caching done right (
edgecache, including a cache leaking one user's data to another): HTTPS Chapter 15. - Idempotency keys and retries, measured (
resilience): HTTPS Chapter 21. - 502/504 from a real reverse proxy (
revproxy): HTTPS Chapter 20. - Attacking L7: request smuggling, cache poisoning, SSRF, XXE, races, and uploads, each with a lab: Security Engineering in Depth, Part 6.