TCP TIME_WAIT State Sockets: Advanced Topics

 

TCP TIME_WAIT State
Chapter 61 · Sockets: Advanced Topics · Part 3 of 5
2×MSL
Duration
60s
On Linux
2
Purposes

What is the TIME_WAIT State?

After the active closer sends the final ACK to end a TCP connection, it does not immediately disappear. Instead it sits in the TIME_WAIT state for a fixed period — twice the Maximum Segment Lifetime (2 × MSL). This feels frustrating to programmers (“why can’t I bind my server port immediately?”), but TIME_WAIT exists for two very good reasons: ensuring reliable termination and preventing stale duplicate packets from corrupting new connections.

On Linux, the MSL is 30 seconds, so TIME_WAIT lasts 60 seconds. On systems following RFC 1122 where MSL is 2 minutes, TIME_WAIT lasts 4 minutes.

Key Terms to Know

TIME_WAIT MSL 2MSL Timer TTL (Time-to-Live) Duplicate Segment Connection Incarnation EADDRINUSE SO_REUSEADDR RST segment Active Close

Purpose 1: Reliable Connection Termination

Recall from Part 1 that the final step of TCP termination is the active closer sending an ACK for the passive closer’s FIN. What if that ACK gets lost?

What Happens If the Final ACK Is Lost?
CLIENT (Active Closer) Network SERVER (Passive Closer)
TIME_WAIT ── ACK ──✗ (lost!) LAST_ACK
Waiting for ACK…
TIME_WAIT
Still alive! Can respond.
<── FIN (retransmit) Timer expires, retransmits FIN
TIME_WAIT ── ACK ──────────────> CLOSED
CLOSED
After 2MSL timer
2MSL expires
What if TIME_WAIT did not exist? If the client immediately moved to CLOSED, it would have no state for this connection. When the server retransmits FIN, the client’s TCP would send an RST (reset) segment. The server would see RST as an error, not a clean close. This breaks the connection termination guarantees of TCP.

Why 2MSL? One MSL covers the time for the final ACK to reach the server. A second MSL covers the time for the retransmitted FIN to come back, if the ACK was lost. Total: 2 × MSL.

Purpose 2: Preventing Stale Duplicate Segments

TCP retransmission means that duplicate copies of segments can exist in the network at any time. These duplicates can arrive late — even after the connection has closed.

The Stale Duplicate Problem
Step 1 — Original Connection
Client 200.0.0.1:50000 ↔ Server 204.152.189.116:21
A segment gets delayed in a router loop (e.g., routing anomaly). The segment is wandering the network.
Step 2 — Connection Closes
Both sides close. Client enters TIME_WAIT using the same 4-tuple (src IP, src port, dst IP, dst port).
Step 3 — New Incarnation Attempted
Client 200.0.0.1:50000 ↔ Server 204.152.189.116:21
A new connection with the exact same 4-tuple is established (new incarnation). The delayed duplicate segment from Step 1 now arrives!
Problem!
The new connection receives the stale segment as if it is valid data for the new connection. This corrupts the data stream silently.
How TIME_WAIT Prevents This
TCP will not allow a new incarnation (same 4-tuple) to be established while the previous connection is still in TIME_WAIT. Since TIME_WAIT lasts 2×MSL, all old duplicate segments will have expired (TTL exhausted) before the new connection can start. No stale data can corrupt the new connection.

What is MSL exactly? The Maximum Segment Lifetime (MSL) is the estimated maximum time a TCP/IP segment can survive in the network before being discarded. IP packets have an 8-bit TTL (Time-To-Live) field that decrements at each router hop. When TTL reaches 0, the packet is dropped. MSL is the time estimate for a packet to exceed 255 hops. In practice, BSD and Linux use 30 seconds for MSL. RFC 1122 suggests 2 minutes.

The Practical Problem: EADDRINUSE Error

The most common complaint about TIME_WAIT among developers: you restart your server and get bind(): Address already in use (EADDRINUSE).

This happens because the previous server process initiated a connection close, entered TIME_WAIT for that port, and the 60-second timer has not expired yet. The new server cannot bind the same port.

/* Demonstrating the EADDRINUSE problem */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <errno.h>

#define PORT 9090

int create_server_socket(int use_reuse_addr) {
    int sock_fd;
    struct sockaddr_in addr;

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket");
        return -1;
    }

    if (use_reuse_addr) {
        /*
         * SO_REUSEADDR: allows binding even if port is in TIME_WAIT.
         * This is safe because TIME_WAIT still protects against stale
         * duplicates — the OS ensures the sequence numbers won't collide.
         * This is the CORRECT FIX for EADDRINUSE on server restart.
         */
        int opt = 1;
        if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR,
                       &opt, sizeof(opt)) < 0) {
            perror("setsockopt SO_REUSEADDR");
            close(sock_fd);
            return -1;
        }
        printf("SO_REUSEADDR enabled.\n");
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        if (errno == EADDRINUSE) {
            printf("bind() FAILED: EADDRINUSE — port %d in TIME_WAIT!\n",
                   PORT);
            printf("Solution: set SO_REUSEADDR before bind()\n");
        } else {
            perror("bind");
        }
        close(sock_fd);
        return -1;
    }

    printf("bind() SUCCESS on port %d\n", PORT);
    return sock_fd;
}

int main() {
    int fd;

    printf("=== Without SO_REUSEADDR ===\n");
    fd = create_server_socket(0); /* May fail with EADDRINUSE */
    if (fd > 0) close(fd);

    printf("\n=== With SO_REUSEADDR ===\n");
    fd = create_server_socket(1); /* Should succeed */
    if (fd > 0) {
        listen(fd, 5);
        printf("Server is listening. Press Ctrl+C to stop.\n");
        /* Normally you would call accept() here */
        close(fd);
    }

    return 0;
}

Best practice: Always set SO_REUSEADDR on TCP server sockets before calling bind(). This is safe — it does not disable TIME_WAIT’s protections. It only allows rebinding a port that is in TIME_WAIT, and the kernel ensures that old sequence numbers from the TIME_WAIT connection cannot be confused with the new connection.

Correct server socket setup pattern:

int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
/* ALWAYS do this before bind() */
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(sock_fd, ...);
listen(sock_fd, SOMAXCONN);

Visualizing TIME_WAIT on Linux

You can see TIME_WAIT connections using netstat or ss:

# Show all TCP connections including TIME_WAIT
netstat -tn | grep TIME_WAIT

# Using ss (newer tool, faster than netstat)
ss -tan state time-wait

# Count how many TIME_WAIT connections exist
netstat -tn | grep TIME_WAIT | wc -l

# Example output you might see:
# tcp  0  0  127.0.0.1:9090  127.0.0.1:54321  TIME_WAIT

On a busy web server, hundreds or thousands of TIME_WAIT connections are normal and expected. Each represents a recently closed HTTP connection where this server was the active closer.

TIME_WAIT Timeline (Linux: MSL = 30s)
Close
TIME_WAIT (60 seconds = 2 × 30s MSL)
CLOSED
t = 0s (active close sends final ACK)t = 30s (1 × MSL)t = 60s (2 × MSL, enters CLOSED)

Why You Should NOT Disable TIME_WAIT

Developers frustrated with EADDRINUSE sometimes try to eliminate TIME_WAIT using socket options or kernel tuning (like tcp_tw_recycle on old Linux kernels). This is dangerous:

Approach Risk
tcp_tw_recycle (removed in Linux 4.12) Caused connection failures for clients behind NAT. Widely known to cause production outages.
Sending RST to assassinate TIME_WAIT Defeats reliable termination. The passive closer may receive an error instead of clean close.
Reusing ports with SO_REUSEPORT improperly Stale duplicate segments from old connections can corrupt new connections on the same port/address pair.
The correct solution is SO_REUSEADDR: It solves the restart problem without disabling TIME_WAIT’s safety guarantees. TIME_WAIT continues to run — you just skip the EADDRINUSE error by allowing the bind while TIME_WAIT is active.

Interview Questions & Answers

Q1. What are the two purposes of the TIME_WAIT state?

First, to implement reliable connection termination: if the final ACK is lost, TIME_WAIT ensures the active closer can resend it when the passive closer retransmits its FIN. Second, to allow stale duplicate segments to expire so they cannot corrupt a new incarnation of the same connection (same 4-tuple of source/dest IP and ports).

Q2. What is MSL and what is its value on Linux?

MSL stands for Maximum Segment Lifetime — the estimated maximum time an IP/TCP segment can live in the network before being discarded due to TTL exhaustion. On Linux (following BSD conventions), MSL is 30 seconds. TIME_WAIT therefore lasts 60 seconds (2 × MSL) on Linux. RFC 1122 recommends MSL of 2 minutes (making TIME_WAIT 4 minutes on those systems).

Q3. Why does TIME_WAIT last 2 × MSL and not just 1 × MSL?

One MSL accounts for the time needed for the final ACK to travel from the active closer to the passive closer. A second MSL accounts for the time a retransmitted FIN from the passive closer (triggered by a lost ACK) would take to travel back to the active closer. Together, 2 × MSL covers both the outbound ACK and a possible returning FIN, ensuring the active closer can handle retransmissions.

Q4. Why does only the active closer enter TIME_WAIT, not the passive closer?

The passive closer is the initiator of the final exchange from its perspective: it sends the last FIN and waits for the ACK. It can retransmit its FIN if no ACK arrives. The active closer sends the final ACK and must remain available to resend that ACK if needed. Since the passive closer has a retransmit mechanism, it does not need a TIME_WAIT equivalent.

Q5. What error does TIME_WAIT cause for developers and what is the correct fix?

When a server is restarted and tries to bind the same port, it may get EADDRINUSE (Address already in use) because a TCP connection from the previous run is still in TIME_WAIT. The correct fix is to call setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) before bind(). This allows binding while TIME_WAIT is active without removing TIME_WAIT’s safety guarantees.

Q6. What is a “new incarnation” of a TCP connection?

A new incarnation is a new TCP connection that uses the exact same 4-tuple (source IP, source port, destination IP, destination port) as a previous connection that has now closed. TCP prevents new incarnations from being established while a TIME_WAIT exists on that 4-tuple. This ensures stale duplicate segments from the old incarnation cannot be mistakenly accepted as valid data in the new one.

Q7. What role does the IP TTL field play in the TIME_WAIT design?

The IP TTL (Time-to-Live) is an 8-bit field (max 255) that decrements at each router hop. When it reaches 0, the packet is discarded. MSL is the worst-case estimate for how long a packet can survive in the network before TTL runs out. By waiting 2 × MSL, TCP guarantees that all duplicates from the old connection have exceeded their TTL and been discarded by routers before a new incarnation is allowed.

Continue Learning

Next: Using netstat to inspect and debug socket states

← Part 2: shutdown() Part 4: netstat →

Leave a Reply

Your email address will not be published. Required fields are marked *