In Livewire 4, how do $this->stream() and wire:stream send partial output to the browser during one action, and what are the production caveats?
answer
- one request, many flushed chunks
- wire:stream="name" is the target
- append by default, replace: true
- v4: content is the first argument
- chunks are inserted as raw HTML
basics
~20 s$this->stream() flushes a chunk of content to the browser while the action is still running; Livewire inserts it into the element marked wire:stream with the same name, appending by default, and renders normally when the action finishes.
solid answer
~40 sInside an action, `$this->stream('chunk', name: 'summary')` (or the older named form `to: 'summary'`) starts a streamed response and flushes that chunk immediately. The browser finds the element with `wire:stream="summary"` in the component and **appends** the chunk, or replaces its contents with `replace: true` or a `wire:stream.replace` target. When the action returns, Livewire finishes the normal lifecycle and sends the final render. In Livewire 4 the signature is `stream($content, $replace, $name, $el, $ref)`, so v3-style positional calls with the target first break. Production caveats: the PHP worker is held for the whole action; output buffering and proxy buffering must not hold chunks back; the docs state it is not supported under Octane; and chunks are inserted as **HTML**, so untrusted text must be escaped with `e()` first.
code
php · 23 lines<?php
use Livewire\Component;
new class extends Component {
public string $summary = '';
public function summarise(): void
{
$this->summary = '';
foreach ($this->regionLines() as $line) {
$safe = e($line).'<br>';
$this->stream($safe, name: 'summary');
$this->summary .= $safe;
}
}
protected function regionLines(): iterable
{
return []; // app-specific, slow per region
}
};go deeper
Recall that stream() pushes content into a wire:stream target while an action is still running.
Explain append versus replace, the name, el and ref targets, and that the final render still happens.
Escape streamed text, account for held workers and proxy buffering, and know the Octane limitation.
Decide when streaming inside a request is acceptable versus moving long work to queues with progress reporting.
## What streaming is for Most Livewire actions are request-response: the browser waits, the server renders once, the page updates. Some actions produce output gradually, such as a generated sales summary arriving token by token or a report that processes regions one after another. **Streaming** lets the component show that output as it is produced, while **one** request is still in flight. ## How it works 1. The template marks a target: `<p wire:stream="summary">{{ $summary }}</p>`. 2. In an action, each call to `$this->stream(...)` makes Livewire start a streamed HTTP response (if it has not already), write a small JSON message with the chunk, and **flush** it to the client. 3. The browser reads each message, finds the target element inside the component and inserts the content. 4. When the action returns, Livewire continues its normal lifecycle: render, dehydrate, and a final message with the full HTML and new snapshot. So the streamed chunks are a preview; the final render is the source of truth. Assign the finished text to a property during the action so the final render shows it too. ## Targeting and modes | Call | Target | |---|---| | `stream('chunk', name: 'summary')` | the element with `wire:stream="summary"` | | `stream('chunk', to: 'summary')` | the same; `to:` is kept as an alias of `name:` | | `stream('chunk', el: '#summary')` | an element matched by a CSS selector in the component | | `stream('chunk', ref: 'summary')` | the element marked with the matching `wire:ref` | By default content is **appended**. `replace: true`, or marking the target `wire:stream.replace="summary"`, replaces its contents instead, which suits counters and progress text. ## The Livewire 4 signature change In v3 the target came first: `stream($to, $content, $replace)`. In Livewire 4 the method is `stream($content = null, $replace = false, $name = null, $el = null, $ref = null)`, with `to:` still accepted as a named alias for the target name. A v3 positional call such as `$this->stream('summary', $chunk)` now treats `'summary'` as the content and `$chunk` as the replace flag, and because no target is given nothing is sent to the browser at all, so upgrades must switch to named arguments. The fluent form `$this->stream($chunk)->to('summary')` is equivalent to passing `name:`. ## Production caveats - **Raw HTML.** The browser inserts streamed content with HTML insertion, not as text. Text from users or an external generator must be escaped with Laravel's `e()` before streaming, or it becomes a cross-site scripting hole. - **A held worker.** The PHP process serving the request stays busy for the whole action. Long streams tie up PHP-FPM workers and count against request time limits. - **Buffering.** Livewire flushes PHP's output buffers and sends `X-Accel-Buffering: no` with a `text/event-stream` content type, but a proxy or compression layer that ignores those signals can still deliver everything at the end. - **Octane.** The Livewire docs state `wire:stream` is not supported under Laravel Octane. - **Not a job queue.** Work that takes minutes belongs in a queued job with progress reported another way; streaming suits seconds, not minutes. ## When interviewers ask about it The strong answer covers the mechanism (one request, flushed chunks, final render), the target and mode options, the v4 argument order, and at least the escaping and worker-occupancy caveats, because those are the ones that bite in production. ## Streaming versus the alternatives | Approach | Good for | Cost | |---|---|---| | `wire:stream` | output produced over a few seconds | one busy PHP worker per stream | | `wire:poll` against a job's progress | long work in a queued job | periodic requests while it runs | | broadcast events | many listeners, server-initiated updates | a real-time connection to run | For a sales-summary generator that takes five seconds, streaming gives the best experience for the least machinery. For a year-end report that takes three minutes, a queued job plus a polled progress indicator keeps web workers free.
- Why must streamed chunks from an external text generator be escaped?Livewire inserts streamed content as HTML into the target element. Text containing markup or script from users or a generator would be interpreted by the browser. Escaping each chunk with `e()` turns it into inert text.
- Streaming works locally but production shows everything at the end. What would you check?Whether something between PHP and the browser buffers the response: a reverse proxy, a compression layer or a hosting platform that collects the full body. Livewire flushes PHP's buffers and sends `X-Accel-Buffering: no`, but it cannot force every later layer to pass chunks through.
saying these in an interview costs you the question
- Believes each stream() call sends a separate HTTP request.
- Assumes streamed chunks are inserted as escaped text automatically.
- Keeps the v3 positional order stream('target', $content) in Livewire 4 code.
- Thinks the final render is skipped after streaming.
- Uses wire:stream for multi-minute work instead of a queued job.