Blog
What the TCP Three-Way Handshake Actually Negotiates
TCP turns an unreliable packet network into an ordered, reliable byte stream, and it all starts with three small segments that synchronize two machines.
Every Node server you start with app.listen and every Postgres connection your Go service opens rides on TCP, yet most backend developers treat it as an opaque pipe. TCP is the Transmission Control Protocol, and its job is to take IP, which delivers packets with no guarantee of order or arrival, and build a reliable, ordered, full-duplex byte stream on top. Before any application data flows, the two endpoints must agree on starting sequence numbers, confirm both directions work, and exchange options like maximum segment size and window scaling. That negotiation is the famous three-way handshake. Understanding it explains real production behavior: why a connection hangs on SYN, why dropped packets cause latency spikes rather than data corruption, and why connection setup cost matters for high-throughput services that open many short-lived sockets.
Why TCP Needs A Handshake
IP delivers individual packets but makes no promises. Packets can arrive out of order, be duplicated, get corrupted, or vanish entirely. TCP's contract is the opposite: bytes arrive exactly once, in the order they were sent, with errors detected and retransmitted. To deliver that, both sides must track every byte using sequence numbers, and they must agree on where counting begins. If each side simply guessed a starting point, a delayed packet from an old connection could be mistaken for fresh data, corrupting the stream. The handshake exists to synchronize these initial sequence numbers and to prove that both the outbound and return paths actually work before any payload is committed. It is essentially a mutual agreement: I can hear you, you can hear me, and here is the number I will start counting from.
The SYN Segment
The client begins by sending a segment with the SYN flag set, which stands for synchronize. This segment carries the client's initial sequence number, a randomly chosen value rather than zero, which makes it harder for an attacker to inject forged packets into the stream. The SYN also advertises options the client supports: the maximum segment size it can receive, whether it understands window scaling for high-bandwidth links, selective acknowledgment support, and sometimes timestamps. At this point the kernel moves the connection into the SYN_SENT state and starts a retransmission timer. If your client appears to hang here, the cause is usually a firewall silently dropping the SYN or the destination port having no listener, in which case you would normally get a reset instead of silence.
The SYN-ACK Response
When the server receives a valid SYN on a listening port, it replies with a single segment that has both the SYN and ACK flags set. The ACK acknowledges the client's sequence number by sending back that number plus one, confirming the server received the client's opening byte position. The SYN portion carries the server's own randomly chosen initial sequence number, because TCP is full-duplex and each direction needs independent sequencing. The server also echoes its own supported options, so the final agreed maximum segment size is the smaller of the two advertised values. Internally the server places this half-open connection on a queue. This stage is what SYN flood attacks abuse: by sending many SYNs and never completing the handshake, an attacker can exhaust that queue, which is why SYN cookies were invented to avoid storing state.
The Final ACK
The client completes the handshake by sending a pure ACK that acknowledges the server's initial sequence number with that value plus one. No SYN flag is set this time because synchronization in both directions is now done. Once the server receives this ACK, the connection moves to the ESTABLISHED state on both ends and application data can flow. A useful optimization is that this third ACK can piggyback real payload, so an HTTP client often sends the ACK and the start of its request together, saving a round trip. The whole exchange costs roughly one and a half round trips before the first byte of data, which is precisely why connection pooling and keep-alive matter so much. Reopening a fresh TCP connection for every request pays this latency tax repeatedly, and on a high-latency link that adds up quickly.
Sequence Numbers And Reliability
After establishment, every byte sent has a sequence number, and the receiver acknowledges the highest contiguous byte it has received. If a segment is lost, the receiver keeps acknowledging the last good byte, producing duplicate ACKs. The sender interprets several duplicate ACKs as a signal to retransmit the missing segment without waiting for a timeout, a mechanism called fast retransmit. Selective acknowledgment, negotiated in the handshake, lets the receiver report exactly which non-contiguous blocks it already has, so the sender retransmits only the true gap rather than everything after it. This is why a single dropped packet on TCP causes a latency hiccup, not data loss: the protocol quietly resends and reorders. For backend developers this explains why a flaky network shows up as slow queries and timeouts rather than garbled responses.
Flow Control With Windows
TCP prevents a fast sender from overwhelming a slow receiver using the receive window. Each acknowledgment advertises how many more bytes the receiver currently has buffer space to accept. The sender must never have more unacknowledged data in flight than this advertised window. On modern high-bandwidth, high-latency networks the original sixteen-bit window field is far too small, so the window scaling option negotiated during the handshake multiplies it by a power of two. If you have ever seen throughput plateau far below the available bandwidth on a long-distance link, an undersized window is a common culprit. Flow control is distinct from congestion control: flow control protects the receiver's buffers, while congestion control protects the network itself. Both run continuously, and together they shape how quickly your data actually moves between two services.
Congestion Control Basics
Beyond protecting the receiver, TCP tries to avoid collapsing the network. It maintains a congestion window separate from the receive window, and the effective sending limit is the smaller of the two. A new connection starts cautiously in slow start, doubling the congestion window each round trip until it detects loss or reaches a threshold, then it grows more gently in congestion avoidance. When loss occurs, the algorithm backs off, interpreting loss as a sign of congestion. This behavior has a direct consequence for short connections: they often finish before slow start ramps up, so they never reach full speed. That is another reason long-lived pooled connections outperform fresh ones for repeated requests. Algorithms like CUBIC and BBR refine this logic, but the core idea is the same: probe for capacity, then retreat when the network pushes back.
Closing And TIME-WAIT
TCP closes gracefully with a four-way exchange because each direction shuts down independently. One side sends a FIN, the other acknowledges it, then later sends its own FIN, which is acknowledged in turn. The side that initiates the close enters the TIME_WAIT state and lingers there for roughly twice the maximum segment lifetime. This wait ensures any delayed duplicate segments from the old connection expire before the same port pair could be reused, preventing stale data from leaking into a new connection. Backend engineers meet TIME_WAIT when a busy service that opens many outbound connections exhausts local ports, filling the connection table with sockets stuck in this state. The fix is usually connection reuse and pooling rather than tuning kernel timeouts, because reusing an established connection sidesteps both the handshake cost and the TIME_WAIT accumulation entirely.