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 FIN flag and includes sequence number x
  • Client enters FIN_WAIT_1 state
  • 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_WAIT state
  • Server can still send data to client (half-close)

Step 3: Server sends FIN

  • Server sends its own FIN with sequence number y
  • Server enters LAST_ACK state
  • Server has finished sending data

Step 4: Client sends ACK

  • Client acknowledges server's FIN with ACK (ack=y+1)
  • Client enters TIME_WAIT state
  • After 2MSL (Maximum Segment Lifetime), client enters CLOSED state

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:

  1. Prevent old duplicate packets: Ensures any delayed packets from the connection are discarded
  2. 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:

  1. Prevent old duplicate packets: Ensures any delayed/duplicate packets from the connection are discarded before a new connection uses the same port pair
  2. 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:

  1. Connection Pooling: Reuse connections instead of creating new ones
  2. SO_REUSEADDR: Allow binding to ports in TIME_WAIT state
  3. Connection Draining: Gracefully close connections during shutdown
  4. Load Balancer Health Checks: Use keep-alive to maintain connections
  5. Ephemeral Port Range: Increase system's ephemeral port range
  6. Connection Tracking: Monitor and cleanup TIME_WAIT connections
  7. 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

About the author

InterviewCrafted helps you master system design with patience. We believe in curiosity-led engineering, reflective writing, and designing systems that make future changes feel calm.