sarfraznawaz2005/laravel-sse
A lightweight Laravel package for Server-Sent Events (SSE) that lets you stream real-time updates from your app to the browser without WebSockets, using a simple API for sending events and keeping connections alive.
composer require sarfraznawaz2005/laravel-sse — 3.0.0 is Laravel 12 only (the sole change in this release is bumping illuminate/support to ^12.0). If you are still on Laravel 10/11, stay pinned to ^2.3; there is no code-level reason to upgrade beyond the framework constraint.
Discovery is automatic via the package's service provider — there is nothing to publish or register for the basic case. Look first at:
routes/web.php route you can hit with curl -N before wiring any frontend.vendor/sarfraznawaz2005/laravel-sse/src) — it is a thin, readable wrapper around response()->stream(), so if an option is missing you can extend it rather than fight it.First use case — a heartbeat/notification endpoint:
use Sarfraznawaz2005\SSE\Facades\SSE;
Route::get('/stream', function () {
return SSE::start()
->setRetry(3000)
->addEvent('ping', ['at' => now()->toDateTimeString()]);
});
Consume it from a plain Blade page with the native browser API; do not add a JS dependency until you actually need reconnection logic:
<script>
const es = new EventSource('/stream');
es.addEventListener('ping', e => console.log(JSON.parse(e.data)));
es.onerror = () => { /* EventSource auto-reconnects using the retry value */ };
</script>
Verify with curl -N -H 'Accept: text/event-stream' http://localhost:8000/stream — you should see framed event:/data: lines and no HTML.
Builder-per-tick inside a loop. The package is a formatter, not a scheduler. The idiomatic pattern is a while loop that builds and flushes one event per iteration:
Route::get('/orders/{order}/stream', function (Order $order) {
return SSE::start()->setRetry(5000)->stream(function () use ($order) {
while (true) {
if (connection_aborted()) break;
// read from cache/queue, emit one event
yield ['event' => 'status', 'data' => ['status' => $order->fresh()->status]];
sleep(2);
}
});
});
(Exact fluent method names differ slightly across versions — the README example is authoritative; the shape above is stable.)
Cross-request message passing. A streaming request blocks its PHP worker, so you cannot push from another request directly. Use a store as the channel: the writer does Cache::put("stream:{$userId}", $payload, 60) or Redis::publish(), and the streaming loop polls/blpops it. Prefer Redis BLPOP over sleep(1) polling — it cuts latency and DB load.
Route model + auth. SSE routes still run web middleware, so session auth works — but EventSource cannot send custom headers. Either use a short-lived signed URL (URL::temporarySignedRoute) and signed middleware, or accept a token as a query param and validate it manually. Never rely on Authorization: Bearer from a browser EventSource.
Extract the payload generator. Put "what should I send next" into a small service class and let the route do nothing but format and flush. This is the only part you can meaningfully unit test, and it keeps the streaming loop to ~10 lines.
Lifecycle hooks. Subscribe in the loop to your own Laravel events via a queue/cache-backed listener rather than firing domain events directly inside the stream — that keeps the streaming request read-only.
Frontend composables. For Inertia/Vue/React, wrap EventSource in one composable that owns reconnect, Last-Event-ID bookkeeping (use setId() server-side to make resume work), and cleanup in onUnmounted — leaked EventSource instances are the most common production bug with this pattern.
illuminate/support ^10|^11 will deadlock with 3.0.0. Run composer why illuminate/support before bumping.<?php, a dd(), or a middleware that echoes anything will break framing silently — the browser just fires onerror forever. If events "don't arrive", curl -N and diff the bytes before debugging anything else.X-Accel-Buffering: no (or proxy_buffering off); Apache: SetEnv no-gzip 1 and disable mod_deflate for the route; Cloudflare will buffer unless the response is text/event-stream and large enough — test through the real proxy, not just artisan serve.session()->save() immediately before entering the loop, or every other request from that user hangs until the stream closes.set_time_limit(0) + ignore_user_abort(true) server-side, an early connection_aborted() check client-side, a hard max duration (e.g. 5 min) so connections recycle, and a dedicated FPM pool for stream routes.->fresh() or use DB::connection()->disableQueryLog(), and break out of the loop after a bounded number of iterations.Content-Type: text/event-stream, Cache-Control: no-cache).retry per-event, binary), subclass the package class or bypass it with response()->stream() — the package adds little beyond header defaults, so there is no lock-in.How can I help you explore Laravel packages today?