A TCP connection carries two independent byte streams: one from endpoint A to endpoint B and another from B to A. Closing the connection therefore requires more than a single "disconnect" message.
Each endpoint must separately announce:
I have no more bytes to send in my direction.
TCP normally communicates that decision with the FIN flag. The peer acknowledges the FIN, but it does not have to close its own sending direction immediately. This independence is why graceful termination commonly uses four segments rather than mirroring the three-way connection handshake.
TCP can also terminate abruptly with RST. FIN means the stream ended normally after the preceding bytes. RST means the connection was aborted.
Understanding the difference explains several production states and symptoms: CLOSE-WAIT leaks, large numbers of TIME-WAIT sockets, connections stuck in FIN-WAIT-2, repeated FIN packets, and "connection reset by peer" errors.
An established TCP connection is full duplex:
The two arrows use independent sequence-number spaces. A FIN closes only the arrow sent by the endpoint that transmitted it.
Suppose endpoint A sends FIN first:
Endpoint A can no longer add ordinary bytes after its FIN. It can still receive bytes from B and send acknowledgments for them. Endpoint B can continue sending until it also closes its direction.
This intermediate condition is a half-closed connection. One direction has reached an orderly end, while the other remains usable.
The endpoint that sends the first FIN is called the active closer. The endpoint that receives that FIN first is the passive closer. These roles are unrelated to who originally opened the connection. A server can be the active closer, and a client can be the passive closer.
Assume peer A initiates a graceful close after sending all of its application data.
The messages have distinct responsibilities:
The ACK and FIN from B are separate because receiving a FIN does not force B's application to stop sending. B's kernel acknowledges A promptly, informs B's application that A's stream ended, and waits for the application to close its own direction.
If B is already ready to close, it can combine its ACK of A's FIN with its own FIN. The exchange then uses three segments:
The "four-way close" is therefore the common logical model, not a rule that every graceful termination must contain exactly four packets.
Loading simulation...
When peer A's application closes its sending direction, TCP first handles any bytes already queued for transmission. FIN is ordered after those bytes in the stream.
Assume A's next sequence number is:
A sends:
Like SYN, FIN consumes one sequence position even when it carries no application payload. B will acknowledge 5002.
If FIN accompanies the last 100 payload bytes beginning at sequence number 5001, those bytes occupy positions 5001 through 5100, and FIN consumes the next position:
After sending FIN, A enters FIN-WAIT-1. It is waiting for an acknowledgment of its FIN, a FIN from B, or both.
No later application bytes may appear after A's FIN in that stream direction. A can still send pure acknowledgments because ACK alone does not consume sequence space.
When B receives A's FIN in sequence, it acknowledges the next sequence position:
B enters CLOSE-WAIT. Its TCP implementation has received an orderly end to A's stream and reports that condition to B's application.
At the socket API, the application normally observes end-of-file after reading all bytes that preceded the FIN. Depending on the language, this can appear as a zero-length read, an empty byte sequence, or a specific EOF result.
EOF is not an error. It means:
B's sending direction remains open. It can finish a response, flush queued output, or perform any protocol-defined final exchange.
Once A receives the ACK for its FIN, A enters FIN-WAIT-2. Its sending direction is closed and confirmed, but it continues waiting for B to finish.
B sends its own FIN only after B's application closes or shuts down its sending direction.
Suppose B's next sequence number was 9001 when it received A's FIN. B then sends 200 final bytes:
B's FIN uses sequence number 9201. After sending it, B enters LAST-ACK and waits for A to acknowledge 9202.
This example shows why A must continue reading after sending its own FIN. The peer is allowed to send valid application data during the half-closed period.
The complete sequence-number exchange is:
The two FINs use different sequence spaces. A's FIN advances A's sequence number, while B's FIN advances B's sequence number.
A acknowledges B's FIN and enters TIME-WAIT. B receives that ACK, deletes its connection state, and enters CLOSED.
Why does A remain after sending the final ACK?
The final ACK is not itself acknowledged. If it is lost, B remains in LAST-ACK and sends its FIN again. A must retain enough state to recognize the repeated FIN and send the final ACK again.
Without TIME-WAIT, A might discard the connection immediately. A repeated FIN could then be mistaken for an unrelated packet, and B would not learn that its close completed.
The normal active and passive paths use different TCP states.
Each state answers a specific question:
FIN-WAIT-1: Has the peer acknowledged the FIN, and has the peer sent its own FIN?
FIN-WAIT-2: The local FIN is acknowledged. When will the peer finish its direction?
CLOSE-WAIT: The peer has finished. When will the local application close?
LAST-ACK: The local FIN has been sent. When will its acknowledgment arrive?
TIME-WAIT: Has enough time passed to handle a repeated peer FIN and isolate this connection from delayed packets?
CLOSING: Both endpoints sent FIN before either FIN exchange completed. When will the local FIN be acknowledged?
These are local states. A normal close intentionally places the endpoints in different states at the same moment.
Loading simulation...
TIME-WAITTIME-WAIT serves two correctness purposes.
First, it allows the endpoint that sent the final ACK to answer a retransmitted FIN. This makes graceful termination reliable even though the final ACK has no acknowledgment of its own.
Second, it prevents delayed segments from an old connection from being confused with a new connection that reuses the same IP addresses and ports. Old packets need time to disappear before the same connection identity can be reused without ambiguity.
The waiting interval is 2 × MSL, where MSL means Maximum Segment Lifetime. The standards model defines MSL as two minutes, which yields a four-minute interval. Deployed operating systems often use shorter effective values, so the observed wall-clock duration is platform-specific and should not be assumed from another system.
TIME-WAIT is normally held by the endpoint that actively closed and sent the final ACK. If a backend server closes connections first, the server can accumulate TIME-WAIT entries just as a client can.
A TIME-WAIT entry is not an open application socket. The application descriptor can already be gone while the kernel retains minimal protocol state. It also does not prevent the server from accepting all new clients on its listening port. The main restriction concerns safely reusing the same connection identity.
Large TIME-WAIT counts can be normal on a host that creates and closes many short connections. They become operationally important when connection churn contributes to ephemeral-port exhaustion, memory pressure, or inability to recreate the required endpoint tuple.
The first remedy is usually architectural: reuse connections, configure sensible pools, and avoid unnecessary connect-close cycles. Aggressively shortening or bypassing TIME-WAIT changes a TCP safety mechanism and should not be treated as a generic cleanup optimization.
CLOSE-WAITCLOSE-WAIT means the kernel received the peer's FIN, acknowledged it, and informed the local application that the incoming stream ended. The kernel is now waiting for the local application to close its sending direction.
A brief stay in CLOSE-WAIT is normal. The application may need to finish a response or release request-specific resources.
A large or steadily growing number of CLOSE-WAIT sockets usually points to an application lifecycle problem:
The kernel cannot invent the application's protocol decision and send FIN merely because the peer stopped sending. As long as the local process retains the socket and does not close its direction, the connection can remain in CLOSE-WAIT.
This makes CLOSE-WAIT different from TIME-WAIT:
Reducing a TCP timer does not fix a CLOSE-WAIT leak. The application must close every connection on success, error, cancellation, and timeout paths.
FIN-WAIT-2, LAST-ACK, and CLOSINGThe less visible termination states also provide useful diagnostic evidence.
FIN-WAIT-2The local endpoint sent FIN, and the peer acknowledged it, but the peer has not sent its own FIN. This can be a legitimate long-lived half-close if the protocol allows the peer to keep sending.
If connections remain here unexpectedly, the peer application may not be closing, or its FIN may not be reaching the local endpoint. Operating systems often apply cleanup policies to orphaned FIN-WAIT-2 connections, but exact behavior depends on socket ownership and platform configuration.
LAST-ACKThe endpoint received the peer's FIN, later sent its own FIN, and is waiting for the final ACK. Repeated FIN transmissions from this state indicate that the acknowledgment is not arriving or is not acceptable.
CLOSINGCLOSING appears during a simultaneous close. Both applications send FIN before either endpoint receives acknowledgment of its own FIN.
Each endpoint moves from FIN-WAIT-1 to CLOSING after receiving the peer's FIN without yet having its own FIN acknowledged. Once its FIN is acknowledged, it enters TIME-WAIT.
Simultaneous close is valid but uncommon in ordinary request-response systems.
Applications sometimes need to close only their sending direction.
On POSIX-style socket APIs, an operation such as:
normally tells the kernel that the application will send no more bytes. After queued bytes are handled, TCP sends FIN. The descriptor remains usable for receiving the peer's remaining data.
A complete close() releases the application's descriptor. The kernel can continue protocol processing after the call returns, so a successful local close() does not mean the peer has already acknowledged the FIN.
Exact behavior depends on the socket API, operating system, and configured options. If an application closes while received data remains unread, some TCP implementations send RST to make the discarded data visible as an abort. An application that needs an orderly exchange should consume the protocol-required input and use directional shutdown where appropriate.
An application protocol can use half-close as a message boundary:
This works when the protocol defines EOF as the end of the request or response. Persistent protocols usually frame multiple messages within one connection instead, because FIN ends that stream direction permanently.
Descriptor ownership matters. If a process duplicates a socket descriptor or inherits it into another process, closing one descriptor may not close the underlying socket. FIN is delayed until the final reference that owns the sending direction is closed. This is a common source of connections that outlive the code path that appeared to close them.
TCP orders FIN after the bytes that precede it. If the graceful exchange succeeds, the receiving TCP endpoint gets those bytes before observing EOF.
That is a transport-level guarantee. It does not prove that the remote application:
For example, a client can send an order, close gracefully, and receive a valid final ACK even if the server process later rejects the order. TCP acknowledgments confirm receipt by the remote TCP implementation, not completion of business logic.
An application that needs proof of processing must define an application-level response and decide what retries mean when that response is missing.
TCP uses RST, or reset, to abort a connection instead of closing it gracefully.
| Property | FIN-based close | RST-based abort |
|---|---|---|
| Meaning | No more bytes after this ordered stream position | The connection is invalid or being abandoned |
| Queued data | TCP attempts orderly delivery before FIN | Pending data can be discarded |
| Peer observation | EOF after preceding bytes | Connection-reset error |
| State exchange | FINs and acknowledgments close both directions | The FIN exchange is skipped, and the peer aborts its connection state |
| Typical intent | Graceful completion | Error, invalid state, or explicit abort |
An application can request abortive behavior, and TCP can generate RST when a segment does not match valid connection state. A firewall, proxy, or load balancer can also reset a connection on behalf of an endpoint.
Some socket APIs can be configured for an abortive close, commonly through platform-specific linger behavior. This should be used deliberately: it can discard queued bytes and changes what the peer observes from EOF to an error.
A RST does not explain its cause by itself. "Connection reset by peer" can mean that the peer application aborted, a process closed a socket in a way that discarded data, a middlebox enforced policy, or one side no longer had matching connection state. Logs and captures from multiple components are needed to attribute it correctly.
If a host loses power or a network path disappears, no FIN or RST may be sent. The surviving endpoint can remain in ESTABLISHED because TCP has received no packet that changes its state.
This is a half-open connection in the failure sense: one endpoint believes the connection exists while the other has lost its state or become unreachable.
Half-open and half-closed mean different things:
If the failed host restarts and receives a segment for a connection it no longer knows, it can answer with RST. If it remains unreachable, the surviving endpoint learns about the failure only when communication attempts and relevant timers produce an error.
Applications that need bounded failure detection must use appropriate operation deadlines or liveness policies. A TCP connection can remain quiet by design, so silence alone is not proof that the peer is healthy or dead.
Capture the complete flow so that FINs, ACKs, final data, and resets remain in context:
Replace en0 with the relevant interface. -S displays absolute sequence numbers.
A close with final server data may resemble:
Relevant tcpdump flags include:
Wireshark can locate termination signals with:
After finding a FIN or RST, inspect the entire TCP stream. A reset viewed without the preceding data and timing often leads to the wrong conclusion.
For every graceful close, ask:
On Linux, ss can show the termination states:
These snapshots become more useful when observed over time. Ten CLOSE-WAIT sockets that disappear immediately can be normal; a count that only grows suggests leaked lifecycle handling.
Associate sockets with owning processes when investigating application-controlled states. A packet capture shows what happened on the wire, while socket inspection shows what the local kernel currently believes.
Counters should also be interpreted relative to traffic volume. Thousands of short-lived connections can create many normal TIME-WAIT entries. A much smaller set of old CLOSE-WAIT connections can be the more serious application defect.
Many persistent CLOSE-WAIT sockets: The peers already sent FIN. Find why the local application does not close after EOF, including error paths, blocked workers, and duplicate descriptors.
Many TIME-WAIT entries: Determine which endpoint closes first and whether connection churn is expected. Prefer connection reuse before changing TCP safety timers.
Connections stuck in FIN-WAIT-2: The local FIN was acknowledged, but the peer has not closed its direction. Check whether the protocol permits a long half-close and whether the peer application is waiting indefinitely.
Repeated FIN from LAST-ACK: The sender is not receiving the final ACK. Investigate loss, asymmetric routing, firewall state, and captures nearer the other endpoint.
RST immediately after application data: The receiving endpoint or an intermediary aborted. Check application logs, proxy policies, unread-data behavior, and whether the segment matched a connection still known at the destination.
Connections remain ESTABLISHED after a peer failure: A silent crash sends no termination signal. Verify application deadlines and the intended liveness policy.
The application closed, but no FIN appears: Check whether data is still queued, another descriptor references the socket, an abort produced RST instead, or the capture point missed the packet.
No single state or flag gives the complete cause. Combine endpoint state, packet direction, sequence arithmetic, application logs, and ownership information.
TCP close is directional. A FIN means one endpoint has no more bytes to send; it does not immediately disable the peer's sending direction.
Graceful close is commonly four segments, not always four packets. ACK and FIN can be combined, while loss can produce additional transmissions.
FIN consumes one sequence number. Its acknowledgment advances by one beyond the FIN, after accounting for any payload in the same segment.
EOF and reset are different outcomes. EOF follows an orderly FIN; RST reports an aborted connection.
The active closer is not necessarily the client. Whichever endpoint sends the first FIN normally follows the active-close state path.
TIME-WAIT is not automatically a leak. It is a kernel-managed safety state with a defined protocol purpose.
CLOSE-WAIT is not fixed by shortening TIME-WAIT. It means the local application has not completed its close.
Closing a descriptor does not prove the peer processed the data. TCP acknowledgment and application success are different evidence.
A machine failure may send nothing. Without FIN or RST, the surviving endpoint may continue to report ESTABLISHED.
Half-closed and half-open are different. Half-close is synchronized directional shutdown; half-open is inconsistent connection state after failure.
TCP closes each direction of its full-duplex stream independently. FIN follows queued data, consumes one sequence number, and ends one sending direction; a half-closed connection can still carry data the other way. A normal close uses FIN, ACK, FIN, ACK, though combined flags or loss can change the packet count.
The active closer passes through FIN-WAIT-1, FIN-WAIT-2, and TIME-WAIT; the passive closer uses CLOSE-WAIT and LAST-ACK. TIME-WAIT supports recovery from a lost final ACK and prevents delayed old segments from entering a new connection. Persistent CLOSE-WAIT usually means the local application failed to close after EOF, while FIN-WAIT-2, LAST-ACK, and CLOSING reveal other incomplete or simultaneous paths.
RST aborts rather than gracefully closes the stream. A silent crash may leave the peer believing the connection is still ESTABLISHED, and graceful TCP closure confirms delivery only to the peer's TCP stack, not successful application processing.
Each FIN closes one stream direction, each ACK confirms it, and the final acknowledger retains state temporarily to keep termination reliable.
5 quizzes