The layered model, and how it maps to OSI

TCP/IP is usually described as four layers, each with a narrow job. The link layer (Ethernet, Wi-Fi) moves frames between two devices on the same physical network and worries about MAC addresses and the wire. The internet layer (IP) moves packets between any two hosts across many networks, hop by hop. The transport layer (TCP, UDP) provides communication between application processes, adding reliability and multiplexing. The application layer (HTTP, DNS, SMTP) is your actual protocol. The genius of the arrangement is that each layer talks only to its peer on the other machine and to the layers directly above and below it — nothing else.

The older OSI model splits the same territory into seven layers, and the two are worth lining up because people mix the vocabularies constantly (‘a layer-7 load balancer,’ ‘an L4 firewall’):

OSI layerTCP/IP layerExample
7 Application / 6 Presentation / 5 SessionApplicationHTTP, TLS, DNS
4 TransportTransportTCP, UDP
3 NetworkInternetIP, ICMP
2 Data link / 1 PhysicalLinkEthernet, Wi-Fi

OSI is a teaching abstraction that never shipped as a stack; TCP/IP is the one that actually did. When someone says ‘layer 4,’ they mean transport (TCP/UDP); ‘layer 7’ means the application protocol. Keep the mapping loose — the point is the separation of concerns, not the exact count.

Advertisement

Encapsulation: every layer wraps the one above

Data does not travel down the stack unchanged — at each layer it is encapsulated, meaning the layer wraps whatever it received from above inside its own header (and sometimes a trailer). The layer above becomes an opaque payload that the current layer neither inspects nor modifies. This is the mechanism the lab animates, and it is worth naming the units precisely because the vocabulary is used everywhere:

Application data
  → + TCP header          = segment
     → + IP header         = packet (a.k.a. datagram)
        → + Ethernet hdr/FCS = frame  → onto the wire

On the receiving host the process runs in reverse, called decapsulation: the NIC hands the frame to the link layer, which strips the Ethernet header, checks the frame-check sequence, and passes the packet up; IP strips its header and passes the segment up; TCP strips its header, reassembles the byte stream in order, and delivers clean data to the application. Because each layer reads only its own header, you can carry TCP or UDP over IP interchangeably, run IP over Ethernet or Wi-Fi or a VPN tunnel, and swap any layer without disturbing the others. Encapsulation is what makes the stack modular rather than a monolith — and it is why a single misbehaving layer (a wrong MTU, a dropped TTL) can be reasoned about in isolation.

Advertisement

IP: addressing and best-effort delivery

The Internet Protocol has exactly one job: get a packet from a source address to a destination address across an arbitrary mesh of networks. It does this on a best-effort basis, which is a polite way of saying it makes no guarantees. A packet may be lost, duplicated, delayed, or delivered out of order, and IP will not tell you. Every packet carries a source and destination IP address, and routers forward it hop by hop: each router looks only at the destination address, consults its routing table, and sends the packet one step closer, with no memory of the packet before or after. IP is stateless and connectionless — there is no ‘IP connection.’

Two header fields matter constantly in practice. The TTL (time-to-live) is decremented by every router; when it hits zero the packet is discarded and an ICMP ‘time exceeded’ is returned — this is exactly how traceroute maps a path, by sending packets with increasing TTLs. Fragmentation handles the case where a packet is larger than a link’s MTU: IPv4 routers could split it into fragments to be reassembled at the destination, though modern practice strongly prefers path-MTU discovery to avoid fragmentation entirely. On the addressing itself, IPv4 offers 32-bit addresses (about 4.3 billion, long since exhausted, which is why NAT is everywhere), while IPv6 offers 128-bit addresses, drops router fragmentation, and bakes in autoconfiguration. The transport layer above is what turns this unreliable, connectionless substrate into something an application can trust.

TCP: what a reliable ordered byte stream buys you

If IP gives you a firehose of independent packets that may or may not arrive, TCP gives you a clean pipe. The specification’s own phrase — a reliable, ordered, connection-oriented byte stream — packs in four distinct promises, and it is worth unpacking each because together they are the entire value proposition.

Reliable means every byte you send arrives, or you find out the connection failed; TCP detects loss and retransmits so the application never has to. Ordered means bytes are delivered to the application in the exact order they were sent, even though the underlying packets may have taken different routes and arrived scrambled — TCP buffers and reassembles. Connection-oriented means the two endpoints establish shared state (sequence numbers, window sizes) via a handshake before any data flows, so both sides agree on where the conversation starts. Byte stream means TCP presents the data as a continuous flow of bytes with no message boundaries — if you call write() twice, the receiver may read it in one chunk or three; preserving your own message framing is the application’s job, a fact that surprises people the first time a JSON blob arrives split across two reads. Everything else in this article — the handshake, ACKs, windows, congestion control — exists to deliver those four promises on top of an IP layer that offers none of them.

The 3-way handshake in detail

Before a single byte of data crosses a TCP connection, the two sides run a three-way handshake to synchronize. This is the second thing the lab animates, and each message does specific work:

Client → Server:  SYN      seq = x            (I want to talk; my ISN is x)
Server → Client:  SYN-ACK  seq = y, ack = x+1 (OK; my ISN is y; got your x)
Client → Server:  ACK      ack = y+1          (got your y — we are ESTABLISHED)

The SYN (synchronize) flag marks the opening packets, and each side announces an initial sequence number (ISN). A natural question is why the ISN is not simply zero. It is deliberately randomized for two reasons. First, security: a predictable ISN lets an off-path attacker guess valid sequence numbers and inject or spoof data into a connection, so randomization raises the bar substantially. Second, correctness: if a new connection reused the same four-tuple as a recently closed one, stray packets from the old connection could be mistaken for valid data in the new one; a fresh random ISN makes that collision astronomically unlikely. Notice the acknowledgement is always the peer’s sequence number plus one — TCP acknowledges the next byte it expects, and the SYN flag notionally consumes one sequence number, so acking x+1 says ‘I have your SYN, send me byte x+1 next.’ After the third packet both sides are in the ESTABLISHED state and data flows in both directions.

Reliability: sequence numbers, ACKs, and retransmission

TCP’s reliability rests on a small set of cooperating mechanisms, all built on the sequence numbers agreed during the handshake. Every byte in the stream has a sequence number, so the receiver can order arriving segments and detect gaps and duplicates. The receiver sends acknowledgements that are cumulative: an ACK of N means ‘I have received every byte up to but not including N,’ which compactly confirms a whole run of data in one number and tolerates lost ACKs, since a later ACK subsumes an earlier one.

When an ACK does not arrive, the sender falls back on a retransmission timeout (RTO): each segment is sent with a timer derived from the measured round-trip time and its variance, and if the timer fires before the ACK, the segment is resent. Waiting for a timeout is slow, so TCP adds fast retransmit: because a cumulative ACK repeats the same value when out-of-order data arrives, three duplicate ACKs for the same sequence number are a strong signal that one segment was lost while later ones got through, and the sender retransmits immediately without waiting for the RTO. Underpinning all of this is the checksum in the TCP header, which covers the header and payload (plus a pseudo-header of IP addresses) so that corrupted segments are detected and dropped rather than delivered as good data. Selective acknowledgement (SACK), a widely used option, lets the receiver report exactly which non-contiguous blocks it holds, so the sender resends only the true gaps instead of everything after the loss.

Flow control: the sliding receive window

Reliability makes sure data arrives; flow control makes sure the sender does not overwhelm the receiver. The mechanism is the sliding window. In every ACK, the receiver advertises a receive window (rwnd): the number of bytes it still has buffer space to accept beyond the last byte it acknowledged. The sender may have at most that many unacknowledged bytes ‘in flight’ at once. As the receiving application drains the buffer, the window slides forward and reopens; if the application stalls and the buffer fills, the advertised window shrinks toward zero and the sender pauses until space frees up. This is a pure back-pressure signal from receiver to sender, entirely separate from the network’s capacity.

The original TCP header reserved only 16 bits for the window, capping it at 65,535 bytes — fine in the 1980s, disastrous on a modern fat, long link. The amount of data you must keep in flight to saturate a path is its bandwidth-delay product: a 1 Gbps link with 80 ms RTT needs roughly 10 MB in flight, and a 64 KB window would leave it more than 99% idle. The fix is window scaling, a handshake option that applies a left-shift multiplier to the advertised window, raising the ceiling to hundreds of megabytes. This is why window scaling is not a nicety but a requirement on any high-bandwidth, high-latency connection — long-haul, satellite, or cross-continent links — and why a capture that shows scaling disabled explains an otherwise baffling throughput ceiling.

Congestion control: slow start and AIMD

Flow control protects the receiver; congestion control protects the network in between, which no single endpoint can see directly. The distinction trips people up, so state it plainly: flow control is limited by rwnd (the receiver’s buffer), congestion control by the congestion window (cwnd, the sender’s estimate of what the path can carry), and the amount actually in flight is bounded by the minimum of the two. The receiver advertises rwnd; cwnd is inferred entirely from feedback, because the network never tells you its capacity — it only drops packets when you exceed it.

A new connection has no idea how fast the path is, so it begins with slow start: cwnd starts small and doubles every round trip, probing exponentially upward until it either reaches a threshold or sees loss. After that it switches to congestion avoidance, which follows AIMD — additive increase, multiplicative decrease. On success the window grows linearly (add roughly one segment per RTT), gently probing for more room; on a loss it is cut multiplicatively (classically halved), backing off hard. This gentle-up, sharp-down asymmetry is what makes many independent TCP flows converge to a fair, stable share of a shared link without any central coordinator. Modern algorithms refine the reaction — CUBIC scales better on fat pipes, and BBR models bottleneck bandwidth and RTT directly rather than treating every loss as congestion — but the core idea of a self-clocking window driven by feedback is unchanged.

TCP vs UDP: reliability vs speed

TCP is not the only transport. UDP, the User Datagram Protocol, is a thin wrapper over IP that adds essentially nothing but ports and a checksum: no handshake, no acknowledgements, no ordering, no congestion control. It hands the application raw datagrams and lets them fend for themselves. That sounds like a downgrade until you see what it buys — no connection setup latency, no head-of-line blocking, and full control over retransmission policy:

PropertyTCPUDP
ConnectionHandshake firstConnectionless
ReliabilityGuaranteed, retransmitsNone — app’s job
OrderingIn-order byte streamUnordered datagrams
Overhead20+ byte header, state8-byte header, stateless
Best forWeb, APIs, files, mailDNS, VoIP, games, video

The rule of thumb: choose TCP when correctness matters more than the last millisecond (a web page, an API call, a file transfer, email), and UDP when timeliness matters more than any single lost packet (live voice and video, gaming, DNS lookups). The most interesting modern twist is QUIC, the transport under HTTP/3: it runs over UDP yet rebuilds reliability, ordering, and congestion control in user space, adding TLS 1.3 and per-stream multiplexing that dodges TCP’s head-of-line blocking. QUIC is effectively ‘a better TCP’ delivered as UDP payload precisely so it can evolve without waiting for every kernel and middlebox on earth to be upgraded.

Ports, sockets, and the 4-tuple

A single host runs thousands of simultaneous connections — dozens of browser tabs, background sync, a database pool — all over the same physical link and often the same IP address. Ports are how the transport layer keeps them apart. A port is a 16-bit number that identifies a specific process endpoint; servers listen on well-known ports (80 for HTTP, 443 for HTTPS, 22 for SSH), while clients are assigned a temporary ephemeral port for each outbound connection.

The crucial insight is that a TCP connection is identified not by a port but by the full 4-tuple: (source IP, source port, destination IP, destination port). The kernel demultiplexes every arriving segment by looking up this exact quadruple to find the one socket it belongs to. This is why a busy web server can hold hundreds of thousands of connections on port 443 without ambiguity — each client contributes a distinct source IP and source port, so every 4-tuple is unique even though the server’s side is identical. It also explains a real scaling limit: a single client talking to a single server IP and port can open at most ~64K connections before it exhausts its ephemeral source ports, which is why high-fan-out load generators and proxies spread across multiple source addresses. A socket is simply the operating system’s handle to one endpoint of one such connection — the API through which your program reads and writes the byte stream.

Connection teardown: FIN, half-close, and TIME_WAIT

Closing a TCP connection is more involved than opening one, because the byte stream is bidirectional and each direction is shut down independently. A graceful close uses the FIN flag: one side sends FIN to say ‘I have no more data,’ the peer ACKs it, and when the peer is likewise done it sends its own FIN, which the first side ACKs — a four-message exchange (often seen as FIN/ACK, FIN/ACK). Because the directions are separate, TCP supports the half-close: one side can finish sending while still receiving, which is how a client can signal end-of-request yet keep reading the response.

The subtle part is TIME_WAIT. The endpoint that sends the final ACK does not free the connection immediately; it lingers in TIME_WAIT for twice the maximum segment lifetime (2 MSL, often on the order of a minute). This exists for two reasons: to guarantee the final ACK can be retransmitted if the peer’s last FIN was lost, and to let any straggling packets from this connection die off before the same 4-tuple could be reused, preventing old data from polluting a new connection. It is correct and necessary — but on a busy server that initiates many short-lived outbound connections, thousands of sockets piling up in TIME_WAIT can exhaust ephemeral ports. The right fixes are architectural (reuse connections with keep-alive, or arrange for the client rather than the server to be the side that closes), not blindly slashing the timeout, which reintroduces the very hazards TIME_WAIT prevents.