TCP, how reliable byte streams run over unreliable IP
IP delivers packets with no guarantees, and TCP turns that into an ordered, reliable stream of exactly the bytes you sent. It does this with sequence numbers, acknowledgements and retransmission.
What does TCP guarantee?
Three guarantees on top of an unreliable network.
TCP (Transmission Control Protocol) provides three guarantees on top of the unreliable IP layer: reliable, ordered, byte-stream delivery. Vint Cerf and Bob Kahn specified it in 1974; RFC 793 standardised it in 1981, refreshed as RFC 9293 in 2022. TCP underlies HTTP/1, HTTP/2, SSH, SMTP, FTP, and most other application protocols.
IP packets can arrive duplicated, out of order, or not at all. TCP layers a reliable, ordered byte-stream abstraction on top of that, and it has been refined for decades.
Reliable
Every byte you send is acknowledged or retransmitted. If a packet drops at a router five hops in, TCP notices the gap and resends, without the application knowing.
Ordered
Bytes arrive in exactly the sequence they were sent. The receiver buffers out-of-order segments and waits, because the OS will not deliver byte 1000 to the application until byte 1 has been delivered.
Byte-stream
There are no messages in TCP. The sender writes 100 bytes; the receiver may read 80 then 20, or 100 in one shot. Application protocols layered on TCP must add their own framing, which is exactly what HTTP, TLS, and Redis all do.
TCP three-way handshake
A connection isn't open until both sides know two things: their own initial sequence number and the peer's. Two packets synchronize only one direction, so synchronizing both directions and proving the round trip works takes three.
Initial Sequence Numbers are chosen unpredictably rather than starting at zero. If a router delays a packet for two minutes and finally delivers it, a random ISN makes it very unlikely that the new connection accepts the stale packet. With predictable ISNs, a long-lost packet from a closed connection could inject corrupted bytes into a live database query.
Three packets, one round trip
Client SYN announces its ISN. Server SYN-ACK announces its own ISN and acknowledges the client's. Client ACK confirms. After this third segment, both sides are in ESTABLISHED; data may flow.
MSS · SACK · window scaling
The handshake also negotiates options: maximum segment size (so neither side overshoots the path MTU), selective acknowledgement (so a single dropped packet doesn't stall the whole window), and window scaling (so 16-bit windows can advertise gigabytes).
The full life of a TCP connection: open, talk, close
The simulator below plays the entire lifecycle: three-way handshake, a single request/response data exchange, and the four-way teardown. Use the phase tabs to jump between phases, the player to scrub, and + Show wire bytes to read the segment header values at each step.
The client starts with a SYN (synchronize). It picks a random Initial Sequence Number (ISN) so that any stale packet from a long-dead connection cannot be mistaken for live data. The window field offers receive-buffer space; options negotiate MSS, selective ACK, and window scaling.
TCP sequence numbers and cumulative ACKs
Every byte gets a number. The sequence number in a TCP header is the number of the first byte of the payload. The acknowledgement number is the next byte the receiver expects.
The ACK is cumulative: ack=1018 means "every byte up to and including 1017 has arrived." If the receiver got bytes 1–100 then bytes 201–300, it does not ack 300. It keeps acking 101, repeatedly, to tell the sender there is a hole. When the sender sees three duplicate ACKs in a row, it doesn't wait for its retransmit timer. It triggers fast retransmit and resends the missing segment immediately.
This is why packet loss over TCP shows up as a delay rather than as corrupted data.
Cumulative ACK alone has a downside. If bytes 200–299 are missing but 300–999 arrived fine, the sender doesn't know the rest of the window is intact and may retransmit too much. SACK (RFC 2018) lets the receiver describe non-contiguous ranges: "I'm missing 200–299, but I have 300–999." It is negotiated in the SYN and is on by default in current stacks.
The sliding window, and how flow control works
A fast sender talking to a slow receiver can overflow the receiver's buffer, and TCP must not lose bytes the receiver's OS has already accepted. The defence is the receive window, a 16-bit field in every TCP header that advertises how much buffer space is free right now.
A mobile phone whose CPU is briefly pinned can advertise a window of zero, and the server stops sending mid-flight however big the file is. As the phone drains its buffer it advertises a larger window, possibly in a small "window update" segment, and transmission resumes. Flow control protects the receiver, while congestion control protects the network, and the two run as independent feedback loops at the same time.
The 16-bit window field maxes out at 65 535 bytes, which is small for high-bandwidth links. The window scaling option, negotiated at SYN time, lets each side multiply the field by 2n, so windows of up to about a gigabyte are possible.
Congestion control: slow start, AIMD, and BBR
Above flow control sits congestion control, the sender's own estimate of how fast it can transmit before a router drops packets. That estimate is the congestion window (cwnd). The sender may have at most min(cwnd, rwnd) bytes in flight at any moment.
- 01
Slow start
A new connection starts with a cwnd of about ten segments. Each ACK grows the window by one segment, so the window doubles every round trip, until either a packet drops or cwnd reaches the slow-start threshold (ssthresh). Despite the name, this is the fastest-growing phase.
- 02
Congestion avoidance
Past ssthresh, growth becomes additive: cwnd increments by one segment per RTT. Each successful round trip adds a single segment of window. This is the Additive Increase half of AIMD.
- 03
Multiplicative decrease
When loss is detected (three duplicate ACKs or a timeout), cwnd is cut, often in half, and the sender then resumes additive growth. Together the two halves are Additive Increase, Multiplicative Decrease.
- 04
BBR · model-based
Classic AIMD treats loss as the only signal. BBR (Bottleneck Bandwidth and Round-trip propagation time) is Google's alternative, used on YouTube, Cloudflare's edge and a growing number of Linux servers. It estimates the bottleneck capacity and minimum RTT, then paces transmission to match. Throughput stays high under loss and queues stay shallow.
TCP head-of-line blocking
TCP's strict in-order delivery is also its main architectural limitation. If you multiplex five HTTP/2 streams over a single TCP connection and the very first segment of stream one is dropped, TCP halts everything. The OS withholds the valid, already-arrived bytes for streams two to five, because it must deliver byte one to the application before byte one thousand.
This cannot be fixed inside TCP without breaking its in-order guarantee. HTTP/3 therefore moved off TCP: QUIC reimplements TCP's reliability on top of UDP, but with independent per-stream sequence numbers, so a loss on one stream stalls only that stream. See the HTTP guide for more.
TCP four-way teardown and TIME_WAIT
Closing a connection is half-duplex. Either side can declare "I have no more data to send" with a FIN, and the other end keeps sending until it sends its own FIN. That gives four control segments: FIN, ACK, FIN, ACK. The middle two often ride together, with the server's ACK piggybacked on its FIN, but conceptually there are four phases.
After the final ACK, the closing side sits in TIME_WAIT for two maximum-segment-lifetimes (2 × MSL, typically 60–240 s). This is deliberate. A duplicate FIN delayed in some router could otherwise arrive long after the connection has closed and confuse a new connection that reuses the same four-tuple. TIME_WAIT keeps the kernel holding the four-tuple until any stragglers have expired.
Servers under enormous churn can run out of ephemeral ports waiting for TIME_WAITs to clear. Common remedies are tuning tcp_tw_reuse and reusing connections via keep-alive.
The TCP knobs operators tune
The settings operators change most often.
- tcp_congestion_control
- The algorithm. Linux defaults to CUBIC; production high-throughput setups (Google, Cloudflare) often switch to BBR. Set globally: sysctl net.ipv4.tcp_congestion_control=bbr.
- tcp_rmem / tcp_wmem
- Receive and send buffer sizes (min/default/max). Defaults are tuned for ~10 ms RTT; for transcontinental connections at 150 ms RTT, raising max to ~16 MB allows TCP to keep the pipe full.
- tcp_fastopen
- Enables TFO (RFC 7413). Saves an RTT on repeat connections. Set to 3 (both client and server). Mostly transparent to applications.
- tcp_tw_reuse
- Allow reuse of TIME_WAIT sockets for new connections. Important for high-RPS services hitting port exhaustion. Set to 1 cautiously (correctness depends on timestamp option being on).
- tcp_keepalive_time / _intvl / _probes
- Keepalive cadence. The defaults of 7200/75/9 mean the OS notices a dead peer about 2.2 hours after it goes silent. If your clients are unreliable mobile or IoT devices, tune down to 60/10/3.
- net.core.somaxconn
- Listen-backlog ceiling. Default 128; serving 100k+ accepts/sec needs 4096 or more.
Kernel versions. Recent Linux kernels add improvements such as TCP zero-copy (SO_ZEROCOPY) and TLS offload in the kernel (kTLS). They make little difference on a single laptop but add up at fleet scale. Cloudflare, Netflix and Meta run custom-tuned kernels.
TCP is forty-five years old and still carries most of the internet's traffic. The 1981 spec left delivery to IP and put everything else in the endpoints, and that everything else has since gained SACK, window scaling, BBR, fast open and multipath. At its core TCP is sequence-number bookkeeping, and that is enough to explain most of its behaviour.
Further
reading
- IETF · RFC 9293 (2022)Transmission Control ProtocolThe current TCP spec, consolidating forty years of patches into one document.
- IETF · RFC 5681TCP Congestion ControlThe standard AIMD algorithms: slow start, congestion avoidance, fast retransmit and fast recovery.
- IETF · RFC 2018TCP Selective AcknowledgementSACK, which makes loss recovery efficient when more than one segment is missing in flight.
- Google ResearchBBR: Congestion-Based Congestion ControlThe original BBR paper, which treats congestion as a bandwidth and RTT estimation problem instead of a loss-detection one.
- Semicolony guideHTTP, the shape of itWhy HTTP/3 moves from TCP to QUIC. Pairs with Part 07.
- Semicolony guideHTTPSWhat the TLS handshake adds on top of TCP: one extra round trip in TLS 1.3, two in 1.2.