Topic Overview
TCP Connection Termination (FIN/ACK)
Understand how TCP connections are terminated gracefully using FIN and ACK packets in a four-way handshake.
TCP connection termination uses a four-way handshake to ensure both sides of the connection can send all remaining data before closing. Unlike the three-way handshake for connection establishment, termination requires four packets because TCP is full-duplex—each side must independently close its sending direction.
The Four-Way Handshake
TCP connection termination follows this sequence:
Client Server
| |
| 1. FIN (seq=x) |
|------------------------------>|
| |
| 2. ACK (ack=x+1) |
|<------------------------------|
| |
| 3. FIN (seq=y) |
|<------------------------------|
| |
| 4. ACK (ack=y+1) |
|------------------------------>|
| |
Step-by-Step Breakdown
Step 1: Client sends FIN
- Client sets
FINflag and includes sequence numberx - Client enters
FIN_WAIT_1state - Client can still receive data but cannot send more data
Step 2: Server sends ACK
- Server acknowledges the FIN with
ACK(ack=x+1) - Server enters
CLOSE_WAITstate - Server can still send data to client (half-close)
Step 3: Server sends FIN
- Server sends its own
FINwith sequence numbery - Server enters
LAST_ACKstate - Server has finished sending data
Step 4: Client sends ACK
- Client acknowledges server's FIN with
ACK(ack=y+1) - Client enters
TIME_WAITstate - After 2MSL (Maximum Segment Lifetime), client enters
CLOSEDstate
TCP States During Termination
Client States
- FIN_WAIT_1: Client sent FIN, waiting for ACK
- FIN_WAIT_2: Client received ACK, waiting for server's FIN
- TIME_WAIT: Client received FIN and sent ACK, waiting 2MSL
- CLOSED: Connection fully closed
Server States
- CLOSE_WAIT: Server received FIN, can still send data
- LAST_ACK: Server sent FIN, waiting for ACK
- CLOSED: Connection fully closed
TIME_WAIT State
The TIME_WAIT state lasts for 2MSL (Maximum Segment Lifetime, typically 30-120 seconds). This serves two purposes:
- Prevent old duplicate packets: Ensures any delayed packets from the connection are discarded
- Ensure final ACK delivery: If the final ACK is lost, the server will retransmit FIN, and client can respond
Why 2MSL?
- 1MSL for the final ACK to reach server
- 1MSL for any retransmitted FIN to reach client
Examples
Normal Termination Flow
# Server side (Python socket)
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8080))
server_socket.listen(5)
client_socket, addr = server_socket.accept()
# Receive data
data = client_socket.recv(1024)
print(f"Received: {data}")
# Send response
client_socket.send(b"Response data")
# Close connection (sends FIN)
client_socket.close() # Sends FIN, enters LAST_ACK
# After receiving ACK, enters CLOSED
# Client side
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('server.example.com', 8080))
# Send data
client_socket.send(b"Request data")
# Receive response
response = client_socket.recv(1024)
# Close connection (sends FIN)
client_socket.close() # Sends FIN, enters FIN_WAIT_1
# After four-way handshake, enters TIME_WAIT, then CLOSED
Half-Close Connection
TCP allows one side to close its sending direction while keeping receiving open:
import socket
# Server can close sending but keep receiving
client_socket.shutdown(socket.SHUT_WR) # Close write direction (sends FIN)
# Can still receive data
data = client_socket.recv(1024)
client_socket.close() # Fully close
Connection Reset (RST)
If a connection must be terminated immediately (not gracefully):
# Forceful termination with RST
# This happens when:
# 1. Port is closed
# 2. Connection doesn't exist
# 3. Application calls close() on a connection in error state
# RST packet has RST flag set, no ACK required
# Both sides immediately enter CLOSED state
Common Pitfalls
- Not handling TIME_WAIT: Applications that rapidly create/destroy connections may exhaust ports. Fix: Use connection pooling or SO_REUSEADDR socket option
- Ignoring half-close: Not properly handling SHUT_WR can cause applications to hang. Fix: Always check for EOF after shutdown
- RST vs FIN confusion: RST is immediate termination, FIN is graceful. Fix: Use FIN for normal shutdown, RST only for error conditions
- Not waiting for final ACK: Closing socket immediately may lose data. Fix: Use proper shutdown sequence
- Port exhaustion: Too many connections in TIME_WAIT state. Fix: Implement connection reuse, adjust TIME_WAIT timeout, or use SO_REUSEADDR
Interview Questions
Beginner
Q: What is the difference between TCP connection establishment and termination? Why does termination need four packets?
A:
Connection Establishment (3-way handshake):
- SYN → SYN-ACK → ACK
- Establishes bidirectional communication
- Both sides agree on initial sequence numbers
Connection Termination (4-way handshake):
- FIN → ACK → FIN → ACK
- Each side independently closes its sending direction
- TCP is full-duplex: each direction must be closed separately
Why four packets?
- Client closes its sending direction (FIN) → Server ACKs
- Server closes its sending direction (FIN) → Client ACKs
- Since each side can independently close, we need two FINs and two ACKs
Intermediate
Q: Explain the TIME_WAIT state. Why does it exist, and what problems can it cause?
A:
Purpose of TIME_WAIT:
- Prevent old duplicate packets: Ensures any delayed/duplicate packets from the connection are discarded before a new connection uses the same port pair
- Ensure final ACK delivery: If the final ACK is lost, the server will retransmit FIN, and the client in TIME_WAIT can respond
Duration: 2MSL (Maximum Segment Lifetime, typically 60-120 seconds)
Problems:
- Port exhaustion: High-frequency connection creation/destruction can exhaust available ports
- Resource usage: Connections in TIME_WAIT consume kernel resources
- Delayed binding: Cannot immediately reuse the same port pair
Solutions:
# Option 1: SO_REUSEADDR (allows binding to TIME_WAIT port)
socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Option 2: Connection pooling (reuse connections)
# Option 3: Adjust TIME_WAIT timeout (system-level, not recommended)
When TIME_WAIT is on client vs server:
- Client: Usually in TIME_WAIT (initiates close)
- Server: Usually in CLOSED (receives close)
- If server closes first, server enters TIME_WAIT
Senior
Q: Design a high-performance TCP server that handles millions of connections. How do you optimize connection termination and handle TIME_WAIT states?
A:
Design Considerations:
class HighPerformanceTCPServer {
private connectionPool: Map<string, Connection>;
private reuseConnections: boolean = true;
constructor() {
// Enable SO_REUSEADDR to reuse TIME_WAIT ports
this.socket.setOption(SO_REUSEADDR, true);
// Connection pooling to minimize connection churn
this.connectionPool = new Map();
}
// 1. Connection Reuse
async getConnection(clientId: string): Promise<Connection> {
const existing = this.connectionPool.get(clientId);
if (existing && existing.isHealthy()) {
return existing; // Reuse existing connection
}
// Create new connection only if needed
const newConn = await this.createConnection(clientId);
this.connectionPool.set(clientId, newConn);
return newConn;
}
// 2. Graceful Shutdown with Timeout
async shutdownConnection(conn: Connection, timeout: number = 5000) {
// Send FIN gracefully
conn.shutdown(SHUT_WR);
// Wait for peer's FIN with timeout
const finReceived = await Promise.race([
conn.waitForFIN(),
this.timeout(timeout)
]);
if (finReceived) {
conn.sendACK(); // Send final ACK
// Enter TIME_WAIT (handled by OS)
} else {
// Timeout: force close with RST
conn.forceClose();
}
}
// 3. Batch Connection Cleanup
async cleanupConnections() {
const timeWaitThreshold = Date.now() - (2 * MSL);
for (const [id, conn] of this.connectionPool.entries()) {
if (conn.state === 'TIME_WAIT' && conn.closeTime < timeWaitThreshold) {
this.connectionPool.delete(id);
conn.releaseResources();
}
}
}
// 4. Load Balancer Configuration
configureLoadBalancer() {
// Use connection draining: stop accepting new connections
// Wait for existing connections to close gracefully
// Set drain timeout to 2MSL to allow TIME_WAIT to complete
}
}
Optimization Strategies:
- Connection Pooling: Reuse connections instead of creating new ones
- SO_REUSEADDR: Allow binding to ports in TIME_WAIT state
- Connection Draining: Gracefully close connections during shutdown
- Load Balancer Health Checks: Use keep-alive to maintain connections
- Ephemeral Port Range: Increase system's ephemeral port range
- Connection Tracking: Monitor and cleanup TIME_WAIT connections
- HTTP Keep-Alive: For HTTP servers, use persistent connections
Monitoring:
# Check TIME_WAIT connections
ss -tan | grep TIME-WAIT | wc -l
# Check connection states
netstat -an | grep TIME_WAIT
# Monitor port usage
ss -s
Key Takeaways
- Four-way handshake: TCP termination requires four packets (FIN → ACK → FIN → ACK) because TCP is full-duplex
- TIME_WAIT state: Lasts 2MSL to prevent old duplicate packets and ensure final ACK delivery
- Half-close: TCP allows closing send direction while keeping receive open using
shutdown(SHUT_WR) - Graceful vs forceful: FIN is graceful termination, RST is immediate termination
- State management: Client typically enters TIME_WAIT, server enters CLOSED (unless server closes first)
- Port exhaustion: High-frequency connections can exhaust ports due to TIME_WAIT; use connection pooling or SO_REUSEADDR
- Connection reuse: Reuse connections when possible to minimize TIME_WAIT overhead
- Best practice: Always close connections gracefully unless error conditions require immediate termination