PHP collects echo output in buffers. Ubuntu 225 's FPM php.ini sets output_buffering = 4096, one 4 KB level that also lets you call header() after a little output, and ob_start() adds levels of your own for capturing a template into a string. A long report or CSV export wants the opposite: each part sent as soon as it is ready.
<?php
header('Content-Type: text/plain');
echo "output_buffering = ", ini_get('output_buffering'), ", levels = ", ob_get_level(), "\n";
while (ob_get_level() > 0) {
ob_end_flush(); // stop php.ini's buffer from holding output
}
for ($row = 1; $row <= 3; $row++) {
echo "row $row ready at ", date('H:i:s'), "\n";
flush(); // hand the bytes to FPM and Apache now
sleep(1); // stands in for a slow query per row
}At first all four lines reached curl 3,008 together after three seconds: mod_proxy_fcgi flushes only when its buffer fills. Adding ProxySet flushpackets=on inside a <Proxy "fcgi://localhost"> block in the virtual host (a worker setting honored only by mod_proxy_fcgi and mod_proxy_ajp) fixed it:
$ curl -sN http://localhost/stream.php | > while IFS= read -r l; do echo "$(date -u +%T) got: $l"; done 09:14:45 got: output_buffering = 4096, levels = 1 09:14:45 got: row 1 ready at 09:14:45 09:14:46 got: row 2 ready at 09:14:46 09:14:47 got: row 3 ready at 09:14:47
Every layer must let go: with Accept-Encoding: gzip, mod_deflate still held the first byte for 3.0 seconds, so exclude streaming URLs from compression.