Networking · Transport Protocols
Connection State Transitions and the TIME_WAIT Hold
Why closing a TCP connection takes four segments rather than two, what TIME_WAIT protects against, and why the state exists on the side that closed first.
A TCP connection is two independent byte streams that happen to share a pair of addresses. Each direction has its own sequence space, its own window, and its own close. That independence is the reason teardown takes four segments rather than two, and the reason the state machine has a final state that lingers for a full minute or more after the application has finished.
Two FINs, not one
The naive close is a single exchange: one side says “done”, the other says “done”, connection gone. The protocol cannot work that way, because the second side may still have data to send when the first side finishes. A client that has sent its request and is finished writing may still need to read a response that arrives seconds later.
The FIN flag therefore closes one direction, not the connection. A segment with FIN consumes one sequence number and means “I have no more data to send.” The peer acknowledges it, and the connection enters a half-closed state in which the peer may continue sending. Only when the peer also sends a FIN does the connection become fully closed.
Four segments result: FIN, ACK of FIN, FIN from the other side, ACK of that FIN. The middle two are often combined, because a peer that has nothing left to send will send its FIN in the same segment as its acknowledgement, making the exchange three segments. The combination is permitted and common. It is not guaranteed, because the peer’s application may not have decided to close yet when the acknowledgement is generated.
The half-closed state is visible to applications as the difference between a shutdown that closes writing and a close that closes both. A socket shutdown on the write side sends a FIN and leaves the read side open; the application can continue reading until it sees end of file. A full close releases the descriptor, and whether the read side continues to deliver buffered data depends on the socket options in force.
The state machine and where it can stall
The states form a directed graph with eleven nodes. The interesting ones are not the names but the transitions that wait for something the local side cannot control.
FIN_WAIT_1 is entered when the local application closes and the FIN is sent. The state waits for
either an acknowledgement of the FIN (moving to FIN_WAIT_2) or a FIN from the peer (moving to
CLOSING). A connection can sit in FIN_WAIT_2 indefinitely if the peer acknowledges the FIN and
never sends its own. The local side has nothing left to send and cannot force the peer to close, so
the state persists until the peer’s application terminates or the connection is reset.
CLOSE_WAIT is the mirror image on the passive side: the peer’s FIN has arrived, the local
application has been notified of end of file, and the local side waits for its own application to
close. A connection that stays in CLOSE_WAIT is a descriptor leak from the application’s
perspective. The kernel cannot close it, because the application may still have buffered data to read,
and the socket stays open until the process closes it or exits.
LAST_ACK waits for the acknowledgement of the local FIN after the application has closed. It is
normally brief, but it lasts for a full retransmission backoff if that acknowledgement is lost.
TIME_WAIT is the state that outlives the application. It is entered by the side that sent the first
FIN, after that side has received the peer’s FIN and acknowledged it. The socket is closed from the
application’s perspective, but the kernel retains a record of the connection for a fixed interval.
What TIME_WAIT is for
Two distinct problems motivate the state, and both are about segments that outlive their connection.
The first is the lost final acknowledgement. The side in TIME_WAIT has sent an ACK for the peer’s
FIN. If that ACK is lost, the peer retransmits its FIN, because from the peer’s perspective its FIN is
unacknowledged. A side that had fully forgotten the connection would respond to the duplicate FIN with
a reset, and the peer would see an abort rather than a clean close. The TIME_WAIT state allows the
retransmitted FIN to be acknowledged again. This is the entire reason the state lasts long enough for
the peer to exhaust its retransmissions of the FIN.
The second is the reuse of the address pair. A connection is identified by the four-tuple of source address, source port, destination address, and destination port. If a new connection reuses the same four-tuple immediately after the old one closes, segments from the old connection that are still in flight can be delivered to the new one. With a sequence number chosen from a clock, the new connection’s numbers are higher than the old one’s, so an old duplicate would be rejected as outside the window, but the window is only 4 GiB wide, and the clock-based initial sequence number advances by roughly 250,000 per second, which wraps the space in under five hours. A connection that lives long enough to wrap its sequence space has no protection from the numbers alone, which is why the modular comparison in Sequence Numbers and the Sliding Window Arithmetic carries a correctness condition rather than being a plain ordering.
The hold interval is therefore defined as twice the maximum segment lifetime: the longest time a
segment can survive in the network before being discarded. RFC 9293 takes the maximum segment
lifetime to be two minutes and calls that an engineering choice, so TIME_WAIT is four minutes.
Implementations routinely use a shorter configured value, and the value is a policy choice rather
than a constant: shorter reduces port pressure, longer improves the protection. A host that shortens
the interval accepts a longer window in which a stale duplicate could be mistaken for new data, and a
host that lengthens it holds more four-tuples.
Why the closing side and not the other
The asymmetry is a direct consequence of the four-segment exchange. The side that sends the first FIN
is the side whose final ACK could be lost in a way that requires a response. The passive side, after
sending its FIN and receiving the ACK, has no further obligation: if its FIN is lost, it retransmits,
and the active side, still in TIME_WAIT, acknowledges. The passive side must retain the connection
only while its own FIN is unacknowledged, which is the ordinary retransmission state, and then it can
forget. The active side must retain it for a full two segment lifetimes because it is the only party
that can answer a late duplicate.
This is why a server that closes connections can accumulate TIME_WAIT entries while its clients do
not, and why the usual advice is for the side that initiates the close to be the side that has ports
to spare. A server listening on a fixed port accepts connections on ephemeral ports chosen by clients;
those four-tuples are held by the server only if the server closed first. A proxy that closes the
client-facing connection while the client waits accumulates entries at a high rate.
The port exhaustion that follows
Each TIME_WAIT entry occupies a four-tuple. A host making many short-lived outbound connections to
one destination has a bounded supply: the ephemeral port range, which on many systems spans a few tens
of thousands of ports, against one destination address and port. At 1,000 connections per second with
a 60-second hold, the steady-state population is 60,000 entries, which exceeds a range of that size.
The result is that connect attempts fail with an inability to allocate a local port. The failure is not a resource exhaustion in the usual sense: memory is available, file descriptors are available, and the network is idle. The exhausted resource is the namespace of four-tuples.
Four mitigations exist, and each has a cost.
Reusing the four-tuple is permitted when the new connection’s initial sequence number is provably
outside the range that old duplicates could occupy. RFC 9293 allows a host in TIME_WAIT to accept a
new SYN directly, provided the new initial sequence number exceeds the largest used on the previous
incarnation and the host returns to TIME_WAIT if the SYN proves to be an old duplicate. That removes
the wait entirely, at the price of relying on the sequence number being higher than any stale
segment’s. The assumption holds when the initial sequence number comes from a clock that has advanced
since the old connection closed, and it fails when it comes from a random source and lands behind the
old value.
Extending the ephemeral port range increases the supply linearly, at the price of colliding with other services that expect particular ranges.
Connecting to more destination addresses spreads the tuples over more namespaces, which works when the service has multiple addresses.
Reducing the hold interval shortens the population proportionally, at the price of the protection described above.
A fifth approach sidesteps the issue: a host that never initiates the close never enters TIME_WAIT.
A client that lets the server close, and a server that lets the client close, both avoid the state
entirely. This is why some protocols specify which side closes, and why the choice is not arbitrary.
Resets, and the states that skip the exchange
A connection can also end with a reset, which is not a clean close and does not enter TIME_WAIT. A
reset is generated when a segment arrives for a connection that does not exist, when a segment arrives
outside the window in a way that indicates the peer has lost state, or when the application requests
an abortive close.
The reset terminates both directions immediately. Data in flight is discarded, and the peer’s application sees a connection reset error rather than end of file. The distinction matters to applications: a clean close means every byte written was delivered and acknowledged, while a reset means some suffix of the stream may have been lost without notice. A protocol that depends on end-of-stream framing must therefore treat a reset as a truncated message rather than as an empty one.
A reset also skips the TIME_WAIT protection, which is why a host that resets instead of closing can
reuse the four-tuple immediately and is vulnerable to the stale-segment problem the state exists to
prevent. The vulnerability is usually theoretical, because a reset implies the peer has already lost
state, but it is the reason reset-based teardown is not a substitute for a clean close in protocols
that care about duplicate suppression. The mechanisms that detect duplicates independently of the
connection lifetime are covered in Timestamps, RTTM, and Protection Against Wrapped Sequences.
The practical consequence is that the state a connection leaves behind is asymmetric even after both applications believe it is finished. One side holds a control block for up to two segment lifetimes and the other holds nothing, and the side that holds it is the side that closed first. Any system that counts connections, allocates ports, or enforces a per-host connection limit sees that asymmetry as uneven load between peers that are, from the application’s point of view, doing identical work.
References
- RFC 9293: Transmission Control Protocol (TCP) — IETF
- RFC 1122: Requirements for Internet Hosts — Communication Layers — IETF
- TCP/IP Illustrated, Volume 1: The Protocols — W. Richard Stevens; Addison-Wesley