Why your terminal pipeline appears to “freeze”
Every terminal user hits this eventually: you run tail -f file | grep thing1 | grep thing2, wait a while, and see nothing—even when matching lines exist. The pipeline isn’t broken. The problem is that intermediate programs hold their output in memory buffers instead of writing it to the pipe immediately.
This is usually a deliberate performance choice. Writing small amounts of data repeatedly means many system calls, which is slow. Instead, programs accumulate output until they have roughly 8KB ready, then write it all at once—or flush at exit. In a chain like the one above, grep thing1 may be waiting for 8KB of matches that never arrive because log lines are coming in slowly.
Why terminals get special treatment
The confusing part is that tail -f file | grep thing works fine, but adding the second grep breaks it. The reason: many programs check whether stdout is a TTY using isatty() and choose a buffering strategy based on the result.
- If stdout is a terminal: use line buffering—flush each line as soon as it’s available.
- If stdout is a pipe or file: use block buffering—save output until there’s about 8KB or the program exits.
For grep, buffering is handled by libc, whose buffer size comes from BUFSIZ. This isn’t a hard rule—a program could buffer when writing to a terminal—but it would be unusual, and almost nothing does it.
Which commands buffer
You generally have to remember which tools buffer output when writing to a pipe. Some commonly used commands that do not buffer include:
tailcattee
Many others buffer by default but provide a flag to disable block buffering:
grep— use--line-bufferedsed— use-uawk— callfflush()tcpdump— use-ljq— use-utr— use-ucut— cannot disable buffering
Commands like sort may or may not buffer, but it hardly matters since they can’t produce output until all input is consumed anyway. Note that behavior can vary between GNU and macOS versions of these tools.
Programming language runtimes behave similarly—their default print statements often buffer when stdout is not a TTY:
- C: disable with
setvbuf - Python: disable with
python -u,PYTHONUNBUFFERED=1,sys.stdout.reconfigure(line_buffering=False), orprint(x, flush=True) - Ruby: disable with
STDOUT.sync = true - Perl: disable with
$| = 1
The print style also matters: in C++, cout << "hello\n" buffers when piped, but cout << "hello" << endl flushes on every call.
Losing buffered data on Ctrl-C
Consider using tcpdump without -l to watch for DNS requests to example.com. When you press Ctrl-C, the signal kills every program in the pipeline, and whatever sits in tcpdump’s output buffer is lost—grep never gets a chance to process it. This is essentially unavoidable: grep receives SIGINT before tcpdump, so even a late flush wouldn’t help.
A workaround does exist: find tcpdump’s PID and run kill -TERM $PID. A termination signal allows the program to flush its buffer, and you’ll see the missed output.
Files buffer too, but recover differently
Redirecting to a regular file exhibits the same buffering behavior; grep thing1 /some/log/file > output.txt may stay empty for a while. The key difference is on termination: with a file redirect, the buffer contents usually get written before the program exits. This is less certain for pipes, but in practice, file redirection tends to behave closer to what you’d expect.
Five ways to work around buffering
Suppose you’re tailing a log file with tail -f /some/log/file | grep thing1 | grep thing2. Here are the practical fixes, roughly in order of how often people use them.
Run a short-lived command instead
The simplest way to dodge the issue: stop streaming and run something that exits quickly.
cat /some/log/file | grep thing1 | grep thing2
This produces the same results as the original pipeline—without making you think about buffering at all.
Pass the line-buffering flag
If you remember that grep has a flag for this, use it:
tail -f /some/log/file | grep --line-buffered thing1 | grep thing2
Rewrite the filter logic
Some users collapse multiple greps into a single awk invocation:
tail -f /some/log/file | awk '/thing1/ && /thing2/'
That rules out the message after passing grep. If your second filter is different or a regex pattern using grep -E, nothing prevents chaining them up to the last stage. Note that awk still buffers, so awk should be the last program in the pipeline to work reliably. You could also combine regexes inside a single grep invocation where that makes sense.
Preload a stdbuf wrapper
A tool like stdbuf uses LD_PRELOAD to alter libc’s buffering behavior globally for a given command:
tail -f /some/log/file | stdbuf -oL grep thing1 | grep thing2
This approach has limits: it won’t work with static binaries, has no effect on programs that don’t use libc’s standard I/O buffering, and isn’t reliable on macOS.
Fake a terminal with unbuffer
Another classic workaround is unbuffer (shipped in the expect package):
tail -f /some/log/file | unbuffer grep thing1 | grep thing2
This forces the program’s stdout to look like a TTY, so it adopts normal terminal behavior—including line buffering and color output. unbuffer is more reliable than stdbuf since no preloading is involved, but consider side effects: grep will color its matches even when its output isn’t your terminal.
How often this matters
Most pipelines transfer large amounts of data quickly and exit, so buffering rarely comes up. The problem surfaces when data trickles in slowly—classic examples include:
tcpdumpoutputtail -fstreams- watching logs via
kubectl logs - output of a slow computation
Among the solutions, unbuffer is the most dependable in practice because it eliminates the root cause—the program sees a TTY and behaves accordingly.
Would a standard env var help?
A well-known environment variable to disable buffering (similar to Python’s PYTHONUNBUFFERED) would be handy. There are precedents: NetBSD provides STDBUF, STDBUF1, and related variables that offer fine-grained control, though most developers won’t implement that many. Something simple like a NO_BUFFER convention, inspired by NO_COLOR, is appealing but hard to standardize. It also remains unclear whether flushing buffers on a timer—say every second—has worse side effects than the occasional stuck pipeline; no major program appears to do it.



