Fundamentals 9 min read

Why a Write After Server Close Can Succeed Once and Then Fail: TCP FIN, RST and Kernel Behavior

The article explains why a client can successfully write to a TCP socket after the server has closed its end, why the first write succeeds while the second fails, and how Linux kernel code and TCP state transitions cause this behavior.

High Availability Architecture
High Availability Architecture
High Availability Architecture
Why a Write After Server Close Can Succeed Once and Then Fail: TCP FIN, RST and Kernel Behavior

When a TCP connection is established and the server side is actively closed, many expect any subsequent write from the client to immediately return an error; however, this is not always the case.

The following C program reproduces the situation. It creates a TCP socket, connects to 127.0.0.1:9999 , ignores SIGPIPE , waits five seconds for the server to be closed, then performs two write calls:

#include
#include
#include
#include
#include
#include
#include
#include
#include
int tcp_connect() {
    int sockfd, err;
    struct sockaddr_in addr;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd != -1);
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(9999);
    err = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
    assert(err == 0);
    return sockfd;
}

int main(int argc, char **argv) {
    int n;
    int sockfd = tcp_connect();
    signal(SIGPIPE, SIG_IGN); // ignore SIGPIPE for testing
    printf("Please close the server within 5 seconds...\n");
    sleep(5);
    // write 1
    n = write(sockfd, "hello\n", 6);
    if (n == -1) { perror("first write failed"); return -1; }
    assert(n == 6);
    printf("first write succeeded!\n");
    sleep(1);
    // write 2
    n = write(sockfd, "world\n", 6);
    if (n == -1) { perror("second write failed"); return -1; }
    assert(n == 6);
    printf("second write succeeded!\n");
    return 0;
}

Run a server with ncat -l 9999 in one terminal, compile the program with gcc main.c , and execute it. The output shows the first write succeeding and the second write failing with “Broken pipe”.

Capturing the traffic with sudo tcpdump -i any -n port 9999 reveals the packet sequence: three packets for the three‑way handshake, a FIN from the server, an ACK from the client, the client’s data packet (“hello\n”), and finally a RST from the server after it has already closed its socket.

Inspecting the Linux kernel source, the function tcp_sendmsg_locked in net/ipv4/tcp_input.c returns -EPIPE only when sk->sk_err is set or the socket’s send side is shut down. After the server sends FIN, the client socket enters CLOSE_WAIT , which does not set an error, so the first write succeeds.

When the server receives the data, it notices its socket is already closed and replies with a RST. The kernel’s tcp_reset handler sets sk->sk_err = EPIPE for the client socket in CLOSE_WAIT state and eventually calls tcp_done , which disables further sending. Consequently, the second write jumps to the error path and returns -EPIPE , producing the observed failure.

This explains why the first write can succeed after the server has closed, while the subsequent write fails, and demonstrates how TCP state transitions and kernel error handling determine the observed behavior.

KernelTCPlinuxsocketrstFINwrite
High Availability Architecture
Written by

High Availability Architecture

Official account for High Availability Architecture.

0 followers
Reader feedback

How this landed with the community

login 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.