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
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?
| 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 |
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.
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.
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.
Both sides close. Client enters TIME_WAIT using the same 4-tuple (src IP, src port, dst IP, dst port).
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!
The new connection receives the stale segment as if it is valid data for the new connection. This corrupts the data stream silently.
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 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.
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);
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.
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. |
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.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.
Next: Using netstat to inspect and debug socket states
