Package · logger

Output & broken pipe

A logger that can crash your web worker is worse than no logger. This page explains the broken-pipe failure mode, the one-line handler that neutralizes it, and why the right output target differs between FPM and Swoole.

The failure mode

Under PHP-FPM, php://stdout is not a log file — it’s the FastCGI response stream back to nginx or Apache. When the HTTP client disconnects before the response finishes, that stream’s read end is gone. The next fwrite() to it raises SIGPIPE, and PHP’s default disposition for SIGPIPE terminates the process. A stray log line, written after a client hangs up, can take down the FPM worker.

text
client disconnects  →  read end of php://stdout closed
log line written    →  fwrite() → SIGPIPE → FPM worker dies

SafeStreamHandler — never throw on write

The fix is a StreamHandler subclass that overrides exactly one method: it suppresses the warning and silently drops the line instead of letting the write blow up.

php
final class SafeStreamHandler extends StreamHandler
{
  protected function streamWrite(mixed $stream, LogRecord $record): void
  {
      if (!is_resource($stream)) {
          return;
      }
      @fwrite($stream, $record->formatted);
  }
}

The @ suppresses the E_WARNING from a failed write, and there’s no exception path — so a broken stream costs you one log line, not a worker. This handler backs both stdout and stderr outputs (via HandlerFactory).

A dropped line is the point

The trade-off is deliberate: when the write target is gone, the line is unrecoverable anyway. Losing it silently is strictly better than crashing the process that was trying to log it.

Why stderr for FPM

SafeStreamHandler keeps the worker alive, but the cleaner fix is to not write to the response stream at all. Under FPM, php://stderr is wired to FPM’s error_log, which is independent of the client connection. It can’t break when the client disconnects, so the SIGPIPE scenario never even arises.

text
FPM stdout  →  FastCGI response  →  dies when client disconnects   ✗
FPM stderr  →  FPM error_log     →  independent of the client       ✓

That’s why every FPM channel in the recommended config uses output: 'stderr'.

Why stdout is fine for Swoole

Swoole doesn’t route HTTP responses through PHP’s stdout — it manages responses through its own async I/O layer ($response->end(...)). So php://stdout in a Swoole worker is a normal stream to the container’s stdout, exactly what you want for Docker/Kubernetes log collection. No broken-pipe risk, so output: 'stdout' is the right choice there.

Output target summary

Runtime Recommended output Why
PHP-FPM stderr Response stream is fragile; error_log is safe.
Swoole stdout Responses are handled by Swoole, not stdout.
CLI stdout Plain terminal/stream; no HTTP client involved.
Docker/K8s stdout / stderr / syslog Collected by the platform’s log driver.

Regardless of choice, SafeStreamHandler is the safety net — even a misconfigured FPM channel on stdout won’t crash the worker.

See also