BrokenPipeError 32: Fix Writes to Closed Pipes, Sockets
Catch BrokenPipeError and exit quietly when the reader leaves.
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
- ✓Running Python scripts from the terminal, with pipes like
| - ✓Basic sockets or HTTP: clients, servers, and responses
- ✓Reading tracebacks to find the failing line and errno
- Fix it fast: wrap the write loop in try/except BrokenPipeError and exit 0 — the reader (head, a pager, a closed socket) is gone, not your logic.
- Errno 32 means you wrote to a pipe or socket whose read end closed; Python turns the SIGPIPE kill into this exception so you can handle it.
- For CLI tools piped to head: flush stdout, catch the error around print loops, and close descriptors quietly to avoid a second traceback.
- For servers: a disconnect mid-response is routine — log it at debug, close the socket, and never retry a write the client abandoned.
Imagine reading a letter aloud to a friend over the phone, and they hang up halfway through. You keep talking into a dead line — that is BrokenPipeError. The fix isn't to shout louder. If you were just chatting (a log line, a paged report), you hang up quietly. If you were dictating a contract (a data file, a payment), you stop and make sure nothing half-written gets treated as final.
Your script worked for months. Then someone ran it as python report.py | head -5, or a phone client hung up mid-download, and the logs filled with BrokenPipeError: [Errno 32] Broken pipe. The traceback points at an innocent print or sendall, and nothing about your logic looks wrong — because your logic isn't wrong. The reader left.
That mismatch is what makes errno 32 so noisy in production. The exception fires at the exact line that did everything right, so teams treat it like a bug, alert on it like an outage, and burn error-tracker quota on what is really routine traffic: pagers quitting early, clients disconnecting, rotators replacing files. Meanwhile the genuine question — was this output disposable or authoritative — never gets asked.
This article draws that line clearly. You'll learn what errno 32 means at the kernel level, why head and less trigger it, how Python's SIGPIPE handling turns sudden death into a catchable exception, how to handle client disconnects without pager fatigue, exactly when ignoring the error is safe, and the narrow guard pattern that keeps production writes quiet and correct.
What Errno 32 Means: Writing After the Reader Went Away
Errno 32 has the kernel's blunt name: EPIPE, broken pipe. It fires when your process writes to a pipe, FIFO, or socket whose read end has no readers left. The classic sequence is short: the reader — head after five lines, a user quitting less, a phone client hanging up — closes its descriptor, your next write finds nobody home, and the kernel answers with SIGPIPE plus an EPIPE return on the syscall.
What you see in Python is the civilized version of that event. A C program dies on SIGPIPE by default; the Python runtime installs SIG_IGN for SIGPIPE at startup, so the deadly signal is discarded and the failed syscall surfaces as BrokenPipeError, a subclass of OSError with errno set to 32. You get a catchable exception naming the exact write that failed instead of a corpse and exit code 141.
Don't confuse it with its cousins. ECONNRESET (104) means the peer slammed the TCP connection shut with a RST packet; EPIPE means your own write found the local read end gone. Both say the other side left, but they arrive through different kernel paths and sometimes need different handling. Either way the diagnostic move is identical: read the traceback's frame for which descriptor died, ask whether that output was disposable or authoritative, and handle it at the boundary that owns the reader relationship.
Head and Less Closing Stdout: the Classic CLI Crash
The most reported BrokenPipeError in the wild comes from a two-word command: head. You run python report.py | head -5 to preview output, head prints five lines and exits, the shell closes the read end of your stdout pipe, and your next print explodes. Less does the same when the user presses q. Your script is perfectly correct; its audience simply stopped listening early.
The failure timing confuses people because buffering hides it. With block-buffered stdout, small outputs flush once at exit — possibly after the reader left, possibly into a pipe that still accepts bytes — so tiny reports survive while big ones crash. That size dependence sends teams chasing data bugs that don't exist. Run with python -u once and the crash becomes deterministic, which perversely makes it easier to fix.
The fix is a boundary guard, not output surgery. Wrap the producing loop in try/except BrokenPipeError, and in the handler close stdout and stderr quietly (closing stderr avoids a second traceback while the interpreter tears down streams), then sys.exit(0). Exit zero matters: it tells the pipeline your program succeeded, because it did — the reader got everything it asked for. The snippet below reproduces the exact kernel condition with os.pipe so you can practice the pattern without needing head.
SIGPIPE and Python's BrokenPipeError: Why You See an Exception
Every Unix C programmer learns SIGPIPE the hard way: write to a readerless pipe and the kernel kills your process with prejudice. No handler runs, no cleanup executes, and the shell reports exit code 141 (128 plus signal 13). That behavior is sane for 1970s filters but brutal for applications that deserve to decide for themselves.
Python decided differently at startup. Before your first line runs, the runtime sets SIGPIPE to SIG_IGN, so the killer signal is silently discarded and the syscall simply returns EPIPE — which the I/O layer raises as BrokenPipeError. That single design choice converts sudden death into a catchable, loggable, testable event. You can verify it any time: signal.getsignal(signal.SIGPIPE) prints SIG_IGN in a normal interpreter.
Two corollaries bite people. First, if you or a native extension reset SIGPIPE to SIG_DFL, you restore silent death — don't, unless a very specific subprocess contract demands it. Second, child processes are a separate world: the subprocess module resets SIGPIPE to SIG_DFL in the child by default, so pipelines like your script feeding a C helper behave with classic Unix semantics on the far side. Know which side of the fork you're on, handle the exception on yours, and leave the signal disposition alone.
Client Disconnects: Sockets That Close Mid-Response
On the server side, BrokenPipeError is the sound of a client hanging up mid-sentence. Mobile apps lose signal in tunnels, users mash cancel, curl gets Ctrl-C, load-balancer health checks open and abandon connections by design. Your handler is formatting a response nobody will read, the next sendall finds a closed socket, and errno 32 lands in your logs.
At any real scale this is background radiation, not signal. A service doing ten thousand responses a minute will see dozens of hang-ups an hour from normal human behavior alone. Teams that alert on each one train themselves to ignore the alert channel entirely — which is how the 40,000-event weekend in our incident report happened. The disconnect rate is product analytics; it is not an error budget burn.
Handle it at the per-connection boundary with a narrow except clause: catch BrokenPipeError (and its sibling ConnectionResetError), log at debug with the client address for forensics, close the socket, and return. Never retry pushing bytes at a client that hung up — there is nobody to receive them, and retries convert a one-line event into a hot loop. If you need the business signal (abandoned checkouts, dropped downloads), emit a counter, not a traceback.
When It Is Safe to Ignore: Logs, Pipes, and Fire-and-Forget Writes
Not every BrokenPipeError deserves handling — some deserve deliberate ignoring, and knowing which is which separates calm on-call rotations from noisy ones. Safe to drop: log lines to a collector that restarted, metrics to a fire-and-forget UDP-style sink, paged report output, progress bars, and anything the reader can trivially re-request. These outputs are hints, not records; losing one to a closed reader costs nothing.
Never ignore: data files, billing records, database commits, queue acknowledgments, audit trails — anything where a half-write could be mistaken for a complete one. If the reader's departure leaves your system's state ambiguous, you don't have a pipe problem, you have a durability problem, and swallowing the exception converts it into silent corruption.
The decision rule fits in one sentence: if the bytes are reproducible or disposable, drop them quietly; if they're authoritative, fail loudly and let the retry or reconciliation path run. Encode that rule as a helper like the safe_send below — it returns False for dead readers so callers branch explicitly, while unexpected errors still raise and get the attention they deserve. Explicit branches get reviewed; swallowed exceptions don't.
Production-Safe Writes: Catch, Guard, and Flush in Order
The production pattern has three parts in a fixed order: flush in chunks so failures surface promptly, catch at the boundary so one decision covers every write, and close descriptors quietly so teardown can't raise a second exception. Get the order right and pipe handling becomes a solved problem instead of a recurring ticket.
Chunked flushing is the performance compromise. Flushing every print costs a syscall per line and tanks large reports; never flushing lets megabytes buffer and delays the failure past the point where exiting cleanly is easy. Flushing every few hundred lines — or flushing before a long computation — surfaces a dead reader quickly without measurable cost. Python's print(flush=True) on chunk boundaries is all it takes.
The boundary handler wraps the whole producing loop, not individual prints: try around the loop, except BrokenPipeError that closes stdout then stderr inside a nested guard, then sys.exit(0). Test it the way production will run it: pipe to head -c 0 for the instant-quit case, pipe to head -5 for the mid-stream case, and close a socket mid-response for the server case. The emit helper below shows the shape in miniature — boolean results, no surprise tracebacks, quiet teardown.
40,000 Sentry Events From Phones Hanging Up Mid-Download
- Name severity by cause, not by exception class. A client hang-up is traffic; only your own failures deserve pages.
- Error-tracker quota is an operational resource. Spending 40k events on routine disconnects hid the outage that mattered.
- Disconnect rate belongs on a dashboard next to throughput, so product sees real user behavior instead of engineers seeing false alarms.
python report.py 2>&1 | head -c 0; echo "pipe exit: ${PIPESTATUS[0]}" captures your script's real exit code separately from head's. If your code exits 1 with a traceback naming print, the diagnosis is confirmed — add the boundary guard.python -c "import signal; print(signal.getsignal(signal.SIGPIPE))". Healthy output shows SIG_IGN. If your app or a native extension reset it to SIG_DFL, the process dies with exit 141 and no traceback — restore SIG_IGN and handle the exception instead.python -u report.py 2>trace.log | head -5 and compare with buffered runs. If unbuffered output fails faster, the error was always there — buffering just delayed it. The fix is the same guard; buffering only changes when the failure surfaces.curl --max-time 2 http://localhost:8000/big-report & sleep 0.2; kill -9 %1 against a local endpoint while tailing server logs. If the server logs EPIPE on the killed request, disconnect handling is the gap — catch at the per-connection boundary and log at debug.grep -c BrokenPipeError /var/log/app/*.log across rotated files and check timestamps against rotation: grep -l 'logrotate' /var/log/syslog. Failures clustered at rotation boundaries mean the fd was replaced mid-write — coordinate SIGHUP reopening with the rotator.| File | Command / Code | Purpose |
|---|---|---|
| broken_pipe_socketpair.py | a, b = socket.socketpair() | What Errno 32 Means |
| pipe_no_reader.py | r, w = os.pipe() | Head and Less Closing Stdout |
| sigpipe_handling.py | print("SIGPIPE handler:", signal.getsignal(signal.SIGPIPE)) | SIGPIPE and Python's BrokenPipeError |
| client_disconnect.py | srv, cli = socket.socketpair() | Client Disconnects |
| safe_send.py | def safe_send(sock, payload): | When It Is Safe to Ignore |
| emit_guard.py | def emit(stream, line): | Production-Safe Writes |
Key takeaways
Common mistakes to avoid
5 patternsCatching broad Exception to silence the traceback
Letting every client disconnect page Sentry or PagerDuty
Resetting SIGPIPE to SIG_DFL in your own process
Retrying writes against a pipe the reader already closed
Flushing stdout after every single print in a hot loop
Interview Questions on This Topic
What is BrokenPipeError, and which errno does it carry?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
That's Errors. Mark it forged?
5 min read · try the examples if you haven't