Home › Python › BrokenPipeError 32: Fix Writes to Closed Pipes, Sockets
Beginner 5 min · September 23, 2026

BrokenPipeError 32: Fix Writes to Closed Pipes, Sockets

Catch BrokenPipeError and exit quietly when the reader leaves.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 9 min
  • ✓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
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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.
✦ Definition~90s read
What is Python BrokenPipeError 32 Fix?

BrokenPipeError is Python's name for the POSIX EPIPE condition: a write to a pipe, FIFO, or socket that has no readers left. Pipes are kernel buffers with a write end and a read end, and the contract is simple — bytes written must have someone to read them.

★
Imagine reading a letter aloud to a friend over the phone, and they hang up halfway through.

When the last reader closes its descriptor, the kernel refuses further writes with the EPIPE error and delivers SIGPIPE to the writer. Python installs SIG_IGN for that signal at startup, so instead of dying you receive BrokenPipeError, an OSError subclass carrying errno 32, at the exact write call that found nobody home.

You will meet it in three places. Command-line tools piped to head or less crash when the pager quits early and closes stdout's read end. Network servers hit it when a client disconnects mid-response and the next sendall lands on a dead socket. Daemons writing to rotated logs or supervised stdout trip over it when the far end is replaced mid-write.

The mental model that fixes all three: the exception describes the reader, not your code. Your write was fine; the audience left. So the handler belongs wherever reader lifetime is known — the CLI main loop, the per-connection handler, the log driver — and its job is to decide whether the lost bytes were disposable output (drop quietly) or authoritative state (fail loudly and reconcile).

Plain-English First

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.

broken_pipe_socketpair.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket

a, b = socket.socketpair()
b.close()  # reader goes away, like quitting `less` mid-stream
count = 0
try:
    while count < 200:
        a.sendall(b"y" * 65536)
        count += 1
    print("no error after", count, "writes")
except (BrokenPipeError, ConnectionResetError) as exc:
    print(type(exc).__name__, "after", count, "writes:", exc)
finally:
    a.close()
📊 Production Insight
A metrics exporter treated EPIPE as fatal and crashed its loop every time the collector restarted. Catching it as routine reconnect traffic took restarts from incidents to non-events.
🎯 Key Takeaway
EPIPE means no readers are left on your write target — find which reader left, then decide if the output mattered.

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.

pipe_no_reader.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
import os

r, w = os.pipe()
os.close(r)  # nobody is reading, as when `head` exits early
try:
    os.write(w, b"hello")
    os.write(w, b"x" * 1000000)
    print("write succeeded")
except BrokenPipeError as exc:
    print("BrokenPipeError:", exc)
finally:
    os.close(w)
💡Head Quitting Is a Success Case
Catch the error around the whole output loop and exit 0. A reader that got what it wanted is a success, not a crash — don't report it as one.
📊 Production Insight
A nightly report emailed tracebacks to the whole data team whenever anyone previewed it with head. One boundary guard ended months of reply-all confusion about whether the numbers were wrong.
🎯 Key Takeaway
Guard the output loop, close streams quietly, exit 0 — a satisfied pager is a success.

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.

sigpipe_handling.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
import signal
import socket

print("SIGPIPE handler:", signal.getsignal(signal.SIGPIPE))
a, b = socket.socketpair()
b.close()
try:
    a.sendall(b"ping")
except BrokenPipeError as exc:
    print("caught instead of killed:", exc)
finally:
    a.close()
📊 Production Insight
A C extension reset SIGPIPE process-wide and a healthy service started dying with 141s that left no tracebacks. Auditing signal handlers at startup found the culprit in an afternoon.
🎯 Key Takeaway
Python ignores SIGPIPE so you get an exception instead of a corpse — leave that default 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.

client_disconnect.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import socket

srv, cli = socket.socketpair()
cli.close()  # client vanishes mid-response
count = 0
try:
    while count < 200:
        srv.sendall(b"y" * 65536)
        count += 1
    print("no error after", count, "writes")
except (BrokenPipeError, ConnectionResetError) as exc:
    print("client gone after", count, "writes:", type(exc).__name__)
finally:
    srv.close()
📊 Production Insight
An API team paged on every mobile hang-up and muted the channel within a month. Moving EPIPE to a debug counter cut alerts 94% and the next genuine outage paged in two minutes.
🎯 Key Takeaway
Hang-ups are traffic, not bugs: catch at the connection boundary, log at debug, close, move on.

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.

safe_send.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import errno
import socket

def safe_send(sock, payload):
    try:
        sock.sendall(payload)
        return True
    except BrokenPipeError:
        return False  # reader left; safe to drop logs and metrics
    except OSError as exc:
        if exc.errno in (errno.EPIPE, errno.ESHUTDOWN):
            return False
        raise

live_a, live_b = socket.socketpair()
print("live send ok:", safe_send(live_a, b"hello"))
dead_a, dead_b = socket.socketpair()
dead_b.close()
print("dead send dropped:", safe_send(dead_a, b"hello") is False)
live_a.close()
live_b.close()
dead_a.close()
📊 Production Insight
A billing worker shared a helper with the metrics exporter and inherited its drop-quietly behavior. Invoices went unsent with zero alerts. Separate helpers for disposable and authoritative writes closed the hole.
🎯 Key Takeaway
Disposable bytes get dropped quietly; authoritative bytes fail loudly — branch explicitly, never swallow.

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.

emit_guard.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys

def emit(stream, line):
    try:
        stream.write(line + "\n")
        stream.flush()
        return True
    except BrokenPipeError:
        return False

class DeadStream:
    def write(self, s):
        raise BrokenPipeError(32, "Broken pipe")

    def flush(self):
        pass

print("real stdout:", emit(sys.stdout, "hello"))
print("dead pipe handled:", emit(DeadStream(), "hello") is False)
📊 Production Insight
A CLI team added the boundary guard plus a CI job piping every report command to head -c 0. Pipe-related tickets went from monthly to zero across fourteen tools.
🎯 Key Takeaway
Flush in chunks, catch at the boundary, close quietly — then prove it with head -c 0 tests.
● Production incidentPOST-MORTEMseverity: high

40,000 Sentry Events From Phones Hanging Up Mid-Download

Symptom
On Saturday morning Sentry showed 40,000 new BrokenPipeError events from the download service. The on-call engineer muted the issue as known noise — and missed the database latency alert that arrived in the same flood and was dropped by quota limits. Checkout downloads stalled for three hours before anyone noticed.
Assumption
The team assumed every traceback in Sentry deserved attention, and EPIPE looked scary — it said Error right in the name. Nobody asked what fraction of mobile clients abandon a large download before it finishes, and the alert rules treated a client hang-up identically to a database outage.
Root cause
The download handler had no boundary guard: when a client disconnected mid-stream, sendall raised BrokenPipeError and the framework logged it as an unhandled error straight to Sentry. A client behavior change tripled abandonment, so routine hang-ups scaled into a 40k-event flood that exhausted the Sentry quota and dropped subsequent events — including the first signals of an unrelated database slowdown.
Fix
EPIPE moved to debug-level logging with a disconnect-rate dashboard, excluded from Sentry entirely. The per-connection handler catches BrokenPipeError, closes the socket, and records a counter. Paging now fires on 5xx rate, never on hang-ups. Sentry volume dropped 94% overnight and the next real outage paged in two minutes instead of hiding in noise.
Key lesson
  • 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.
Production debug guideFive checks that separate a quit pager, a reset signal, buffering luck, a gone client, and a rotation race.5 entries
Symptom · 01
Traceback names print or sys.stdout.write, but only when piped
→
Fix
Reproduce it on purpose: 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.
Symptom · 02
Process vanishes with exit code 141 and no traceback at all
→
Fix
Run 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.
Symptom · 03
Error appears intermittently depending on output size
→
Fix
Run 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.
Symptom · 04
Server logs EPIPE bursts correlated with mobile or flaky clients
→
Fix
Run 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.
Symptom · 05
Failures cluster exactly at log-rotation time
→
Fix
Run 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.
BrokenPipeError 32 — Causes and Fixes at a Glance
Root CauseHow to ConfirmFixPrevention
CLI output piped to head or less quitting earlyExit code 1 with traceback naming print or sys.stdout.write; echo ${PIPESTATUS[0]} shows the failureCatch BrokenPipeError around the loop, close quietly, sys.exit(0)Shared emit() helper plus a CI test piping output to head -c 0
Socket client disconnects mid-responseLogs show send/sendall frames with churned mobile or curl clients; no server-side state changeCatch at the connection boundary, log at debug, close the socketExclude EPIPE from error alerts; track disconnect rate as traffic, not bugs
Log rotator or supervisor closes the fdFailures cluster at rotation times; lsof shows a replaced stdout targetReopen logs on SIGHUP and guard writes during rotation windowsCoordinate rotation signals with the app; test a rotate during load
Library surfaces EPIPE as a hard errorStack passes through helper frames that had no reason to catch itHandle at the boundary where reader lifetime is known, not deep in helpersDocument which functions may raise it; keep helpers transparent
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
broken_pipe_socketpair.pya, b = socket.socketpair()What Errno 32 Means
pipe_no_reader.pyr, w = os.pipe()Head and Less Closing Stdout
sigpipe_handling.pyprint("SIGPIPE handler:", signal.getsignal(signal.SIGPIPE))SIGPIPE and Python's BrokenPipeError
client_disconnect.pysrv, cli = socket.socketpair()Client Disconnects
safe_send.pydef safe_send(sock, payload):When It Is Safe to Ignore
emit_guard.pydef emit(stream, line):Production-Safe Writes

Key takeaways

1
Errno 32 means you wrote to a pipe or socket whose reader already left
handle the reader, not the write.
2
Python ignores SIGPIPE so the condition arrives as a catchable BrokenPipeError instead of killing your process.
3
Piped CLI tools must catch the error around the output loop and exit 0; a satisfied head is not a failure.
4
Client disconnects are routine traffic
log at debug, close the socket, never page on them.
5
Ignoring is safe for disposable output like logs and paged reports, never for authoritative writes.
6
Flush in chunks, catch at the boundary, and close descriptors quietly to avoid a second exception.

Common mistakes to avoid

5 patterns
×

Catching broad Exception to silence the traceback

Symptom
Real bugs vanish along with the pipe noise, and debugging gets harder because every failure looks identical.
Fix
Catch BrokenPipeError narrowly around the output loop and exit 0. Keep except Exception for real bugs — never let a blanket handler swallow the pipe signal silently.
×

Letting every client disconnect page Sentry or PagerDuty

Symptom
Thousands of junk error events burn quota and bury the one real outage signal that mattered that night.
Fix
Drop EPIPE events to debug-level logging and exclude them from error trackers. Alert on rates of 500s, not on clients hanging up.
×

Resetting SIGPIPE to SIG_DFL in your own process

Symptom
The process dies silently with exit code 141 and no traceback, which looks like a crash instead of a closed reader.
Fix
Leave Python's SIGPIPE handling alone. If a child process needs default behavior, pass restore_signals=True to subprocess instead.
×

Retrying writes against a pipe the reader already closed

Symptom
Retry storms hammer a dead descriptor, loop forever, and turn a harmless hang-up into a CPU spike.
Fix
Treat a dead pipe as final: stop writing, close quietly, and exit. Retries only make sense for idempotent reads, never for pushing bytes at a reader that left.
×

Flushing stdout after every single print in a hot loop

Symptom
Throughput collapses on large reports because each line pays a full write syscall even when nobody closed the pipe.
Fix
Flush at chunk boundaries (every N lines) instead of every print. You keep pipe failures prompt without paying a syscall per line.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is BrokenPipeError, and which errno does it carry?
Q02JUNIOR
Why does `python report.py | head -5` crash a script that works otherwis...
Q03SENIOR
How does Python's SIGPIPE handling differ from a C program's?
Q04SENIOR
Your server logs thousands of EPIPE errors from mobile clients. What do ...
Q05SENIOR
How do you design output handling for a service that is both piped and s...
Q01 of 05JUNIOR

What is BrokenPipeError, and which errno does it carry?

ANSWER
It subclasses OSError with errno set to EPIPE (32). The kernel raises SIGPIPE when you write to a pipe or socket with no readers; Python ignores that signal at startup and converts the condition into this catchable exception naming the failed write.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
My script ran fine for months — why did BrokenPipeError appear today?
02
Is it ever safe to ignore BrokenPipeError?
03
What is the difference between BrokenPipeError and ConnectionResetError?
04
How should a CLI tool exit when head closes the pipe?
05
Will my whole process die the moment the pipe breaks?
06
Where exactly should the try/except live?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Errors. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Python Event Loop Closed Fix
13 / 18 · Errors
Next
Python ConnectionResetError 104 Fix
→