SO_LINGER Demystified: Controlling TCP Close Behavior in Linux

This article explains the SO_LINGER socket option's three closure modes, demonstrates why SO_LINGER=0 sends RST instead of FIN, and outlines Linux's preferred solutions for TIME_WAIT accumulation—connection reuse, tcp_tw_reuse, tcp_fin_timeout, and port range expansion—over abrupt connection termination.

Code Archaeology
Code Archaeology
Code Archaeology
SO_LINGER Demystified: Controlling TCP Close Behavior in Linux

1. Issue

TCP connection closure follows a four-way handshake where the active closer enters TIME_WAIT for 2MSL before releasing resources. Linux provides the SO_LINGER socket option that alters this standard flow.

2. Dialogue: Understanding SO_LINGER

2.1 Normal TCP Close (without SO_LINGER)

Standard TCP performs a graceful close:

Application calls close() Kernel sends remaining data in send buffer, then sends FIN Peer acknowledges with ACK Peer sends its FIN Local end acknowledges with ACK Connection enters

TIME_WAIT
close()

does not mean the connection is fully closed; the kernel takes over remaining send work.

2.2 Problems with Early BSD Socket Design

Application cannot know if data was actually sent successfully

Process exit leaves resource cleanup uncertain

Some programs want to terminate connections immediately.

2.3 SO_LINGER Design Goal

BSD introduced SO_LINGER to let applications control close() semantics (immediate return vs. wait for send completion). The option uses a struct linger:

struct linger {
    int l_onoff;
    int l_linger;
};

It provides three closure strategies:

Default (off): Kernel graceful close linger > 0: Blocking close() waits for send completion linger = 0: Immediate RST sent

When SO_LINGER > 0

close()
  ↓
Wait up to N seconds
  ↓
Until:
  - Data sent
  - ACK received
  - Timeout

On timeout, returns -1 with errno set to ETIMEDOUT.

When SO_LINGER = 0

close()
  ↓
Send RST
  ↓
Discard send buffer
  ↓
Connection destroyed immediately

No FIN is sent, no TIME_WAIT state entered. This is a deliberate abort mechanism.

2.4 Why Some Libraries Default to SO_LINGER=0

Most libraries do not enable SO_LINGER by default. When seen, it is usually SO_LINGER=0 because RST releases resources faster than FIN, avoiding TIME_WAIT accumulation.

2.5 Better Solutions for TIME_WAIT Accumulation

Linux offers superior approaches at three layers, in priority order:

Architecture optimization > TCP parameter tuning > Port/connection reuse > (rarely) abort close

Key Insight: TIME_WAIT Is Not a Bug

TIME_WAIT

guarantees two correctness properties:

Delayed packets from old connections do not corrupt new ones

Lost final ACK can be retransmitted

Eliminating TIME_WAIT entirely is wrong; the goal is to prevent it from exhausting resources.

Layer 1: Avoid Active Close on Server

Let clients initiate closure so TIME_WAIT appears on client side, removing server pressure.

Layer 2: Connection Reuse

Excessive TIME_WAIT often signals too many short connections. Fundamental fix: reduce connection creation via:

HTTP KeepAlive

HTTP/2

Connection pools

Layer 3: Linux Kernel Tuning

tcp_tw_reuse

net.ipv4.tcp_tw_reuse = 1

Allows TIME_WAIT sockets to be reused for new connections when the new connection's timestamp exceeds the old one's (PAWS check).

tcp_fin_timeout

net.ipv4.tcp_fin_timeout

Default 60 seconds; can be lowered but not too small. Accelerates release of orphan sockets stuck in FIN_WAIT_2.

Expand Port Range

net.ipv4.ip_local_port_range

Wider range reduces port exhaustion from TIME_WAIT sockets.

Layer 4: Modern Protocols

Protocols like QUIC bypass the TIME_WAIT issue entirely.

Summary of Correct Strategy

Reduce short connections

Reuse connections

Enable tcp_tw_reuse Expand port range

3. Experiment

Code to set SO_LINGER=0:

void set_so_linger_zero(int sockfd) {
    struct linger linger_opt;
    linger_opt.l_onoff = 1;   /* Enable SO_LINGER */
    linger_opt.l_linger = 0;  /* Linger time = 0 */
    if (setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &linger_opt,
        sizeof(linger_opt)) < 0) {
        perror("setsockopt(SO_LINGER)");
        exit(EXIT_FAILURE);
    }
}

Client Sets SO_LINGER=0 (tcpdump)

23:51:38.414443 IP 127.0.0.1.40854 > 127.0.0.1.8888: Flags [S], seq 2975794497, win 43690, options [mss 65495,sackOK,TS val 855851546 ecr 0,nop,wscale 10], length 0
23:51:38.414452 IP 127.0.0.1.8888 > 127.0.0.1.40854: Flags [S.], seq 1521860988, ack 2975794498, win 43690, options [mss 65495,sackOK,TS val 855851546 ecr 855851546,nop,wscale 10], length 0
23:51:38.414458 IP 127.0.0.1.40854 > 127.0.0.1.8888: Flags [.], ack 1521860989, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 0
23:51:38.414492 IP 127.0.0.1.40854 > 127.0.0.1.8888: Flags [P.], seq 2975794498:2975794504, ack 1521860989, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 6
23:51:38.414495 IP 127.0.0.1.8888 > 127.0.0.1.40854: Flags [.], ack 2975794504, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 0
23:51:38.414513 IP 127.0.0.1.8888 > 127.0.0.1.40854: Flags [P.], seq 1521860989:1521861006, ack 2975794504, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 17
23:51:38.414517 IP 127.0.0.1.40854 > 127.0.0.1.8888: Flags [.], ack 1521861006, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 0
23:51:38.414520 IP 127.0.0.1.8888 > 127.0.0.1.40854: Flags [P.], seq 1521861006:1521861012, ack 2975794504, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 6
23:51:38.414522 IP 127.0.0.1.40854 > 127.0.0.1.8888: Flags [.], ack 1521861012, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 0
23:51:38.414539 IP 127.0.0.1.40854 > 127.0.0.1.8888: Flags [R.], seq 2975794504, ack 1521861012, win 43, options [nop,nop,TS val 855851546 ecr 855851546], length 0

Client sends final RST packet.

Server Sets SO_LINGER=0 (tcpdump)

23:57:33.950675 IP 127.0.0.1.45936 > 127.0.0.1.8888: Flags [S], seq 3408802811, win 43690, options [mss 65495,sackOK,TS val 856207082 ecr 0,nop,wscale 10], length 0
23:57:33.950680 IP 127.0.0.1.8888 > 127.0.0.1.45936: Flags [S.], seq 2475716703, ack 3408802812, win 43690, options [mss 65495,sackOK,TS val 856207082 ecr 856207082,nop,wscale 10], length 0
23:57:33.950687 IP 127.0.0.1.45936 > 127.0.0.1.8888: Flags [.], ack 2475716704, win 43, options [nop,nop,TS val 856207082 ecr 856207082], length 0
23:57:33.950707 IP 127.0.0.1.45936 > 127.0.0.1.8888: Flags [P.], seq 3408802812:3408802818, ack 2475716704, win 43, options [nop,nop,TS val 856207082 ecr 856207082], length 6
23:57:33.950710 IP 127.0.0.1.8888 > 127.0.0.1.45936: Flags [.], ack 3408802818, win 43, options [nop,nop,TS val 856207082 ecr 856207082], length 0
23:57:33.950763 IP 127.0.0.1.8888 > 127.0.0.1.45936: Flags [R.], seq 2475716704, ack 3408802818, win 43, options [nop,nop,TS val 856207082 ecr 856207082], length 0

Server sends final RST packet.

4. Kernel Implementation (Linux 5.15.198)

SO_LINGER Handling Flow

User space
    │ setsockopt(SOL_SOCKET, SO_LINGER, &linger, ...)
    ▼
net/core/sock.c: sock_setsockopt()
    │ case SO_LINGER
    │ ├─ Set sock_flag(sk, SOCK_LINGER)
    │ └─ Set sk→sk_lingertime
    ▼
User space
    │ close(sockfd)
    ▼
net/ipv4/tcp.c: tcp_close()
    │ Check sock_flag(sk, SOCK_LINGER)
    │
    ├─ l_linger = 0 → tcp_send_active_reset() [hard close]
    │
    └─ l_linger > 0 → Wait for timeout or data sent

tcp_tw_reuse Check Flow

New connection (connect())
    │
    ▼
Check if port occupied by TIME_WAIT
    │
    ▼
net/ipv4/tcp_ipv4.c: tcp_twsk_unique()
    │
    ├─ Check tw_substate == TCP_FIN_WAIT2
    │   └─ Yes → Reuse forbidden
    │
    ├─ Check tcp_tw_reuse value
    │   │
    │   ├─ 0 → Reuse forbidden
    │   │
    │   └─ 2 → Check loopback interface
    │       └─ Non-loopback → Reuse forbidden
    │
    ├─ Check timestamp (PAWS)
    │   └─ Timestamp unsafe → Reuse forbidden
    │
    └─ Adjust sequence number
        └─ write_seq = old_seq + 65535 + 2

tcp_fin_timeout Timeout Flow

Enter FIN_WAIT2 state
    │
    ▼
include/net/tcp.h: tcp_fin_time()
    │
    ├─ Use tp→linger2 (if set)
    │
    └─ Use sysctl_tcp_fin_timeout
    │
    ├─ Ensure minimum >= 3.5 * RTO
    │
    └─ Return timeout value
    │
    ▼
net/ipv4/tcp.c: tcp_close()
    │
    ├─ tmo > TCP_TIMEWAIT_LEN (60s)
    │   └─ Set keepalive timer
    │
    └─ tmo <= 60s
        └─ tcp_time_wait(sk, TCP_FIN_WAIT2, tmo)
            │
            └─ Create TIME_WAIT socket
                │
                └─ Schedule timeout timer
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

TCPLinux kernelTIME_WAITnetwork programmingsocket programmingSO_LINGERtcp_tw_reusetcp_fin_timeout
Code Archaeology
Written by

Code Archaeology

Learn from the past to understand the present

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.