I type https://example.com/hello and press Enter. Somewhere a server has the page I want. Between us sit my laptop, a Wi-Fi router, a dozen machines I will never see, and a cable under an ocean. None of them knows what a page is.
So the first job is not delivery. It is agreement. Two programs that have never met need one exact form for “send me this” and one exact form for “here it is”, down to the byte where one field ends and the next begins. That agreed form is a protocol, and the web’s is HTTP.
Everything else here is what it takes to carry that agreed message across the machines in between without it being lost, reordered, altered or read. Each of those is a separate job with its own protocol, and every request carries all of them at once, one wrapped inside the other. That is the sentence to keep: HTTP, TLS, TCP and IP are not alternatives to each other. They are layers.
First, agree on what the message means#
HTTP (the web’s request-and-response protocol) gives the browser and server that agreement. In HTTP/1.1, a request can be this small:
GET /hello HTTP/1.1
Host: example.com
GET means “send me this resource.” /hello identifies it. Host names the site, since one server can serve several names. An empty line ends the headers (the message’s descriptive fields). HTTP/1.1 uses \r\n line endings on the wire.
The server understands those rules and can answer:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 16
hello, internet!
200 reports success. Content-Type says how to interpret the body; Content-Length gives its length in bytes. This body is 16 bytes, with no newline after !. Including headers and line endings, the reply is 96 bytes. These are example messages, not a capture from example.com. HTTP/1.1 message format.
It really is just text. A plain socket, no HTTP library, is enough to speak it:
try (var socket = new Socket("example.com", 80);
var out = socket.getOutputStream();
var in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.write("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
.getBytes(StandardCharsets.US_ASCII));
out.flush();
in.lines().forEach(System.out::println);
}
The socket carries bytes; the request text supplies HTTP’s rules. For HTTPS, the socket must also protect those bytes and check the server’s identity. The connection setup becomes:
try (var socket = (SSLSocket) SSLSocketFactory.getDefault()
.createSocket("example.com", 443)) {
var parameters = socket.getSSLParameters();
parameters.setEndpointIdentificationAlgorithm("HTTPS");
socket.setSSLParameters(parameters);
socket.startHandshake();
}
443 selects the HTTPS service. The default factory uses the configured certificate trust, and the HTTPS identification setting adds the hostname check. startHandshake() must succeed before I send the same HTTP request through this socket. This snippet only establishes and closes the connection; the write/read operations from the first example belong inside it. A factory swap without the name check is not equivalent to a browser’s verification. Java TLS socket verification
The HTTP/1.1 message format stays the same. TLS supplies its protected transport.
Agreeing on words does not deliver them#
Writing a letter and delivering it are separate jobs. The words can be perfectly clear while the letter gets lost, goes to the wrong address, or is opened along the way. An HTTP message has the same problem.
A layer is one part of the delivery system, with a specific responsibility. Its protocol defines how the two sides perform that job.
| Problem still to solve | Job added below HTTP |
|---|---|
| Other machines carry my message. Can they read or alter it? | TLS protects the conversation and checks the server’s identity. |
| Pieces may go missing or arrive out of order. | TCP tracks bytes, restores their order and retries losses. |
| The server is on another network. Where is this going? | IP gives packets source and destination addresses. |
| My laptop can only transmit over its local connection. | Ethernet or Wi-Fi delivers each local handoff. |
| Bytes in memory cannot cross a cable or the air by themselves. | Physical hardware encodes them as signals. |
One giant protocol could combine these jobs. Keeping them separate means a new application can reuse delivery and security instead of building both. It also means changing one part needn’t change all the others.
The separation also gives choice: DNS can use a different transport from HTTP, and TLS can protect email as well as web traffic. The cost is extra headers and coordination. Layers are useful boundaries, not sacred ones; HTTP/3 will combine some of these jobs differently.
Now I can follow the actual exchange. My laptop is already online. I’m using a fresh HTTPS connection with HTTP/1.1, TLS 1.3 and TCP, and illustrative IP addresses. HTTP/2 uses binary framing instead of HTTP/1.1’s text; HTTP/3 also changes the transport.
Before the request can leave#
The URL contains three useful instructions:
| Piece | What my browser does with it |
|---|---|
https:// | Use HTTP over a secure connection; port 443 by default |
example.com | Find an IP address and verify that the server can act for this name |
/hello | Ask that server for this resource |
DNS (the name-to-address system) does the lookup. A resolver (a service that finds DNS answers) returns an address such as 198.51.100.7. It’s like looking up a contact’s phone number: it tells me where to call, not what to say.
A cached answer may be enough. Otherwise the resolver may consult other DNS servers. A DNS TTL (how long an answer may be cached) limits how quickly an address change is normally noticed; it does not itself cause an outage. The /hello path is not part of the lookup. DNS has its own messages and transport: commonly UDP or TCP, or an encrypted connection such as DNS over HTTPS. It isn’t a wrapper around my web request. DNS explanation.
One cached answer, TTL 300 seconds:
0 s 60 s 300 s
cache old IP authoritative answer changes cached answer expires
└─ this cache may still use old IP ─┘
For a planned move, I keep the old destination working while caches drain. Lowering the TTL must happen before the move, because already-cached answers retain their earlier lifetime. An outage depends on whether the old destination still works, not just the TTL. Existing connections have their own lifetime too. Route 53 TTL behavior
My browser needs an IP address for example.com. A cached answer can skip this exchange; otherwise a resolver finds it.
TCP: agree to keep track#
An IP address reaches a host. A port (a numbered endpoint on that host) helps reach the right service: 443 for HTTPS here. My laptop chooses a temporary port, say 51203, for its end.
TCP begins with a handshake (an opening exchange): SYN, SYN-ACK, ACK. The endpoints exchange starting byte numbers and connection options, then confirm they can hear each other. There is no reserved cable between them; each endpoint keeps TCP state. NAT devices and stateful firewalls can keep separate connection-tracking state along the path.
That state will let them track which bytes arrived and resend missing ones. It does not establish the website’s identity or encrypt anything. TCP specification.
The socket can outlive the path’s memory#
A connection pool (a set of sockets kept for reuse) can hold a socket after an intermediate device has forgotten its flow. AWS NAT Gateway expires an idle connection after 350 seconds and returns an RST (an abrupt TCP reset) when the private-side client tries to continue it. NAT idle expiry
I set pool idle eviction below the shortest relevant path timeout, or use supported keepalives with enough margin to keep the needed state alive. A failed connection must be replaced; retrying an operation also requires knowing it is safe to repeat. Keepalives are not one universal fix: an HTTP load balancer can require application data rather than TCP probes.
AWS Network Load Balancer has a 350-second default for TCP flows, but its TCP-listener timeout is configurable; TLS listeners differ. I inspect the actual listener and path rather than treating 350 seconds as a TCP rule. NLB idle settings
Temporary ports are another limit. Linux documents a default automatic port range of 32768–60999: 28,232 choices before reservations. A connection is identified by both endpoint addresses and ports, so this is not a universal total-connection limit. Repeated short connections to one destination can run out of available combinations, especially while TIME_WAIT (state retained after an active close) delays reuse. I reuse connections and inspect port allocation before reaching for kernel tuning. Linux port allocation
TLS: establish trust and keys#
TLS (the protocol that protects the conversation) has two different problems to solve: agree on secret keys, and make sure the other end can act for example.com.
There is a puzzle in the first of those. How can my browser encrypt anything for a server it has never spoken to? It cannot, and it does not try. The ClientHello goes out in the clear, carrying the browser’s half of a key agreement: think of two people who each mix a private colour into the same base paint and swap the results in public. Each adds their own private colour to the other’s mix and lands on the same final shade, while anyone watching the swap holds two mixes and no way to un-mix them.
In a typical fresh TLS 1.3 handshake, those public key shares travel in ClientHello and ServerHello. Each side combines the other’s share with its own private value to derive shared secrets. The keys themselves aren’t sent.
The browser checks the certificate’s name, validity and chain of trust, plus the server’s signature proving possession of the corresponding private key. Finished messages check the handshake’s integrity. Application data then travels in encrypted records (protected blocks of bytes), with authentication tags that detect tampering. TLS 1.3.
TLS hides the request’s path and content from ordinary forwarding devices. It doesn’t hide destination IP addresses, sizes or timing. The hostname may also be exposed through DNS or the handshake, depending on which protections are used.
One message, several delivery jobs#
The reply travels back through the same kinds of layers as the request. I can stop it inside the server and watch each job add its information.
96 bytes: status line, headers, blank line, and the 16-byte body. This is the message before encryption.
TLS protects the content. In this example, 96 HTTP bytes become a 118-byte TLS record: 5 bytes of header, 96 encrypted content bytes, an encrypted 1-byte content type, and a 16-byte authentication tag. This assumes no padding and a cipher with a 16-byte tag.
TCP numbers the bytes. Its header identifies the ports and where this piece belongs in the stream. A segment (a piece of the TCP stream with its header) doesn’t have to contain a whole HTTP message or a whole TLS record.
IP supplies the destination. Its header carries source and destination IP addresses. A packet (an IP header and its contents) can cross many networks. IP doesn’t promise arrival or order; that is why this connection also needs TCP.
Ethernet handles the local handoff. A frame (a link’s header and payload, plus its error check) names the sending and next receiving interfaces using MAC addresses (addresses used on the local link). Wi-Fi does a similar job with a different frame format.
The physical link carries signals. The network hardware encodes the frame into electrical, optical or radio signals. A bit isn’t necessarily one pulse; the encoding depends on the link technology.
The sizes aren’t universal. This example uses minimal 20-byte IPv4 and TCP headers and an untagged Ethernet frame. Together they make 176 bytes, excluding Ethernet’s preamble and the gap between frames.
The next device is not the final destination#
A parcel keeps its destination address as couriers hand it onward. Each handoff still has an immediate recipient. IP identifies the destination; the link header identifies the next local recipient.
The router reads the destination IP and looks up the next hop. It decreases TTL (the packet’s remaining hop allowance), updates the IPv4 header checksum, and sends a new frame on the outgoing link. TTL stops a packet circulating forever. A switch normally forwards within a link using MAC addresses. IPv4 forwarding.
Finding the next interface’s MAC address is another local lookup: ARP for IPv4, Neighbor Discovery for IPv6. For a remote server, my laptop looks up its gateway’s MAC, not the server’s.
These are forwarding roles, not limits on what hardware can inspect. The table shows what each job needs to understand; a device can inspect more when the bytes are visible:
| Box on the path | Opens up to | What it changes |
|---|---|---|
| Switch, Wi-Fi access point | Ethernet | nothing; forwards by MAC |
| Router | IP | new frame, TTL minus one, new IP checksum |
| Home router doing NAT (address translation) | TCP/UDP | rewrites address and port so many devices share one address |
| Firewall | IP and ports; some inspect plain HTTP | drops by address, port, protocol |
| Load balancer | ports, or TLS and HTTP | may terminate TLS and read the path |
| CDN edge (a service that serves content near users) | HTTP | terminates TLS, answers from cache, opens its own connection to the origin |
| My laptop’s kernel | TCP | strips each header, hands bytes to the socket for port 51203 |
| My browser | TLS and HTTP | decrypts, parses, renders |
“Encrypted end to end” depends on where those ends are. A CDN that terminates TLS is one of them.
That boundary decides how I balance traffic. An L4 balancer (one that routes transport connections) uses addresses and ports; it does not route HTTP paths. An L7 proxy (one that understands application messages) can terminate TLS and inspect Host and /hello, then open a separate backend connection. That enables HTTP routing or request filtering; caching depends on the chosen product.
| Connection | Who can read HTTP? | Client identity at the backend |
|---|---|---|
| TCP forwarding, TLS ends at the backend | Backend | Source IP preservation depends on the forwarding mode |
| TLS ends at an HTTP proxy; another connection reaches the backend | Proxy and backend | Often the proxy’s socket address; original client IP can be conveyed separately |
An ALB can pass the original address in X-Forwarded-For. I trust forwarded identity only from known proxies with a defined header policy; accepting an arbitrary caller’s header would let them invent their address. PROXY protocol is a different option supported by some other setups. ALB forwarded headers
How routers learn their routes, including BGP between networks, is a separate question. My trace to 1.1.1.1 follows that part.
Inspect the 176-byte example
A constructed example, not a packet capture. These are three Ethernet links with no NAT. Ciphertext is illustrative; IPv4, TCP and Ethernet checksums are calculated. Real captures often omit Ethernet’s trailing checksum.
| version / header length | 45 | IPv4, 5 words = 20 bytes |
| DSCP / ECN | 00 | 0 = no special treatment asked |
| total length | 00 9e | 158 bytes, header through payload |
| identification | 3a 7c | 0x3a7c, used only if fragmented |
| flags / fragment offset | 40 00 | DF set: do not fragment me |
| TTL | 40 | 64 hops left before a router drops it |
| protocol | 06 | 6 = TCP is inside (17 would be UDP) |
| header checksum | 99 99 | 0x9999, recomputed each hop because TTL changed |
| source address | c6 33 64 07 | 198.51.100.7 (the server) |
| destination address | cb 00 71 09 | 203.0.113.9 (the laptop) |
What if part of the reply disappears?#
TCP is like receiving numbered pages: page 3 may arrive before page 2, but reading them in arrival order would change the story. TCP tracks byte positions, acknowledges progress and retransmits missing data.
Bytes 1–4 arrive. Bytes 9–12 also arrive, but TCP keeps them waiting because bytes 5–8 are missing.
That ordering has a cost: later bytes wait behind a gap, even if they have arrived. TCP also adjusts its sending rate to avoid overwhelming the receiver or network. It cannot guarantee eventual delivery through a permanent failure. TCP behavior.
Why a large reply becomes many packets
A link has an MTU (the largest IP packet it carries). With a 1,500-byte MTU and minimal IPv4 and TCP headers, at most 1500 − 20 − 20 = 1460 TCP payload bytes fit. Options, IPv6 and tunnels change that budget.
HTTP can stream a body, TLS protects blocks of it, and TCP carries the resulting stream in segments. The whole response doesn’t need to exist in memory first. One HTTP message can span many TLS records and segments; a socket read can return only part of a record.
A small request works; the upload hangs#
Path MTU (the smallest packet limit along a route) can be lower than the local interface’s limit. For example, VXLAN over IPv4 adds 50 bytes around an inner IP packet, including its inner Ethernet header. With a 1,500-byte underlying MTU, that leaves 1,450 bytes for the inner IP packet. This is one overlay configuration, not every Kubernetes network; Calico documents different budgets for IPv6 and other tunnel types. Calico MTU choices
I configure the workload and tunnel MTUs for their actual overhead, and permit the ICMP feedback that discovery needs: IPv4 “fragmentation needed” or IPv6 “Packet Too Big.” Implementations with packet-size probing can sometimes recover without that feedback, but blocking it can create a PMTU black hole (large packets disappear without usable size feedback). IPv4 discovery, IPv6 discovery
Back into the browser#
At my laptop, the network hardware receives the frame. The operating system handles IP and TCP, finds the connection using its addresses and ports, and supplies an ordered byte stream through a socket (the program’s interface to the connection).
sequenceDiagram participant K as OS / TCP participant T as Browser TLS participant H as Browser HTTP K->>T: ordered bytes Note over T: Buffer a record<br/>Decrypt and verify T->>H: HTTP bytes Note over H: Parse headers<br/>Consume body
The browser gets hello, internet!. If the body were HTML, parsing it could trigger more requests for images, styles and scripts. The same connection can carry later requests; opening a new connection for every file would repeat expensive setup.
For a simple fresh TCP + TLS 1.3 connection, the first response takes roughly three round trips: TCP setup, TLS setup, then the HTTP exchange. At an assumed 50 ms round-trip time, that’s about 150 ms, before DNS, server work or losses. Reusing an established connection skips the first two. With no retry or loss, HTTP/3 can reduce this to roughly two round trips for a fresh connection. A resumed connection with accepted early data can reach one; neither figure includes DNS or server work.
The proxy and backend need compatible clocks#
Reusing a connection assumes the other end will still accept a request. An ALB defaults to a 60-second idle timeout. If my backend closes its idle socket earlier, a request can race with that closure and produce a 502 Bad Gateway (the proxy could not obtain a usable backend response).
AWS recommends an application idle timeout larger than the ALB’s. For example, I might choose 75 seconds at the backend and 60 at the ALB, with a margin appropriate to the system. That addresses this race, not every cause of 502. The ALB’s separate HTTP client keepalive-duration setting is not this idle timer, and TCP keepalive probes do not substitute for application data to reset it. ALB timeout behavior, ALB troubleshooting
Where the seven-layer names fit#
The OSI model (a seven-layer reference model for communication) names these responsibilities. I find the numbers useful once each job has a concrete place in the request.
| OSI layer | The job in this story |
|---|---|
| 7 · Application | HTTP asks for a resource; DNS looks up a name |
| 6 · Presentation | Represent and protect data; TLS covers part of this role |
| 5 · Session | Manage a conversation; this web stack has no separate session-layer protocol |
| 4 · Transport | TCP delivers an ordered stream between endpoints |
| 3 · Network | IP addresses packets across networks |
| 2 · Data link | Ethernet or Wi-Fi carries a frame across a local link |
| 1 · Physical | Hardware carries the bits as signals |
This is a map of responsibilities, not a requirement for seven headers. TLS doesn’t fit neatly into one numbered box. Neither does QUIC (a secure transport carried over UDP), which HTTP/3 uses in place of TCP and the separate TLS record layer.
What HTTP/3 changes about the stack
| HTTP/1.1 or HTTP/2 over HTTPS | HTTP/3 |
|---|---|
| HTTP → TLS records → TCP → IP → link | HTTP/3 → QUIC → UDP → IP → link |
| One ordered TCP byte stream | Several independently ordered QUIC streams |
| Missing bytes hold up later bytes in that TCP stream | A gap in one stream need not hold up delivery on another |
UDP supplies ports and datagrams (separately delivered messages), without TCP’s ordering or retries. QUIC implements reliable streams, loss recovery and congestion control above it, and uses the TLS 1.3 handshake to establish keys. UDP didn’t become reliable; QUIC added the missing jobs. Using UDP makes QUIC easier to deploy through existing networks than a new IP transport number, though some networks still block UDP and require a fallback.
QUIC integrates transport setup with TLS key establishment, so a normal fresh handshake takes one round trip. A return visit may use a previous ticket for 0-RTT (sending application data before waiting for the new handshake), if both sides permit it. The server can reject early data. TLS in QUIC
Early data has a replay risk: a captured request might be processed more than once. A client must not assume an unsafe operation is suitable; even a GET can have effects if the application misuses it. The server can reject early data, delay processing, or return 425 Too Early so the client retries after the handshake. “Return visit” is not permission to replay a payment. HTTP early-data rules
Streams still share network capacity and congestion control. HTTP dependencies can cause waiting too. QUIC removes a particular kind of transport-level blocking, not every reason a page can stall. QUIC’s design.
The question I keep beside this stack is: what is each piece responsible for? HTTP understands /hello. TLS protects the conversation. TCP notices missing bytes. IP carries the destination. The local link gets the packet to the next device. None of them has to do every job.
Comments
Signed in with GitHub. Be kind.