Streaming Responses#
Stream data to clients without buffering whole payloads in memory. Great for logs, progress updates, server-sent events, long CSVs, or large file transformations. Webrick’s Response::stream() hands you a generator interface that yields chunks efficiently.
Basics: yield chunks#
use Infocyph\Webrick\Response\Response;
Route::get('/stream', function () {
return Response::stream(function () {
for ($i = 1; $i <= 5; $i++) {
yield "chunk: {$i}\n";
usleep(150_000); // simulate work
}
return ''; // optional trailer
});
});
Test it
curl -N http://127.0.0.1:8000/stream
The
-Nflag tellscurlnot to buffer.
Binary/data streams#
Write bytes directly—useful for NDJSON, CSV, or custom binary protocols.
Route::get('/numbers.ndjson', function () {
return Response::stream(function () {
for ($i = 1; $i <= 3; $i++) {
yield json_encode(['n' => $i]) . "\n";
}
})->withHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
});
Server-Sent Events (SSE)#
Keep a long-lived HTTP connection and push events.
Route::get('/events', function () {
return Response::stream(function () {
for ($i = 1; $i <= 5; $i++) {
yield "event: tick\n";
yield "data: " . json_encode(['i'=>$i, 't'=>time()]) . "\n\n";
usleep(1000_000);
}
})->withHeader('Content-Type', 'text/event-stream')
->withHeader('Cache-Control', 'no-cache')
->withHeader('Connection', 'keep-alive');
});
Client (browser):
const es = new EventSource('/events');
es.addEventListener('tick', e => console.log('tick', JSON.parse(e.data)));
Streaming large files (transforming on the fly)#
When you need to transform or throttle a big file:
Route::get('/download/report', function () {
$path = __DIR__ . '/../storage/big-report.csv';
return Response::stream(function () use ($path) {
$h = fopen($path, 'rb');
if (!$h) {
yield "error: cannot open file\n";
return '';
}
while (!feof($h)) {
yield fread($h, 64 * 1024); // 64KB chunks
}
fclose($h);
})->withHeader('Content-Type', 'text/csv; charset=UTF-8')
->withHeader('Content-Disposition', 'attachment; filename="report.csv"');
});
If you’re not transforming,
Response::attachment($path, 'name.csv')already streams efficiently—prefer that helper.
Compression & streaming#
Compression middleware may skip or adjust behavior for streaming responses (many compressors need full buffers).
For SSE or already-compressed content, compression is often disabled by design.
If you need compression for predictable chunking, consider pre-compressing artifacts and serving them via
attachment()with the correctContent-Encoding.
Caching & validators#
Streaming responses typically don’t set
ETagautomatically (content length unknown upfront).If your stream is deterministic and cacheable, consider computing a known hash/file size and using
Response::attachment()or a precomputed body.
Web server considerations (important)#
To avoid unwanted buffering:
Nginx: disable proxy buffering for your upstream location (if proxying), e.g.
proxy_buffering off;Apache: avoid output filters that buffer; use
SetEnv no-gzip 1for SSE if needed.CDNs / proxies: some buffer small responses—test with and without CDN.
Timeouts: extend upstream timeouts for long streams (both server and client).
Backpressure & pace#
Yield in bounded chunks (e.g., 8–64KB) and sleep only as needed. For I/O-bound producers (DB, network), chunk sizes of 16–32KB are usually a good balance.
Error handling & early termination#
Wrap your generator body with try/finally if you manage resources:
return Response::stream(function () {
$h = fopen('php://temp', 'wb+');
try {
// ... yield chunks ...
} finally {
if ($h) fclose($h);
}
});
If an exception is thrown mid-stream, the connection will close; ensure upstream logs capture the error.
Progress patterns#
Emit structured progress for clients to parse:
yield json_encode(['stage'=>'prepare']) . "\n";
yield json_encode(['stage'=>'processing','pct'=>42]) . "\n";
yield json_encode(['stage'=>'done']) . "\n";
For browsers, use SSE; for CLIs, newline-delimited JSON.
Checklist#
Use
Response::stream()for long or dynamic outputsPick a sensible chunk size (8–64KB)
Disable proxy/web-server buffering for real-time streams
Don’t rely on compression for SSE/real-time unless pre-compressed
Prefer
attachment()when serving static large filesManage resources with try/finally in generators