0%
Networking for Backend Engineers
The Stack
Names and Trust
Application Protocols
Getting Traffic to a Server
Performance
The TCP Connection Lifecycle
Three packets open a TCP connection, and they are doing far more than saying hello. The handshake establishes initial sequence numbers in both directions, negotiates every option the connection will use for its entire lifetime, and sizes the buffers that determine throughput. Options that are not agreed in the handshake cannot be enabled later, which is why a connection's performance ceiling is set in its first round trip.
The three packets
The client sends a SYN with its initial sequence number, a randomly chosen 32-bit value, plus its options. The server replies with a SYN-ACK carrying its own initial sequence number, an acknowledgement of the client's, and its own options. The client sends an ACK, and the connection is established. Data may ride on the third packet, which is why an HTTP request often appears in the same packet as the final handshake ACK.
The cost is one round trip before any application data moves, and that is the number to carry into every design discussion. On a 1 millisecond local path it is invisible. On a 70 millisecond intercontinental path it is 70 milliseconds spent on nothing, before TLS adds its own round trips, which is the entire motivation for connection reuse, for connection pools, and for QUIC's combined transport and cryptographic handshake.
The options, and why each one matters
Five options appear in nearly every handshake and each one changes behaviour you will later measure.
Maximum segment size advertises the largest payload each side is willing to receive, derived from the interface MTU minus 40 bytes of IPv4 and TCP headers, so 1,460 on a 1,500-byte link. It is advertised, not negotiated: each side states its own limit and the other respects it. A low MSS on one side alone halves effective payload efficiency in that direction.
Window scale multiplies the 16-bit window field by a power of two, raising the maximum receive window from 65,535 bytes to a gigabyte. Without it, throughput on a long path is capped at 65,535 bytes per round trip regardless of available bandwidth, which on a 100 millisecond path is about 5 Mbit/s. This is the single most important option for wide-area throughput, it is negotiated only in the handshake, and a middlebox that strips it, which some older firewalls do, silently caps every connection through it.
Selective acknowledgement lets a receiver report non-contiguous received ranges, so a sender retransmits only the missing segments rather than everything after the first gap. Without it, one lost segment in a window forces retransmission of the whole remainder, which turns a small loss into a large stall.
Timestamps provide round-trip measurement on every segment and protect against sequence number wrap on fast connections. They cost 12 bytes per segment and are what makes round-trip estimation accurate rather than approximate.
Explicit congestion notification lets routers mark packets as congested instead of dropping them, which is the difference between a sender slowing down because it was warned and slowing down because it lost data.
That one line tells you the connection supports SACK, uses timestamps, and will scale its window by 2^7, which means a maximum receive window of about 8 MB. A handshake missing wscale is a performance problem waiting to appear on a long path.
Initial sequence numbers are random for a reason
Each side picks a random 32-bit initial sequence number rather than starting at zero, and the randomness is a security property. An attacker who can guess the sequence numbers of an existing connection can inject data into it, or forge a reset that tears it down, without ever seeing a packet from it. That attack was practical in the 1990s because implementations derived initial sequence numbers from a simple clock, and it remains the reason the modern rule is a cryptographic function of the four-tuple and a secret, which gives unpredictable values while still increasing monotonically for the same four-tuple so that old duplicate segments can be recognised.
Two consequences show up in practice. Blind injection is no longer a realistic threat against TCP itself, which is why off-path attacks target DNS, BGP and application layers instead. And an on-path attacker who can observe sequence numbers can still inject or reset at will, which is the plain reason transport-level confidentiality and integrity, meaning TLS, is not optional for anything that matters. TCP's checksum detects accidental corruption and nothing else; it was never an integrity mechanism against an adversary.
Fast Open, and why you probably do not have it
TCP Fast Open allows data on the SYN itself, removing the handshake round trip for repeat connections to a server that has issued the client a cookie. The saving is real, one full round trip on every new connection, and the deployment is poor: middleboxes that do not understand data on a SYN drop it, which produces a connection that fails rather than a connection that is slower, so clients must fall back and the fallback costs more than the saving. Linux supports it on both sides, net.ipv4.tcp_fastopen controls it, and almost nothing on the public internet relies on it.
The reason to know about it is that it is the problem QUIC solves properly. By running over UDP and combining the transport and cryptographic handshakes, QUIC gets a zero round trip resumption path that middleboxes cannot break in the same way, because there is no SYN for them to have opinions about. The lesson generalises: changes to TCP's handshake are nearly impossible to deploy, which is why the industry moved the transport rather than fixing it.
Backlogs, and the two queues people confuse
A listening socket has two queues, and conflating them produces the wrong fix in a capacity incident.
The SYN queue, sized by net.ipv4.tcp_max_syn_backlog, holds half-open connections between the arriving SYN and the final ACK. Its occupancy is one round trip per entry, so it fills under load or under a SYN flood. When full, the kernel either drops new SYNs, which clients experience as a connection timeout, or sends SYN cookies if net.ipv4.tcp_syncookies is enabled, encoding the connection state into the sequence number so no queue entry is needed. Cookies cost the loss of negotiated options for affected connections, which is a real but acceptable degradation compared with refusing service.
The accept queue, sized by the smaller of the listen() backlog argument and net.core.somaxconn, holds fully established connections waiting for the application to call accept(). It fills when the application is too slow, not when the network is slow, and when it overflows the kernel drops the ACK, leaving the client believing the connection is open while the server has no record of it. That mismatch is one of the few ways to get a connection that appears established on one side only.
The distinction tells you where to look. ss -ltn prints Recv-Q as the current accept queue depth and Send-Q as its limit for listening sockets, so a Recv-Q near Send-Q means the application is not accepting fast enough and no amount of network tuning will help. nstat -az TcpExtListenOverflows TcpExtListenDrops counts the overflows, and a rising ListenOverflows is proof rather than inference.
A listen backlog of 128, which is the default in many frameworks, is not a performance setting, it is a burst tolerance. If 500 connections arrive in the time the application takes to accept 128, the remainder are dropped and the clients see a timeout rather than a refusal, because a dropped ACK produces no error. Raise both the listen() argument and net.core.somaxconn, and monitor TcpExtListenOverflows, since this failure is silent on the server and looks like a network fault from outside.
What a refused connection actually is
Three outcomes follow a SYN, and distinguishing them resolves most connectivity questions immediately. A SYN-ACK means something is listening. A RST means the packet reached a host that has nothing listening on that port, so the path works and the service does not. And nothing at all, repeated until the client gives up, means a firewall is dropping silently, the host is absent, or the path is broken.
The practical value is that connection refused and connection timed out are different diagnoses with no overlap. Refused means reachability is proven and you should look at the process; timed out means reachability is unproven and you should look at the path, the security group, and whether the host exists. Treating them as the same error, which many client libraries encourage by reporting both as a connection error, costs more debugging time than almost any other conflation in this course.