The TCP protocol doesn’t actually guarantee delivery; it guarantees efforts towards delivery, using a clever dance of acknowledgments and retransmissions.

Let’s watch this dance in action. Imagine two services, client-app and server-app, communicating over TCP.

# On the server side, we can use tcpdump to see the packets
sudo tcpdump -i any -n 'port 8080'

# On the client side, we'll send some data
echo "Hello, TCP!" | nc server-ip 8080

When nc sends "Hello, TCP!", it’s not just one big chunk. TCP breaks it into segments, each with a sequence number. The server receives these segments and sends back ACKs (acknowledgments) for the data it successfully received, also indicating the next sequence number it expects.

For example, you might see: server-ip.8080 < 192.168.1.10.54321: Flags [P.], seq 1:13, ack 1, win 4096 This means the server is acknowledging receipt of data up to sequence number 12 (seq 1:13 means bytes 1 through 12).

If a segment gets lost (which tcpdump can help us spot as a gap in sequence numbers without a corresponding ACK), the sender won’t receive an ACK for it. After a timeout, it will retransmit that specific segment. This is how TCP achieves reliability.

Order is maintained by those sequence numbers. Even if segments arrive out of order due to network quirks, the receiving TCP stack buffers them and reassembles them according to their sequence numbers before passing the complete data to the application. The ACK number signals not just what was received, but what is next expected, enforcing this order.

Flow control is managed by the "window size" field in TCP segments. The receiver advertises how much buffer space it has available (the receive window). The sender will not send more data than this advertised window allows, preventing it from overwhelming the receiver. If the receiver’s buffer starts filling up, it advertises a smaller window.

Consider a scenario where the server application is slow to read data. The server-app’s TCP receive buffer will fill up. As it fills, the advertised window size in the ACKs sent back to the client will shrink.

You might see ACKs like: server-ip.8080 < 192.168.1.10.54321: Flags [.], seq 13, ack 1, win 2048 and later: server-ip.8080 < 192.168.1.10.54321: Flags [.], seq 13, ack 1, win 1024

The sender will then throttle its transmission rate to match this shrinking window. If the window shrinks to zero, the sender stops sending data entirely until the receiver advertises a non-zero window again.

The initial three-way handshake (SYN, SYN-ACK, ACK) is crucial for establishing these initial sequence numbers and window sizes. It ensures both sides are ready and have agreed on the starting parameters for their communication.

TCP also implements congestion control, which is distinct from flow control. While flow control is about the receiver’s capacity, congestion control is about the network’s capacity. When packet loss is detected (indicating potential congestion), TCP algorithms like slow start and congestion avoidance dynamically adjust the sending rate to avoid overloading the network path.

What most people miss is that the "window" advertised by the receiver is not just about buffer space, but also about data that has been sent but not yet acknowledged. So, if a sender has sent 1000 bytes and received ACKs for 500, and the receiver advertises a window of 2000, the sender can only send an additional 1500 bytes (2000 - 500 = 1500). This is often called the "effective window."

The next fundamental concept to grasp is how TCP handles retransmissions and the potential for duplicate packets.

Want structured learning?

Take the full Tcp course →