TCP Connection Termination (4-Way)

intermediate tcp fin close time-wait transport

Closing a TCP connection takes four messages, not three. Each side has to independently say “I’m done sending” and get an acknowledgement. Then there’s a weird state called TIME_WAIT that sysadmins love to complain about.

In simple language: TCP is full-duplex — both sides can send. So both sides have to close their end separately. That’s why we get four messages instead of three.

The Four Messages

  1. FIN (A → B) — A says “I have no more data to send.”
  2. ACK (B → A) — B acknowledges the FIN. (B can still send data to A.)
  3. FIN (B → A) — B says “I’m also done sending.”
  4. ACK (A → B) — A acknowledges. Connection fully closed.

The Termination Timeline

CLIENT (A)
SERVER (B)
FIN seq=1000
────────────▶
(CLOSE_WAIT)
(FIN_WAIT_2)
◀────────────
ACK ack=1001
(still FIN_WAIT_2)
◀────────────
FIN seq=2000
ACK ack=2001
────────────▶
(CLOSED)
A enters TIME_WAIT (~2× MSL)

State Walkthrough

The side that calls close() first is the active closer. The other side is the passive closer.

Active closer:   ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
Passive closer:  ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED

The active closer is stuck in TIME_WAIT for a while. The passive closer goes straight to CLOSED after the final ACK.

What is TIME_WAIT?

After sending the final ACK, the active closer waits for 2 × MSL (Maximum Segment Lifetime). MSL is typically 60s by default, so TIME_WAIT lasts ~30–120s depending on the OS.

Why Wait?

Two reasons:

  1. The final ACK might be lost. If the other side never sees it, they’ll resend their FIN. We need to be around to ACK it again.
  2. Old duplicate packets must die. A delayed segment from the just-closed connection shouldn’t show up in a brand-new one with the same 5-tuple.

Why TIME_WAIT Causes Headaches

A busy server (think a load balancer making lots of short-lived outbound connections) can pile up tens of thousands of sockets in TIME_WAIT. Each consumes a 5-tuple. We can run out of ephemeral ports.

# Count TIME_WAIT sockets on Linux
ss -tan state time-wait | wc -l

# Useful kernel knobs (Linux)
sysctl net.ipv4.tcp_fin_timeout      # how long FIN_WAIT_2 lingers
sysctl net.ipv4.tcp_tw_reuse         # 1 = reuse TIME_WAIT sockets for outgoing connections

Tuning tcp_tw_reuse=1 is a common fix for proxy servers. Don’t enable it blindly though — read the docs.

Simultaneous Close (Rare)

If both sides send FIN at the same time, we end up in a state called CLOSING and the dance becomes:

A: FIN_WAIT_1 -> CLOSING -> TIME_WAIT
B: FIN_WAIT_1 -> CLOSING -> TIME_WAIT

Cool but rarely seen in practice.

Half-Close

TCP supports a half-close — A says FIN, but keeps reading. This is what shutdown(sock, SHUT_WR) does. Useful for protocols like HTTP/1.0 where the client sends a request, half-closes write, and reads the response.

Reset (RST) — The Hard Hangup

If something goes wrong (connection refused, port not listening, app crashes), the OS sends a RST instead of going through the polite 4-way termination. Both sides immediately tear down. No TIME_WAIT.

Common Gotcha

A common interview answer is “TCP closes with 3 messages, like the open.” Wrong — it’s 4 because each direction is closed independently. The trick is that two of those four can sometimes piggyback into a single segment if both sides are ready, but logically there are still four.

Interview Tip

If asked “why does TIME_WAIT exist?” — both reasons matter (lost ACK + ghost packets). Don’t just say “to be safe.” Mentioning that the active closer pays the cost is bonus.