php 61 lines · 8 steps

Streaming job progress with SSE in Laravel

An invokable controller pushes live job-progress updates to the browser using Server-Sent Events.

Explained by highlit
1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Repositories\JobProgressRepository;
6use Illuminate\Http\Request;
7use Symfony\Component\HttpFoundation\StreamedResponse;
8 
9class JobProgressStreamController extends Controller
10{
11 public function __construct(private JobProgressRepository $progress)
12 {
13 }
14 
15 public function __invoke(Request $request, string $jobId): StreamedResponse
16 {
17 $response = new StreamedResponse(function () use ($jobId) {
18 $lastStatus = null;
19 
20 while (! connection_aborted()) {
21 $snapshot = $this->progress->find($jobId);
22 
23 if ($snapshot && $snapshot->status !== $lastStatus) {
24 $lastStatus = $snapshot->status;
25 
26 $this->send('progress', [
27 'status' => $snapshot->status,
28 'percent' => $snapshot->percent,
29 'message' => $snapshot->message,
30 ]);
31 
32 if (in_array($snapshot->status, ['completed', 'failed'], true)) {
33 $this->send('done', ['status' => $snapshot->status]);
34 break;
35 }
36 }
37 
38 usleep(750_000);
39 }
40 });
41 
42 $response->headers->set('Content-Type', 'text/event-stream');
43 $response->headers->set('Cache-Control', 'no-cache');
44 $response->headers->set('Connection', 'keep-alive');
45 $response->headers->set('X-Accel-Buffering', 'no');
46 
47 return $response;
48 }
49 
50 private function send(string $event, array $data): void
51 {
52 echo "event: {$event}\n";
53 echo 'data: ' . json_encode($data) . "\n\n";
54 
55 if (ob_get_level() > 0) {
56 ob_flush();
57 }
58 
59 flush();
60 }
61}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Server-Sent Events let you push updates over one long-lived HTTP response without WebSockets.
  2. 2Checking connection_aborted() and diffing state keeps a streaming loop cheap and safe from runaway output.
  3. 3Disabling buffering at the framework, PHP, and proxy layers is what actually makes streamed chunks reach the client.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming job progress with SSE in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code