POSIX Signals - SIGPIPE

SIGPIPE (Broken Pipe, signal number 13) is a standard POSIX signal sent by the operating system kernel to a process when it attempts to write data to a pipe, FIFO, or network socket whose reading end has been closed.

Because the default action for SIGPIPE is immediate, silent process termination, an unhandled SIGPIPE is one of the most common reasons backend web servers, reverse proxies, and microservices crash in production when client browsers abruptly close connections.

How SIGPIPE Is Generated

┌────────────────────────────────────────────────────────┐
│ Process A (Writer)                                     │
│ Calls write(fd, buf, len) or send(fd, buf, len, 0)     │
└──────────────────────────┬─────────────────────────────┘
                           │
                           ▼
┌────────────────────────────────────────────────────────┐
│ Operating System Kernel                                │
│ Checks read-end reference count of pipe or socket state│
└──────────────────────────┬─────────────────────────────┘
                           │
             ┌─────────────┴─────────────┐
             │ Reader still open?        │ Reader closed / RST received?
             ▼                           ▼
┌──────────────────────────┐┌────────────────────────────┐
│ Data queued to buffer    ││ 1. Kernel raises SIGPIPE   │
│ write() returns bytes    ││ 2. write() returns -1      │
│                          ││    with errno == EPIPE     │
└──────────────────────────┘└────────────┬───────────────┘
                                         │
                                         ▼
                            [ Is SIGPIPE Handled/Ignored? ]
                                         │
                         ┌───────────────┴───────────────┐
                         │ Default: Terminate Process    │ Ignored (SIG_IGN):
                         ▼                               ▼
                 [ PROCESS DIES ]                [ Program handles EPIPE ]

The Two Common Trigger Scenarios

  1. Pipes and Shell Pipelines: In a pipeline like cat huge_file.txt | head -n 5, head reads 5 lines and exits, closing standard input. When cat subsequently attempts to write the next chunk of data to the closed pipe, the kernel sends SIGPIPE to cat, terminating it cleanly so it stops wasting CPU cycles.
  2. Network Sockets (The Server Crasher): In a TCP server, a client opens a connection, requests a large response (e.g. streaming a 50MB file), and the user closes their browser tab or loses Wi-Fi connection. The client operating system sends a TCP RST (Reset) packet. When the server calls write() or send() on the socket, the kernel delivers SIGPIPE, instantly killing the backend server if unhandled.

Three Defenses Against SIGPIPE

Production software should never allow SIGPIPE to terminate a server. There are three standard methods to neutralize it:

Method 1: Globally Ignore SIGPIPE (The Industry Standard)

The most robust approach in network daemons is to ignore SIGPIPE at process startup:

#include <signal.h>

void setup_signals(void) {
    struct sigaction sa;
    sa.sa_handler = SIG_IGN; // Ignore signal
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGPIPE, &sa, NULL);
}

When SIGPIPE is ignored, any subsequent write() or send() to a broken connection simply returns -1 with errno == EPIPE:

ssize_t bytes_sent = write(client_fd, buffer, length);
if (bytes_sent == -1) {
    if (errno == EPIPE || errno == ECONNRESET) {
        // Client disconnected gracefully; close socket and free resources
        close(client_fd);
    } else {
        perror("write error");
    }
}

Method 2: Per-Call MSG_NOSIGNAL Flag

On Linux and POSIX-compliant systems, you can suppress SIGPIPE on individual socket calls using the MSG_NOSIGNAL flag in send():

#include <sys/socket.h>

// Suppresses SIGPIPE exclusively for this transmission
ssize_t sent = send(client_fd, data, len, MSG_NOSIGNAL);
if (sent == -1 && errno == EPIPE) {
    // Handled locally without generating a signal
}

Method 3: SO_NOSIGPIPE Socket Option (macOS & BSD)

On Apple macOS and FreeBSD systems where MSG_NOSIGNAL was historically unavailable, disable SIGPIPE per-socket via setsockopt:

int optval = 1;
setsockopt(client_fd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));

Comparison of Defenses

Technique Scope Portability Thread Safety
sigaction(SIGPIPE, SIG_IGN) Entire Process Universal POSIX (Linux, macOS, BSD, Solaris) Thread-safe (process-wide mask)
send(..., MSG_NOSIGNAL) Single Syscall Linux, modern POSIX (sockets only; not pipes) Fully thread-safe
setsockopt(..., SO_NOSIGPIPE) Single Socket macOS, iOS, FreeBSD (not available on Linux) Fully thread-safe

Frequently Asked Questions (FAQ)

What is the default action when a process receives SIGPIPE?

The default action for SIGPIPE is abnormal process termination without producing a core dump. If an application does not explicitly ignore SIGPIPE or install a custom signal handler, any write to a closed socket or pipe causes the operating system to kill the process immediately.

What error does write() return when SIGPIPE is ignored?

When SIGPIPE is ignored (SIG_IGN) or blocked, write() and send() return -1 and set errno to EPIPE (Broken pipe). This allows software to gracefully clean up client connections and log disconnect events rather than crashing.

Why does SIGPIPE exist if write() can already return EPIPE?

SIGPIPE was introduced in early Unix to terminate upstream processes in command-line shell pipelines (like yes | head -n 1) as soon as the downstream consumer exits. Without SIGPIPE, infinite generator programs like yes would continue executing and spinning the CPU forever if they did not explicitly check the return value of every write operation.

Related Topics