skip to content

A bash script running with `set -o pipefail` intermittently exits with status 141 on the line `producer | head -n 20`. What does 141 mean there, why is it intermittent, and how should the script handle it?

level: seniorimportance: nice to knowfreq 34%

answer

  1. 128 plus the signal number
  2. signal 13 is SIGPIPE
  3. the reader quit first
  4. a race with the pipe buffer
  5. accept it, do not disable pipefail

basics

~20 s

141 is 128 plus 13, the status bash records for a process killed by SIGPIPE. head exits after 20 lines and closes the pipe, so the producer dies on its next write, and pipefail promotes that 141 to the whole pipeline's status.

solid answer

~50 s

Exit status 141 is bash's encoding for "killed by signal 13", which is SIGPIPE — the producer tried to write into a pipe whose reader had already gone away. `head -n 20` exits as soon as it has its twenty lines, and the next write from `producer` gets SIGPIPE. Without `pipefail` you would never see it, because the pipeline would report `head`'s 0; with pipefail the producer's 141 becomes the pipeline's status. It is intermittent because it is a race: if the producer happens to finish and exit before `head` closes the pipe, or all its output already fit in the pipe buffer, nobody gets signalled and the status is 0. This is expected behaviour, not a bug, so handle it explicitly — append `|| true` to that pipeline, scope pipefail off around it, or inspect `${PIPESTATUS[@]}` and treat a leading 141 as success.

code

bash · 12 lines
bash
#!/usr/bin/env bash

set -o pipefail
yes | head -n 2 >/dev/null
echo "status: $?  stages: ${PIPESTATUS[*]}"   # 141  141 0

yes | head -n 2 >/dev/null
st=("${PIPESTATUS[@]}")
case ${st[0]} in
  0|141) echo "producer finished or was SIGPIPEd - fine" ;;
  *)     echo "producer failed: ${st[0]}" >&2; exit "${st[0]}" ;;
esac

go deeper

for a junior

Recognise that exit statuses above 128 mean the process was killed by a signal, and that piping into head can end the upstream command early on purpose.

for a middle

Do the arithmetic out loud: 141 is 128 plus 13, SIGPIPE, so the producer wrote into a pipe head had already closed. Explain why pipefail is what made it visible.

for a senior

Show you can diagnose the intermittency as a buffer-size race and pick a targeted fix — tolerate 141 from the producer stage, scope pipefail off, or restructure so nothing surplus is produced.

for a principal

Own the rule the team follows: a strict-mode standard has to come with a sanctioned way to mark benign non-zero statuses, or engineers will disable the safety net across the codebase to quiet one line.

## Decoding 141 When a command is terminated by a signal, bash reports its exit status as `128 + signal number`. SIGPIPE is signal 13 on Linux and macOS, so `128 + 13 = 141`. Seeing 141 anywhere in a script's status is therefore not a mystery error code — it is a specific statement: *this process was killed because it wrote to a pipe with no reader*. The same arithmetic explains its neighbours: 130 is SIGINT (Ctrl-C, 2), 137 is SIGKILL (9), 143 is SIGTERM (15). If a script's exit status is in the 128–165 range, read it as a signal. ## Why `head` produces it `head -n 20` reads twenty lines and exits. Its exit closes the read end of the pipe. The producer upstream is still running and eventually issues another `write()` into that pipe; with no reader, the default disposition of SIGPIPE terminates it. That is the entire mechanism of an early-exiting reader, and it is exactly what makes `head`, `grep -q`, `grep -m 1`, and `sed 1q` cheap on huge inputs — they stop the producer instead of waiting for it. ```bash set -o pipefail yes | head -n 2 >/dev/null echo "$? / ${PIPESTATUS[*]}" # 141 / 141 0 ``` Note `head` itself exited 0. Only the producer carries the 141. ## Why it is intermittent It is a race between the producer finishing and the reader closing. Two ways nobody gets signalled: - The producer's total output fits in the pipe buffer, so every write completes before `head` exits and the producer terminates normally. - The producer is fast and short-lived, exiting on its own before `head` has read its quota. With a small file the pipeline returns 0 every time; with a large one it returns 141 every time; near the boundary it flips run to run. That is why the symptom typically surfaces as "the CI job started failing when the log got big", which is a confusing bug report until you decode the 141. A second source of variability is the producer itself. Some programs install a handler for SIGPIPE or ignore it and instead get `EPIPE` from `write()`, in which case they exit with their own error status (Python famously prints a `BrokenPipeError` traceback and exits non-zero, and `grep` may report a write error). So the same shape of pipeline can yield 141, or 1, or 2, depending on what is upstream. ## Handling it in a script First decide what the pipeline means. If an early-exiting reader is intentional, a SIGPIPE'd producer is success, and the script should say so rather than pretending the status did not happen. Simplest, when the whole pipeline is allowed to be non-zero: ```bash producer | head -n 20 || true ``` Scoped, when you want the rest of the file strict: ```bash set +o pipefail producer | head -n 20 set -o pipefail ``` Precise, when you want to accept 141 from the producer but still fail on a real error: ```bash producer | head -n 20 st=("${PIPESTATUS[@]}") case ${st[0]} in 0|141) : ;; # finished, or SIGPIPEd - both fine *) echo "producer failed: ${st[0]}" >&2; exit "${st[0]}" ;; esac ``` Or avoid the race entirely by not making the producer produce more than you need — `head -n 20 file` instead of `cat file | head -n 20`, or a bounded query rather than a full dump piped into a truncating reader. Removing the useless-`cat` stage is usually both faster and quieter. What you should *not* do is blanket-suppress the status of every pipeline, or disable pipefail globally because of one line. Both re-open the silent-failure hole that pipefail exists to close, in exchange for fixing one benign case. ## Interview framing The reason this question separates candidates is that it requires holding two ideas at once: bash's signal-to-status encoding, and the fact that `pipefail` is indiscriminate — it promotes a *correct* non-zero status just as eagerly as an incorrect one. Candidates who have only ever pasted `set -euo pipefail` into a header tend to read 141 as a defect in their own script rather than as the expected outcome of asking a reader to stop early.

  • Which stage of `producer | head -n 20` actually carries the 141?
    The producer, at `${PIPESTATUS[0]}`. `head` did its job and exits 0 — you can see the pair as `141 0` in the array. That distinction matters when you write the tolerance check: you want to accept 141 specifically from the upstream stage, not to blanket-accept 141 from anywhere in the pipeline.
  • Why does the same pipeline sometimes exit 1 or 2 instead of 141?
    Because not every program dies on SIGPIPE. A process that ignores or handles the signal gets an `EPIPE` error from its write instead, and then exits with whatever status it chooses — Python raises BrokenPipeError, some tools print a write error and exit 1 or 2. So a tolerance check keyed only on 141 can still be surprised by the program upstream.
  • What would you change so the pipeline stops producing 141 at all?
    Stop generating output nobody reads. `head -n 20 file` needs no producer at all; a query with a LIMIT beats dumping a whole table into a truncating reader; dropping a useless `cat` removes a stage. When the producer genuinely must stream, treat 141 as an accepted status rather than trying to engineer it away.

saying these in an interview costs you the question

  • Reads 141 as a generic unknown error rather than a signal
  • Says head itself failed with 141
  • Disables pipefail script-wide to silence one benign line
  • Claims the failure is random rather than a buffer-size race
  • Assumes every producer dies on SIGPIPE rather than handling EPIPE

context