Skip to main content

What is three-way handshake?

Three-way handshake is the TCP connection setup process where client and server exchange three packets (SYN, SYN-ACK, ACK) to confirm both sides are reachable and agree on sequence numbers before any data moves.

Theory

TL;DR

  • Like a phone line check: "hello?" (SYN), "hello, I hear you" (SYN-ACK), "got it, let's talk" (ACK). Both sides confirm the channel works before saying anything real.
  • Happens automatically for every TCP connection. HTTP, HTTPS, SSH, database connections, WebSockets - all start with this.
  • The actual goal: both sides agree on initial sequence numbers so TCP can detect lost or out-of-order packets later.
  • UDP skips the handshake entirely. Faster, but no delivery guarantee.
  • ECONNREFUSED = server rejected your SYN (port closed). ETIMEDOUT = no SYN-ACK came back (server unreachable or filtered by firewall).

Quick example

javascript
const net = require('net'); const socket = net.createConnection({ host: 'example.com', port: 80 }); socket.on('connect', () => { // Handshake is done - SYN -> SYN-ACK -> ACK happened in the OS kernel socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); }); socket.on('error', (err) => { // ECONNREFUSED: server is up, port is closed // ETIMEDOUT: SYN went nowhere - wrong host, firewall, or dead server console.log('Connection failed:', err.code); });

The connect event fires only after all three packets complete. Your application code never touches the handshake itself. The OS kernel handles everything.

How it works step by step

Step 1 (SYN): the client picks a random initial sequence number (ISN) and sends a SYN packet. "I want to connect, and my byte stream starts at this number."

Step 2 (SYN-ACK): the server picks its own ISN and replies. It acknowledges the client's ISN by echoing it plus one, and announces its own starting number. One packet, two jobs.

Step 3 (ACK): the client acknowledges the server's ISN. Both sides move to ESTABLISHED state. Data can flow.

That's why three steps are needed and not two. A two-step exchange would confirm the server got the client's SYN, but the client would never confirm it received the server's sequence number. The third step closes that gap.

// tcpdump output for a real handshake // Client 192.168.1.100:54321 -> Server 192.168.1.1:80 // Step 1: SYN 192.168.1.100.54321 > 192.168.1.1.80: Flags [S], seq 1000000000 // Step 2: SYN-ACK 192.168.1.1.80 > 192.168.1.100.54321: Flags [S.], seq 2000000000, ack 1000000001 // Step 3: ACK 192.168.1.100.54321 > 192.168.1.1.80: Flags [.], ack 2000000001 // Data starts flowing 192.168.1.100.54321 > 192.168.1.1.80: Flags [P.] "GET / HTTP/1.1"

Key difference from UDP

The handshake is not about sending data. It sets up shared context: both sides know where the other's byte stream starts. From that point, TCP can detect missing packets, duplicates, and reorder arrivals. UDP sends packets and forgets them. That trade-off is why video calls use UDP (a dropped frame is acceptable) and file transfers use TCP (every byte must arrive in order).

When to use

  • TCP connections: the handshake is automatic. You don't choose it - it's part of the protocol.
  • UDP (no handshake): DNS queries, video streaming, online games. Latency beats reliability there.
  • Debugging timeouts: if a connection hangs, check whether SYN-ACK ever comes back. A firewall silently dropping SYN packets looks identical to an unreachable server from the client side.
  • Security awareness: SYN flood attacks send millions of SYN packets without completing the ACK step. The server fills its connection table with half-open entries and runs out of resources.

Common mistakes

Mistake 1: writing data before the connection is ready

javascript
// Wrong: socket.write() before the handshake completes const socket = net.createConnection({ host: 'example.com', port: 80 }); socket.write('GET / HTTP/1.1\r\n'); // handshake not done yet // Right: wait for the connect event socket.on('connect', () => { socket.write('GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'); });

Mistake 2: timeout too short for slow or distant networks

javascript
// Wrong: 100ms is not enough for cross-region connections net.createConnection({ host: 'remote-server.com', port: 443, timeout: 100 }); // Right: handshake on a distant server can take 200-500ms net.createConnection({ host: 'remote-server.com', port: 443, timeout: 5000 });

Mistake 3: treating ECONNREFUSED and ETIMEDOUT as the same thing

ECONNREFUSED means the server replied with RST - it's alive, but that port is closed. ETIMEDOUT means your SYN got no reply at all: wrong host, firewall blocking it, or a dead server. Different errors need different fixes. Retrying on ECONNREFUSED is pointless.

Mistake 4: assuming sequence numbers are predictable

Old TCP stacks used time-based or sequential ISNs. Attackers could guess them and forge packets. Modern TCP randomizes ISNs per RFC 6528. Never build logic that depends on predicting a remote side's sequence number.

Mistake 5: running production servers without SYN flood protection

bash
# Linux: enable SYN cookies at the OS level sysctl net.ipv4.tcp_syncookies=1 # In nginx: rate-limit connections per IP # limit_conn_zone $binary_remote_addr zone=addr:10m; # limit_conn addr 100;

SYN cookies let the server answer with SYN-ACK without allocating state for each half-open connection. If the client completes the handshake, state is reconstructed from the cookie. If it never does, nothing was wasted. I've seen this misconfigured on internal services before - a single load test from a staging environment looked like a SYN flood to the production box.

Real-world usage

  • Browser requests: every HTTP GET starts with SYN. For HTTPS, a TLS handshake follows on top of the completed TCP handshake.
  • SSH: that 1-2 second delay on ssh user@host is the TCP handshake plus key exchange, not network lag.
  • Database connections: MySQL, PostgreSQL, and MongoDB all go through a TCP handshake before the first query. Connection pooling exists to avoid repeating this on every request.
  • WebSockets: the HTTP upgrade to WebSocket happens after TCP is already in ESTABLISHED state.
  • Load balancers: nginx and HAProxy watch for handshake failures to mark backends as unavailable.

Follow-up questions

Q: Why three steps instead of two?
A: Two steps would confirm the server received the SYN, but the client would never confirm it got the server's sequence number. Without the third step, the server has no proof its packets reach the client at all.

Q: What happens if the final ACK is lost?
A: The server stays in SYN_RECV and retransmits SYN-ACK with exponential backoff, roughly 60 seconds total. The client is already in ESTABLISHED and sends data. The server sees data from an unexpected state, sends RST, and the client surfaces this as "Connection reset by peer."

Q: How does TIME_WAIT relate to the three-way handshake?
A: It doesn't directly - TIME_WAIT is about closing, not opening. After the four-way FIN exchange, the side that initiated close waits 2 minutes (2 x MSL) to ensure stale packets from that session don't corrupt a new connection reusing the same port. That's why you sometimes can't immediately restart a server on the same port.

Q: Can you send data inside the SYN packet?
A: Yes, with TCP Fast Open (TFO, RFC 7413). The server issues a cookie on the first connection. Future SYN packets carry that cookie plus data, cutting one round-trip from setup. Chrome and Linux kernel 3.7+ support TFO.

Q: [Senior] How do you detect and handle a SYN flood in production?
A: Watch netstat -an | grep SYN_RECV for growing half-open connections. Enable SYN cookies at the OS level. Put nginx or HAProxy in front with per-IP connection rate limiting. For critical services, use DDoS mitigation at the network edge (Cloudflare, AWS Shield). In Node.js, server.maxConnections caps accepted connections, but floods usually hit the kernel queue before your process ever sees them.

Examples

Basic: TCP server in Node.js

javascript
const net = require('net'); const server = net.createServer((socket) => { // This callback fires only when the connection reaches ESTABLISHED state // SYN / SYN-ACK / ACK already happened in the kernel console.log('Client connected:', socket.remoteAddress); socket.write('Connected\n'); socket.on('data', (data) => { socket.write(`Echo: ${data}`); }); socket.on('error', () => socket.destroy()); }); server.listen(3000, () => console.log('Listening on port 3000'));

You never write code to handle individual handshake packets. The kernel does SYN / SYN-ACK / ACK, then hands you a ready socket.

Intermediate: diagnosing connection failures by error type

javascript
const net = require('net'); function connectWithDiagnosis(host, port) { const socket = net.createConnection({ host, port, timeout: 5000 }); socket.on('connect', () => { console.log('Handshake complete - connection ready'); }); socket.on('timeout', () => { socket.destroy(); // No SYN-ACK received - check firewall rules or host reachability console.log('Timeout: SYN-ACK never arrived'); }); socket.on('error', (err) => { if (err.code === 'ECONNREFUSED') { // Server is alive, SYN reached it, but the port is closed console.log('Port closed - check the port number'); } else if (err.code === 'EHOSTUNREACH') { // No route exists - check the IP address or network config console.log('No route to host'); } else { console.log('Unexpected error:', err.message); } }); } connectWithDiagnosis('example.com', 80);

ECONNREFUSED tells you the SYN reached the server. ETIMEDOUT tells you it didn't. These two facts point you in completely different directions when debugging.

Advanced: watching the handshake with tcpdump

bash
# Capture TCP handshake packets in real time tcpdump -i any 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0' -n # What you'll see: # 10:01:01 client.54321 > server.80: Flags [S], seq 1000000000 # 10:01:01 server.80 > client.54321: Flags [S.], seq 2000000000, ack 1000000001 # 10:01:01 client.54321 > server.80: Flags [.], ack 2000000001 # 10:01:01 client.54321 > server.80: Flags [P.] "GET / HTTP/1.1..."

[S] is SYN. [S.] is SYN-ACK. [.] is a pure ACK. [P.] is data (PSH+ACK). Every TCP connection your machine makes produces this exact four-line sequence at the start.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?