Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TCP States and Lifecycle

A TCP connection progresses through a series of states from creation to termination. Understanding these states helps you debug connection issues, interpret netstat output, and understand why connections sometimes linger.

The State Diagram

                            ┌───────────────────────────────────────┐
                            │                CLOSED                  │
                            └───────────────────┬───────────────────┘
                                                │
              ┌─────────────────────────────────┼─────────────────────────────────┐
              │                                 │                                 │
              │ Passive Open                    │ Active Open                     │
              │ (Server: listen())              │ (Client: connect())             │
              ▼                                 ▼                                 │
      ┌───────────────┐                ┌───────────────┐                          │
      │    LISTEN     │                │   SYN_SENT    │                          │
      │               │                │               │                          │
      │ Waiting for   │                │ SYN sent,     │                          │
      │ connection    │                │ waiting for   │                          │
      │ request       │                │ SYN-ACK       │                          │
      └───────┬───────┘                └───────┬───────┘                          │
              │                                 │                                 │
              │ Receive SYN                     │ Receive SYN-ACK                 │
              │ Send SYN-ACK                    │ Send ACK                        │
              ▼                                 ▼                                 │
      ┌───────────────┐                ┌───────────────┐                          │
      │   SYN_RCVD    │                │  ESTABLISHED  │◄─────────────────────────┘
      │               │                │               │
      │ SYN received, │                │  Connection   │
      │ SYN-ACK sent  │──────────────>│  is open      │
      │ waiting ACK   │ Receive ACK   │               │
      └───────────────┘                └───────────────┘
                                                │
                                    ┌───────────┴───────────┐
                                    │                       │
                                    │ Close requested       │
                                    │                       │
                                    ▼                       ▼
                            (Active Close)          (Passive Close)
                            ┌───────────────┐      ┌───────────────┐
                            │   FIN_WAIT_1  │      │  CLOSE_WAIT   │
                            │               │      │               │
                            │ FIN sent,     │      │ FIN received, │
                            │ waiting ACK   │      │ ACK sent,     │
                            └───────┬───────┘      │ waiting for   │
                                    │              │ app to close  │
                      Receive ACK   │              └───────┬───────┘
                            ┌───────┴───────┐              │
                            │               │              │ App calls close()
                            ▼               │              │ Send FIN
                    ┌───────────────┐       │              ▼
                    │   FIN_WAIT_2  │       │      ┌───────────────┐
                    │               │       │      │   LAST_ACK    │
                    │ Waiting for   │       │      │               │
                    │ peer's FIN    │       │      │ FIN sent,     │
                    └───────┬───────┘       │      │ waiting ACK   │
                            │               │      └───────┬───────┘
                Receive FIN │               │              │
                Send ACK    │               │ Receive FIN  │ Receive ACK
                            │               │ Send ACK     │
                            ▼               ▼              ▼
                    ┌───────────────────────────┐  ┌───────────────┐
                    │        TIME_WAIT          │  │    CLOSED     │
                    │                           │  │               │
                    │  Wait 2×MSL before        │  │  Connection   │
                    │  fully closing            │  │  terminated   │
                    │  (typically 60-120 sec)   │  │               │
                    └─────────────┬─────────────┘  └───────────────┘
                                  │
                                  │ Timeout (2×MSL)
                                  ▼
                          ┌───────────────┐
                          │    CLOSED     │
                          └───────────────┘

State Descriptions

CLOSED

The starting and ending state. No connection exists.
Not actually tracked—absence of connection state.

LISTEN

Server is waiting for incoming connections.
Created by: listen() system call

$ netstat -an | grep LISTEN
tcp   0   0  0.0.0.0:80      0.0.0.0:*    LISTEN
tcp   0   0  0.0.0.0:22      0.0.0.0:*    LISTEN

SYN_SENT

Client has sent SYN, waiting for SYN-ACK.
Created by: connect() system call

Typical duration: 1 RTT (plus retries if lost)

If you see many SYN_SENT:
  - Remote server might be down
  - Firewall blocking SYN-ACKs
  - Network connectivity issues

SYN_RCVD (SYN_RECEIVED)

Server received SYN, sent SYN-ACK, waiting for ACK.
Part of the half-open connection.

Typical duration: 1 RTT

If you see many SYN_RCVD:
  - Could be SYN flood attack
  - Check SYN backlog settings
  - Consider SYN cookies

ESTABLISHED

Three-way handshake complete. Data can flow.
This is the normal "connection open" state.

$ netstat -an | grep ESTABLISHED
tcp  0   0  192.168.1.100:52431  93.184.216.34:80  ESTABLISHED

FIN_WAIT_1

Application called close(), FIN sent.
Waiting for ACK of FIN (or FIN from peer).

Brief transitional state.

FIN_WAIT_2

FIN acknowledged, waiting for peer's FIN.
Peer's application hasn't closed yet.

Can persist if peer doesn't close:
  - Application bug (not calling close())
  - Half-close intentional

Linux: tcp_fin_timeout controls how long to wait

CLOSE_WAIT

Received FIN from peer, sent ACK.
Waiting for local application to close.

If you see many CLOSE_WAIT:
  - Application not calling close()!
  - Resource leak / application bug
  - Common source of "too many open files"

LAST_ACK

Sent FIN after receiving peer's FIN.
Waiting for final ACK.

Brief transitional state.

TIME_WAIT

Connection fully closed, waiting before reuse.
The "lingering" state that often confuses people.

Duration: 2 × MSL (Maximum Segment Lifetime)
  MSL typically 30-60 seconds
  TIME_WAIT typically 60-120 seconds

Why it exists: (see below)

CLOSING

Rare state: Both sides sent FIN simultaneously.
Each waiting for ACK of their FIN.

Simultaneous close scenario.

Why TIME_WAIT Exists

TIME_WAIT serves two important purposes:

1. Reliable Connection Termination

Scenario: Final ACK is lost

Client                               Server
   │                                    │
   │──── FIN ──────────────────────────>│
   │<─── ACK, FIN ──────────────────────│
   │──── ACK ───────────X               │  ← Lost!
   │                                    │
   │    (Client in TIME_WAIT)           │  (Server in LAST_ACK)
   │                                    │
   │<─── FIN (retransmit) ──────────────│
   │──── ACK ──────────────────────────>│  (Re-ACK)
   │                                    │
   │    TIME_WAIT ensures we can        │
   │    re-acknowledge if needed        │

2. Prevent Stale Segments

Old connection: 192.168.1.100:52431 → 93.184.216.34:80
  Some segments still in network (delayed)

New connection with same 4-tuple:
  If TIME_WAIT didn't exist, could reuse immediately
  Old segments might be accepted as valid!

TIME_WAIT (2×MSL) ensures old segments expire:
  MSL = Maximum Segment Lifetime in network
  2×MSL = round-trip time for any lingering data

TIME_WAIT Problems and Solutions

The Problem

High-traffic servers can accumulate thousands of TIME_WAIT connections:

$ netstat -an | grep TIME_WAIT | wc -l
15234

Each TIME_WAIT:
  - Consumes memory (small, but adds up)
  - Holds ephemeral port (can exhaust ports!)
  - 4-tuple unavailable for new connections

Solutions

1. SO_REUSEADDR

# Allow bind() to reuse address in TIME_WAIT
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Server can restart immediately after crash
# Doesn't allow simultaneous bind to same port

2. tcp_tw_reuse (Linux)

# Allow reusing TIME_WAIT sockets for outgoing connections
$ sysctl -w net.ipv4.tcp_tw_reuse=1

# Safe because timestamps prevent confusion
# Only for outgoing connections (client side)

3. Reduce TIME_WAIT duration (careful!)

# Not recommended - violates TCP specification
# Some systems allow it anyway

# Linux doesn't have direct control
# tcp_fin_timeout only affects FIN_WAIT_2

4. Connection pooling

Reuse established connections
  - HTTP Keep-Alive
  - Database connection pools
  - gRPC persistent connections

Fewer connections = fewer TIME_WAITs

5. Use server-side close

If server closes first → Server gets TIME_WAIT
If client closes first → Client gets TIME_WAIT

For servers with many short-lived connections:
  Let clients close first (HTTP/1.1 does this)

Viewing Connection States

Linux/macOS

# All connections with state
$ netstat -an
$ ss -an

# Count by state
$ ss -s
TCP:   2156 (estab 234, closed 1856, orphaned 12, timewait 1844)

# Filter by state
$ ss -t state established
$ ss -t state time-wait

# Show process info
$ ss -tp
$ netstat -tp

State Distribution Check

# Quick state summary
$ ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn
   1844 TIME-WAIT
    234 ESTAB
     56 FIN-WAIT-2
     23 CLOSE-WAIT
      5 LISTEN
      3 SYN-SENT

Connection Termination: Normal vs. Abort

Graceful Close (FIN)

Normal termination - all data delivered

Client:  close() → sends FIN
         Waits for peer's FIN
         Both sides agree connection is done

4-way handshake:
  FIN →
  ← ACK
  ← FIN
  ACK →

Can be combined (FIN+ACK together)

Abortive Close (RST)

Immediate termination - may lose data

Triggers:
  - SO_LINGER with timeout=0
  - Receiving data on closed socket
  - Connection to non-listening port
  - Firewall timeout/rejection

No TIME_WAIT needed - immediate cleanup
But: any in-flight data is lost

Half-Close

TCP allows closing one direction:

Client: shutdown(SHUT_WR)
  - Client can't send more data
  - Client can still receive
  - Server sees EOF when reading

Use case:
  "I'm done sending, but I'll wait for your response"

Example: HTTP request sent, waiting for response

Common Issues

Too Many CLOSE_WAIT

Symptoms:
  - Connections stuck in CLOSE_WAIT
  - "Too many open files" errors
  - Application eventually fails

Cause:
  - Application receiving FIN but not calling close()
  - Bug in cleanup code
  - Exception handling not closing sockets

Fix:
  - Fix application to properly close sockets
  - Use finally blocks / context managers
  - Check for file descriptor leaks

Too Many TIME_WAIT

Symptoms:
  - Thousands of TIME_WAIT connections
  - Port exhaustion for outgoing connections
  - "Cannot assign requested address" errors

Cause:
  - Many short-lived outgoing connections
  - Server closing connections (gets TIME_WAIT)

Fix:
  - Connection pooling
  - tcp_tw_reuse (client-side)
  - Let clients close first (server-side)
  - Longer-lived connections

SYN_RECV Accumulation

Symptoms:
  - Many connections in SYN_RCVD
  - New connections rejected
  - Server appears slow or unresponsive

Cause:
  - SYN flood attack
  - Slow/lossy network (ACKs not arriving)

Fix:
  - Enable SYN cookies
  - Increase SYN backlog
  - Rate limiting
  - DDoS protection

Summary

TCP states track the connection lifecycle:

StateSideMeaning
LISTENServerWaiting for connections
SYN_SENTClientHandshake in progress
SYN_RCVDServerHandshake in progress
ESTABLISHEDBothConnection open
FIN_WAIT_1CloserSent FIN, waiting ACK
FIN_WAIT_2CloserFIN ACKed, waiting peer FIN
CLOSE_WAITReceiverReceived FIN, app hasn’t closed
LAST_ACKReceiverSent FIN, waiting final ACK
TIME_WAITCloserWaiting to ensure clean close
CLOSEDBothNo connection

Key debugging insights:

  • CLOSE_WAIT accumulation = application not closing sockets
  • TIME_WAIT accumulation = many short connections (may be normal)
  • SYN_RCVD accumulation = possible SYN flood attack

This completes our deep dive into TCP. You now understand the protocol that powers most of the internet. Next, we’ll look at UDP—the simpler, faster alternative.