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:

flowchart TD
    subgraph Normal["Kondisi Normal"]
        direction LR
        A1["Proses A (Writer)<br>write('data')"] --> P1["Pipe Buffer"]
        P1 --> B1["Proses B (Reader)<br>read()"]
    end

    subgraph Broken["Broken Pipe"]
        direction LR
        A2["Proses A (Writer)<br>write('data')<br>(← SIGPIPE/EPIPE)"] --> P2["Pipe (closed)"]
        P2 -. "tidak ada reader" .-> B2["Proses B (Reader)<br>(sudah exit)"]
    end

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 #

package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
)

// Pendekatan 1: Ignore SIGPIPE (umum untuk CLI tools)
// Mencegah proses terminate ketika pipe ditutup oleh pembaca
func ignoreSigpipe() {
	signal.Ignore(syscall.SIGPIPE)
}

// Pendekatan 2: Handle broken pipe secara eksplisit
func safeWrite(data string) {
	_, err := fmt.Println(data)
	if err != nil {
		if errors.Is(err, syscall.EPIPE) {
			// Pembaca sudah menutup pipe — ini OK untuk CLI tools
			// Keluar dengan kode yang tepat
			os.Exit(0)
		}
		log.Fatalf("write error: %v", err)
	}
}

// Pendekatan 3: Untuk script dengan output yang besar
// Ini pola yang paling robust untuk CLI tools
func main() {
	for item := range generateLargeOutput() {
		fmt.Println(item)
	}
}
// Menangani client disconnect saat mengirim response
func sendResponse(conn net.Conn, data []byte) {
	_, err := conn.Write(data)
	if err != nil {
		if errors.Is(err, syscall.EPIPE) ||
			errors.Is(err, syscall.ECONNRESET) ||
			errors.Is(err, syscall.ECONNABORTED) {
			// Client menutup koneksi sebelum response selesai dikirim
			// Ini normal — log di level DEBUG, bukan ERROR
			log.Printf("DEBUG: client disconnected before response completed: %v", err)
			return
		}
		// Error socket yang tidak expected — log sebagai ERROR
		log.Printf("ERROR: socket error: %v", err)
	}
}

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 #

// Penanganan broken pipe ala Node.js di Go

// CLI tools — menulis ke stdout yang di-pipe
func writeToStdout(lines []string) {
	for _, line := range lines {
		_, err := fmt.Println(line)
		if err != nil {
			if errors.Is(err, syscall.EPIPE) {
				// Output di-pipe ke proses yang sudah selesai (misal: | head -5)
				// Exit dengan code 0 — ini adalah perilaku yang diharapkan
				os.Exit(0)
			}
			// Error lain — propagate
			log.Fatalf("write error: %v", err)
		}
	}
}

// HTTP server — client disconnect saat streaming response
func streamLargeData(w http.ResponseWriter, r *http.Request) {
	stream := generateLargeDataStream()

	// Client menutup koneksi — berhenti generate data
	ctx := r.Context()
	go func() {
		<-ctx.Done()
		stream.Close()
		log.Printf("DEBUG: client disconnected, stream destroyed")
	}()

	if err := stream.WriteTo(w); err != nil {
		if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
			// Client menutup koneksi — normal untuk streaming
			log.Printf("DEBUG: client disconnected: %v", err)
			return
		}
		// Error lain — propagate
		log.Printf("ERROR: stream error: %v", err)
	}
}

// HTTP server — handle write setelah client disconnect
func handleRequest(w http.ResponseWriter, r *http.Request) {
	// Go menampilkan broken pipe sebagai write error — tidak perlu cek socket dulu
	_, err := w.Write(generateResponse())
	if err != nil {
		if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
			log.Printf("DEBUG: client disconnected: %v", err)
		} else {
			log.Printf("ERROR: response error: %v", err)
		}
	}
}

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.
package main

import (
	"io"
	"log"
	"os"
	"strings"
)

// Memfilter broken pipe dari log error server aplikasi.
// Go tidak punya WSGI server seperti Gunicorn; ini ekuivalen
// logging filter: buang pesan yang menyebut "broken pipe".
type brokenPipeFilter struct {
	inner io.Writer
}

func (f brokenPipeFilter) Write(p []byte) (int, error) {
	if strings.Contains(strings.ToLower(string(p)), "broken pipe") {
		return len(p), nil // buang diam-diam
	}
	return f.inner.Write(p)
}

func main() {
	// Arahkan semua output log lewat filter
	log.SetOutput(brokenPipeFilter{inner: os.Stderr})
}

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.

package main

import (
	"log"
	"os"
	"os/signal"
	"sync/atomic"
	"syscall"
	"time"
)

type GracefulServer struct {
	shutdown       chan struct{}
	inShutdown     atomic.Bool
	activeRequests atomic.Int64
}

// Handle SIGTERM untuk graceful shutdown
func NewGracefulServer() *GracefulServer {
	s := &GracefulServer{shutdown: make(chan struct{})}

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
	go func() {
		<-ch
		s.startShutdown()
	}()

	return s
}

func (s *GracefulServer) startShutdown() {
	// Mulai graceful shutdown saat menerima SIGTERM
	log.Println("Received shutdown signal, starting graceful shutdown...")
	s.inShutdown.Store(true)
	close(s.shutdown)
}

// Handle satu request dengan tracking untuk graceful shutdown
func (s *GracefulServer) handleRequest(request Request) Response {
	if s.inShutdown.Load() {
		// Tolak request baru saat shutdown
		return Response{Status: 503, Body: "Service shutting down"}
	}
	s.activeRequests.Add(1)
	defer s.activeRequests.Add(-1)

	return processRequest(request)
}

// Tunggu semua request selesai sebelum exit.
// Mencegah broken pipe ke client yang sedang aktif.
func (s *GracefulServer) waitForShutdown(timeout time.Duration) {
	<-s.shutdown // tunggu shutdown signal

	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if s.activeRequests.Load() == 0 {
			break
		}
		log.Printf("Waiting for %d active requests...", s.activeRequests.Load())
		time.Sleep(500 * time.Millisecond)
	}

	if s.activeRequests.Load() > 0 {
		log.Printf("Shutdown timeout: %d requests still active", s.activeRequests.Load())
	}
	log.Println("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 #

package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
)

// ✗ Anti-pattern 1: Log broken pipe sebagai ERROR
// Mengakibatkan ribuan false positive di log dan alert yang tidak perlu
func antiPattern1(err error) {
	log.Printf("ERROR: Broken pipe error: %v", err) // JANGAN untuk client disconnect
}

// ✓ Solusi: log sebagai DEBUG untuk client disconnect yang expected
func solution1(err error) {
	log.Printf("DEBUG: Client disconnected (EPIPE) - normal behavior: %v", err)
}

// ✗ Anti-pattern 2: Tidak handle SIGPIPE, biarkan proses crash
// CLI tool tanpa signal handling crash saat output di-pipe ke | head
func generateReport() {
	for _, line := range hugeDataset {
		_, err := fmt.Println(line)
		if err != nil {
			log.Fatal(err) // crash dengan SIGPIPE jika | head -10 digunakan
		}
	}
}

// ✓ Solusi: handle broken pipe di CLI tools
func generateReportFixed() {
	for _, line := range hugeDataset {
		_, err := fmt.Println(line)
		if err != nil {
			if errors.Is(err, syscall.EPIPE) {
				os.Exit(0)
			}
			log.Fatal(err)
		}
	}
}

// ✗ Anti-pattern 3: Ignore SIGPIPE secara global tanpa pertimbangan
// signal.Ignore(syscall.SIGPIPE) 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
func ignoreSigpipe() {
	signal.Ignore(syscall.SIGPIPE)
	// 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 -5 atau | less menutup 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 terminationGracePeriodSeconds mengontrol 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.

← Sebelumnya: Micro Frontend   Berikutnya: Circuit Breaker →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact