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.
client disconnects → read end of php://stdout closed
log line written → fwrite() → SIGPIPE → FPM worker diesSafeStreamHandler — 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.
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.
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
- Channel config → Output targets
- Framework integration — per-runtime wiring
- API reference → Handler & formatter