Broken Pipe #
Di suatu pagi, seorang engineer melihat ribuan baris error di log produksi: BrokenPipeError: [Errno 32] Broken pipe. Tidak ada yang crash, aplikasi masih berjalan, tapi error itu terasa mengkhawatirkan — ada yang tidak beres. Setelah investigasi, ternyata penyebabnya sederhana: sebuah script monitoring yang membaca output dari proses lain dengan | head -5 — mengambil 5 baris pertama lalu berhenti — menyebabkan error di proses pengirim karena pipe sudah ditutup oleh pembaca.
Broken pipe adalah salah satu error yang paling umum di Unix/Linux tapi sering disalahmengerti. Ia bukan selalu tanda bug — kadang adalah perilaku yang diharapkan. Memahaminya dengan benar membantu engineer membedakan mana yang perlu ditangani, mana yang bisa diabaikan, dan bagaimana mencegah error ini bocor ke log yang seharusnya bersih.
Apa Itu Pipe dan Kenapa Ia Bisa “Broken” #
Pipe adalah mekanisme komunikasi antar proses di Unix — cara menyambungkan output satu proses ke input proses lain. Ketika kamu menulis ls | grep txt, shell membuat pipe: output dari ls mengalir ke input grep.
Visualisasi pipe:
Proses A (Writer) Pipe Buffer Proses B (Reader)
┌─────────────────┐ ┌──────────┐ ┌─────────────────┐
│ write("data") │──────→ │ data ... │ ────────→ │ read() │
│ │ │ │ │ │
└─────────────────┘ └──────────┘ └─────────────────┘
Broken pipe terjadi ketika:
Proses A (Writer) Pipe Proses B (Reader)
┌─────────────────┐ ┌──────────┐ ┌─────────────────┐
│ write("data") │──────→ │ [closed] │ ✗ │ [sudah exit] │
│ ← SIGPIPE/EPIPE │ └──────────┘ └─────────────────┘
Writer mencoba menulis ke pipe yang tidak punya pembaca lagi
→ Kernel mengirim sinyal SIGPIPE ke writer
→ Atau write() return -1 dengan errno = EPIPE
Broken pipe bisa terjadi di tiga konteks utama:
Konteks 1: Shell pipe
$ long-running-command | head -5
head membaca 5 baris → menutup pipe → long-running-command dapat SIGPIPE
→ Normal dan diharapkan
Konteks 2: Network socket
Client HTTP mengirim request → server mulai kirim response
Client menutup koneksi sebelum response selesai
→ Server dapat "broken pipe" saat mencoba menulis ke socket
→ Juga normal — client boleh disconnect kapan saja
Konteks 3: File descriptor
Proses A menulis ke file descriptor
Proses B yang harusnya membaca sudah crash atau menutup fd-nya
→ Proses A mendapat EPIPE
SIGPIPE vs EPIPE: Dua Cara Sistem Memberi Tahu #
SIGPIPE (sinyal):
→ Dikirim ke proses oleh kernel ketika proses menulis ke pipe/socket
yang tidak punya pembaca
→ Default behavior: TERMINATE proses (proses langsung mati)
→ Bisa di-ignore: signal(SIGPIPE, SIG_IGN)
→ Bisa di-handle: signal(SIGPIPE, my_handler)
EPIPE (errno):
→ Error code yang dikembalikan oleh write()/send() call
→ Terjadi ketika SIGPIPE di-ignore dan write() gagal
→ Atau pada socket non-blocking
→ Harus di-handle secara eksplisit di kode
Urutan kejadian:
1. Proses mencoba write() ke pipe/socket yang broken
2. Kernel kirim SIGPIPE ke proses
3. Jika SIGPIPE tidak di-handle → proses terminate
4. Jika SIGPIPE di-ignore → write() return -1, errno = EPIPE
5. Aplikasi harus cek errno dan tangani error
Menangani di Berbagai Bahasa dan Konteks #
Python #
import signal
import sys
import errno
# Pendekatan 1: Ignore SIGPIPE (umum untuk CLI tools)
# Mencegah proses terminate ketika pipe ditutup oleh pembaca
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
# Pendekatan 2: Handle BrokenPipeError secara eksplisit
def safe_write(data: str):
try:
print(data)
sys.stdout.flush()
except BrokenPipeError:
# Pembaca sudah menutup pipe — ini OK untuk CLI tools
# Keluar dengan kode yang tepat
sys.exit(0)
except IOError as e:
if e.errno == errno.EPIPE:
sys.exit(0)
raise # re-raise error lain yang tidak expected
# Pendekatan 3: Untuk script yang pipe output-nya besar
# Ini adalah pola yang paling robust untuk Python CLI
def main():
try:
for item in generate_large_output():
print(item)
except BrokenPipeError:
# Hapus stderr untuk menghindari "BrokenPipeError" di terminal
# ketika user menggunakan | head atau | less
sys.stderr.close()
sys.exit(0)
# Mengapa stderr.close() sebelum exit:
# Jika kamu tidak close stderr, Python akan mencoba flush stderr
# saat exit dan bisa menghasilkan error tambahan yang membingungkan
# Di Django/Flask: biasanya tidak perlu handle karena framework menangani
# BrokenPipeError yang berasal dari client disconnect
# Penanganan di web server context — client disconnect
import socket
from contextlib import suppress
def send_response(client_socket, data: bytes):
"""
Kirim response ke client dengan handling untuk client disconnect.
"""
try:
client_socket.sendall(data)
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError) as e:
# Client menutup koneksi sebelum response selesai dikirim
# Ini normal — log di level DEBUG, bukan ERROR
logger.debug(
"Client disconnected before response completed",
extra={'error': str(e), 'client': client_socket.getpeername()}
)
except socket.error as e:
if e.errno == errno.EPIPE:
logger.debug("EPIPE: client disconnected")
else:
# Error socket yang tidak expected — log sebagai ERROR
logger.error(f"Socket error: {e}")
raise
Go #
package main
import (
"errors"
"io"
"net"
"syscall"
"log"
"os"
)
// Cek apakah error adalah broken pipe
func isBrokenPipe(err error) bool {
if err == nil {
return false
}
// Cek berbagai bentuk broken pipe error di Go
if errors.Is(err, syscall.EPIPE) {
return true
}
if errors.Is(err, io.ErrClosedPipe) {
return true
}
// Untuk network error
var netErr *net.OpError
if errors.As(err, &netErr) {
if errors.Is(netErr.Err, syscall.EPIPE) {
return true
}
}
return false
}
// Handler HTTP yang menangani client disconnect
func handleRequest(w http.ResponseWriter, r *http.Request) {
data := generateLargeResponse()
_, err := w.Write(data)
if err != nil {
if isBrokenPipe(err) {
// Client disconnect — log debug saja
log.Printf("DEBUG: client disconnected: %v", err)
return
}
// Error lain yang perlu diperhatikan
log.Printf("ERROR: write failed: %v", err)
}
}
// Menulis ke stdout dengan handling broken pipe
// (untuk CLI tools yang output-nya di-pipe)
func writeToStdout(lines []string) {
for _, line := range lines {
_, err := fmt.Println(line)
if err != nil {
if isBrokenPipe(err) {
// Pembaca menutup pipe — exit normal
os.Exit(0)
}
log.Fatalf("Write error: %v", err)
}
}
}
Node.js #
// Node.js — stdout broken pipe (umum di CLI tools)
process.stdout.on('error', (err) => {
if (err.code === 'EPIPE') {
// Output di-pipe ke proses yang sudah selesai (misal: | head -5)
// Exit dengan code 0 — ini adalah perilaku yang diharapkan
process.exit(0)
}
// Error lain — propagate
throw err
})
// Express.js — client disconnect saat streaming response
app.get('/large-data', (req, res) => {
const stream = generateLargeDataStream()
req.on('close', () => {
// Client menutup koneksi
stream.destroy()
logger.debug('Client disconnected, stream destroyed')
})
stream.on('error', (err) => {
logger.error('Stream error:', err)
})
stream.pipe(res)
})
// HTTP server — handle write setelah client disconnect
const server = http.createServer((req, res) => {
// Cek apakah koneksi masih aktif sebelum write
if (res.socket && res.socket.destroyed) {
return // Jangan coba write ke socket yang sudah destroyed
}
res.on('error', (err) => {
if (err.code === 'EPIPE' || err.code === 'ECONNRESET') {
logger.debug('Client disconnected:', err.message)
} else {
logger.error('Response error:', err)
}
})
res.end(generateResponse())
})
Broken Pipe di Web Server: Client Disconnect #
Broken pipe paling sering muncul di web server karena client disconnect. Ini sangat normal dan biasanya tidak menandakan masalah:
Skenario umum client disconnect yang menyebabkan broken pipe:
1. User menutup tab browser sebelum halaman selesai load
→ Server mencoba kirim response → client socket sudah ditutup → EPIPE
2. Mobile user kehilangan sinyal di tengah request
→ TCP connection terputus → server dapat ECONNRESET atau EPIPE
3. Load balancer timeout sebelum server selesai generate response
→ Load balancer menutup koneksi → server dapat EPIPE
4. Client yang tidak sabar (timeout di sisi client)
→ Client timeout → server dapat EPIPE
5. API consumer yang implementasinya salah
→ Client kirim request tapi langsung tutup koneksi
Semua skenario ini NORMAL dan tidak menandakan bug.
Yang penting: log di level DEBUG, bukan ERROR.
Jangan alert on-call engineer untuk broken pipe dari client disconnect.
# Konfigurasi logging yang tepat untuk Gunicorn (Python WSGI server)
# gunicorn.conf.py
errorlog = '-'
loglevel = 'info'
# Gunicorn sudah menangani broken pipe dari client disconnect
# dan tidak log sebagai error — tapi beberapa versi perlu konfigurasi
# Untuk menghapus broken pipe dari access log Gunicorn:
# Buat custom logger yang filter broken pipe
import logging
class BrokenPipeFilter(logging.Filter):
def filter(self, record):
# Filter out broken pipe warning dari gunicorn
return 'broken pipe' not in record.getMessage().lower()
# Di konfigurasi aplikasi:
gunicorn_error_logger = logging.getLogger('gunicorn.error')
gunicorn_error_logger.addFilter(BrokenPipeFilter())
Graceful Shutdown dan Broken Pipe #
Saat aplikasi menerima sinyal shutdown (SIGTERM), ia perlu menyelesaikan request yang sedang berjalan sebelum menutup. Jika ini tidak dilakukan dengan benar, client yang aktif akan mendapat broken pipe.
import signal
import threading
import time
class GracefulServer:
def __init__(self):
self._shutdown = threading.Event()
self._active_requests = 0
self._lock = threading.Lock()
# Handle SIGTERM untuk graceful shutdown
signal.signal(signal.SIGTERM, self._handle_sigterm)
signal.signal(signal.SIGINT, self._handle_sigterm)
def _handle_sigterm(self, signum, frame):
"""Mulai graceful shutdown saat menerima SIGTERM."""
logger.info("Received shutdown signal, starting graceful shutdown...")
self._shutdown.set()
def handle_request(self, request):
"""Handle satu request dengan tracking untuk graceful shutdown."""
with self._lock:
if self._shutdown.is_set():
# Tolak request baru saat shutdown
return {'status': 503, 'body': 'Service shutting down'}
self._active_requests += 1
try:
return process_request(request)
finally:
with self._lock:
self._active_requests -= 1
def wait_for_shutdown(self, timeout: int = 30):
"""
Tunggu semua request selesai sebelum exit.
Mencegah broken pipe ke client yang sedang aktif.
"""
self._shutdown.wait() # tunggu shutdown signal
deadline = time.time() + timeout
while time.time() < deadline:
with self._lock:
if self._active_requests == 0:
break
logger.info(f"Waiting for {self._active_requests} active requests...")
time.sleep(0.5)
if self._active_requests > 0:
logger.warning(
f"Shutdown timeout: {self._active_requests} requests still active"
)
logger.info("Server shutdown complete")
# Kubernetes deployment — graceful shutdown configuration
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
# Berikan waktu untuk graceful shutdown sebelum pod di-kill
terminationGracePeriodSeconds: 60 # default 30 detik
containers:
- name: app
lifecycle:
preStop:
exec:
# Tunggu 10 detik sebelum container mulai shutdown
# Memberi waktu untuk load balancer remove pod dari rotation
command: ["/bin/sleep", "10"]
Debugging Broken Pipe #
# Mendeteksi dan menginvestigasi broken pipe
# 1. Cek log untuk broken pipe
grep -i "broken pipe\|EPIPE\|SIGPIPE" /var/log/app/error.log
# 2. Cek apakah proses menerima SIGPIPE
strace -e signal -p <PID> 2>&1 | grep SIGPIPE
# 3. Monitor file descriptor suatu proses
lsof -p <PID> # lihat semua fd yang terbuka
# Periksa apakah ada pipe yang salah ujungnya sudah tidak ada
# 4. Cek network connection state
ss -tp # TCP connections dengan proses yang terkait
# Perhatikan state CLOSE_WAIT — bisa jadi sumber broken pipe
# 5. Monitor secara real-time dengan SystemTap atau eBPF
# (advanced - membutuhkan kernel support)
# bpftrace -e 'kprobe:pipe_write { ... }'
# 6. Reproduksi broken pipe secara terkontrol untuk testing
# Terminal 1: jalankan writer
python3 -c "
import time, sys
for i in range(100):
print(f'line {i}')
time.sleep(0.1)
" | head -3
# Terminal 2: lihat behavior
# head -3 membaca 3 baris lalu exit
# Python script mendapat SIGPIPE/BrokenPipeError
Perbedaan Broken Pipe dengan Error Serupa #
EPIPE vs ECONNRESET vs ECONNABORTED:
EPIPE (Broken pipe):
→ Terjadi saat WRITE ke pipe/socket yang closed
→ Pembaca sudah menutup ujung pipe
→ Sering di: server menulis response ke client yang sudah disconnect
ECONNRESET (Connection reset by peer):
→ Terjadi saat koneksi TCP di-reset oleh remote
→ Remote mengirim RST packet
→ Sering di: client crash, firewall yang memutus koneksi, NAT timeout
ECONNABORTED (Connection aborted):
→ Koneksi dibatalkan oleh local stack
→ Sering terjadi saat accept() dipanggil pada koneksi yang sudah di-RST
ETIMEDOUT (Connection timed out):
→ Tidak ada response dari remote dalam waktu yang ditentukan
→ Bisa menunjukkan masalah jaringan atau server yang lambat
Semua ini bisa muncul bersamaan:
→ Client disconnect → ECONNRESET dari perspektif TCP
→ Server mencoba write → EPIPE/SIGPIPE
→ Keduanya dari event yang sama tapi terlihat berbeda di kode
Anti-Pattern yang Harus Dihindari #
# ✗ Anti-pattern 1: Log broken pipe sebagai ERROR
# Mengakibatkan ribuan false positive di log dan alert yang tidak perlu
logger.error("Broken pipe error!") # JANGAN untuk client disconnect
# ✓ Solusi: log sebagai DEBUG untuk client disconnect yang expected
logger.debug("Client disconnected (EPIPE) - normal behavior")
# ✗ Anti-pattern 2: Tidak handle SIGPIPE, biarkan proses crash
# CLI tool tanpa signal handling crash saat output di-pipe ke | head
def generate_report():
for line in huge_dataset:
print(line) # crash dengan SIGPIPE jika | head -10 digunakan
# ✓ Solusi: handle BrokenPipeError di CLI tools
try:
for line in huge_dataset:
print(line)
except BrokenPipeError:
sys.stderr.close()
sys.exit(0)
# ✗ Anti-pattern 3: Ignore SIGPIPE secara global tanpa pertimbangan
# signal(SIGPIPE, SIG_IGN) di semua kode
# Bisa menyebabkan proses terus berjalan padahal tidak ada yang membaca
# dan loop menulis tanpa henti ke /dev/null
# ✓ Solusi: handle EPIPE setelah SIG_IGN secara eksplisit
signal.signal(signal.SIGPIPE, signal.SIG_IGN)
# Sekarang HARUS cek return value write() dan errno
# ✗ Anti-pattern 4: Tidak ada graceful shutdown
# Pod di-kill langsung → semua request yang aktif dapat broken pipe
# ✓ Solusi: terminationGracePeriodSeconds + preStop hook di Kubernetes
# ✗ Anti-pattern 5: Alert on-call untuk setiap broken pipe
# Broken pipe dari client disconnect bisa ribuan per hari di aplikasi yang sehat
# ✓ Solusi: bedakan antara "broken pipe dari client disconnect" (normal)
# dengan "broken pipe karena bug internal" (perlu alert)
Checklist Penanganan Broken Pipe #
CLI TOOLS:
□ Handle BrokenPipeError untuk tool yang output-nya bisa di-pipe
□ Exit dengan kode 0 saat broken pipe (bukan error)
□ Tutup stderr sebelum exit untuk menghindari secondary error
WEB SERVER:
□ Client disconnect (EPIPE/ECONNRESET) di-log sebagai DEBUG, bukan ERROR
□ Tidak ada alert on-call untuk broken pipe dari client disconnect
□ Response streaming di-stop saat client disconnect (jangan terus generate)
GRACEFUL SHUTDOWN:
□ SIGTERM handler ada dan berfungsi
□ Active request selesai sebelum proses exit
□ Kubernetes terminationGracePeriodSeconds dikonfigurasi sesuai kebutuhan
□ preStop hook memberikan waktu untuk load balancer remove pod
NETWORK PROGRAMMING:
□ Return value dari write()/send() selalu di-cek
□ EPIPE ditangani berbeda dari error yang lain (tidak re-raise, tidak log error)
□ Socket error handler ada untuk semua koneksi yang di-maintain
DEBUGGING:
□ Log mengandung cukup konteks untuk membedakan broken pipe yang normal vs bug
□ Monitoring membedakan "client disconnect" dari "internal error"
□ Tidak ada noise di error log dari broken pipe yang expected
Ringkasan #
- Broken pipe terjadi ketika writer mencoba menulis ke pipe atau socket yang sudah ditutup oleh reader — kernel mengirim SIGPIPE ke writer, atau write() return EPIPE jika SIGPIPE di-ignore.
- Broken pipe di web server dari client disconnect adalah NORMAL — user menutup tab, mobile kehilangan sinyal, load balancer timeout — semua ini menyebabkan broken pipe yang tidak menandakan bug.
- Log broken pipe dari client disconnect sebagai DEBUG, bukan ERROR — ribuan broken pipe per hari di aplikasi yang sehat adalah hal biasa. Log sebagai ERROR hanya menciptakan noise yang membingungkan.
- CLI tools yang output-nya di-pipe harus handle BrokenPipeError — ketika
| head -5atau| lessmenutup pipe, tool harus exit dengan code 0, bukan crash atau print error.- SIGPIPE default behavior adalah terminate proses — pastikan CLI tools dan server daemon yang perlu bertahan dari broken pipe me-handle atau me-ignore SIGPIPE secara eksplisit.
- Graceful shutdown mencegah broken pipe ke active clients — saat SIGTERM diterima, tunggu semua request selesai sebelum menutup koneksi. Kubernetes
terminationGracePeriodSecondsmengontrol berapa lama waktu tunggu.- Bedakan EPIPE, ECONNRESET, dan ECONNABORTED — semuanya tanda koneksi bermasalah tapi dari perspektif yang berbeda. Ketiganya biasanya dari event yang sama (client disconnect).
- Jangan alert on-call untuk setiap broken pipe — buat alerting yang membedakan antara broken pipe yang expected (client disconnect) dengan yang unexpected (bug internal).
- Setelah ignore SIGPIPE, wajib cek errno setelah write() — jika SIGPIPE di-ignore, write() yang gagal akan return -1 dengan errno EPIPE. Kode yang tidak mengecek ini akan terus berjalan seolah write berhasil.
- Streaming response harus di-stop saat client disconnect — jangan terus generate data jika tidak ada yang membaca. Ini buang CPU dan bisa menumpuk data di buffer yang tidak pernah terkirim.