Content Index
- Advantages and Limitations of SSE
- Advantages
- Limitations
- Use Cases and Efficient Implementation
- Output Mechanism: Buffers and Data Stream in PHP
- Backend Structure and Connection Control
- Receiving the Stream on the Client (JavaScript / Vue)
- 1. What is Server-Sent Events?
- Advantages of SSE
- Limitations
- SSE vs WebSockets
- 2. Architecture Flow
- 3. Backend — Laravel Controller
- Key Controller Points
- 4. Backend — Route
- 5. Frontend — Vue 3 Component
- Key Component Points
- 6. Router and Navigation
- 7. Tests (Pest PHP)
- 8. Verification with curl
- 9. Production Notes
- Worker blocking
- Rate limiting
- Authentication
- Scalability
- 10. Created or Modified Files
- Steps to Test
Server-Sent Events (SSE) are a technology based on the standard HTTP protocol that allows the server to send real-time updates to the client through an open connection. Unlike WebSockets, it does not require a specific protocol or an independent messaging server (such as Pusher or Reverb); it can be used directly on existing infrastructure.
In this architecture, the client is built with JavaScript (for example, using Vue's reactivity) and the backend is responsible for emitting the event stream (using frameworks like Laravel, Django, FastAPI, Express, or any Node.js environment).
Advantages and Limitations of SSE
To determine when to implement SSE instead of WebSockets, it is necessary to consider its main characteristics:
Advantages
- Standard protocol: Works directly over HTTP/HTTPS without special configurations on the server.
- Automatic reconnection: Modern browsers natively handle reconnection if the signal is interrupted.
- Development simplicity: Simple data formatting and processing (usually in JSON format).
Limitations
- Unidirectionality: Communication flows exclusively from the server to the client. If the client needs to send data, it must make a traditional HTTP request (POST, PUT, etc.).
- Connection limit: Under HTTP/1.1, browsers limit concurrent connections to a maximum of 6 per domain.
- Emission frequency: It is not designed for ultra-high frequency applications (such as real-time video games), being ideal for intervals of one second or higher.
Use Cases and Efficient Implementation
SSE is ideal for notification panels, progress bars for heavy background tasks, or live update systems. Given the limit of concurrent connections on shared servers or under HTTP/1.1, it is recommended to manage the opening and closing of the stream consciously:
- On-demand connection: Open the connection only when the user interacts with the module (for example, when opening the notification menu or a chat interface).
- Interval polling (Distributed polling): Activate the connection for brief periods from time to time (for example, every 5 or 10 minutes) to check for new events and close it immediately afterward.
Output Mechanism: Buffers and Data Stream in PHP
To stream data progressively without waiting for the entire script execution to complete, it is necessary to manipulate PHP's and the web server's buffering system using ob_flush() and flush().
The technical output transmission flow operates on two layers:
ob_flush()(Application Level): Flushes PHP's own interpreter output buffer.flush()(Server Level): Forces the transmission of data buffered by the web server to the client's browser.
Without calling these two functions sequentially, the server would hold all messages emitted by echo statements and deliver them in a single block when the loop ends, losing the real-time behavior.
Backend Structure and Connection Control
When implementing a streaming response (for example, using response()->stream() in Laravel), you must constantly check the connection status to release resources in case the client closes the tab or loses signal.
The following flow exemplifies the event emission structure:
- Disconnection check: The
connection_aborted()function is evaluated before each iteration. If the client has disconnected, loop execution is aborted. - SSE message format: The SSE specification requires the basic structure
event: name\ndata: { ... }\n\n. - Closure notification: Upon completing the heavy process or data loop, a custom event (for example,
event: close) is sent to notify the client that transmission has finished. - Required HTTP headers: The response must include the headers
Content-Type: text/event-stream,Cache-Control: no-cache, andConnection: keep-alive.
Receiving the Stream on the Client (JavaScript / Vue)
In the browser, no additional libraries are required, as the native EventSource API is used to consume the endpoint:
- Instantiation: An instance of
EventSource('/api/v1/events')is created pointing to the API route. - Listening to custom events: Listeners are registered using
addEventListener('message', ...)to process incoming information and append it to the interface's reactive data structures. - Closure and error management: The close event configured by the server or the error event (
onerror) is listened to in order to invoke the disconnect function (eventSource.close()) and reset the interface state.
1. What is Server-Sent Events?
Server-Sent Events (SSE) is a technology that allows the Laravel server to send real-time updates to the Vue application over a single open HTTP connection.
Advantages of SSE
- • Works over standard HTTP
- • Does not require extra server (Pusher, Reverb…)
- • Built-in automatic reconnection in the browser
- • Automatic parsing of data format
- • Compatible with all modern browsers
Limitations
- • Unidirectional (server → client only)
- • Maximum 6 connections per domain (HTTP/1.1)
- • Not ideal for high frequency (<1s between events)
- • PHP worker blocking during the stream
SSE vs WebSockets
| Feature | SSE | WebSockets |
|---|---|---|
| Direction | Unidirectional | Bidirectional |
| Protocol | HTTP/1.1 or HTTP/2 | ws:// or wss:// |
| Reconnection | Automatic (EventSource) | Manual (must implement) |
| Infrastructure | Standard HTTP | Pusher, Reverb, Socket.io… |
| Complexity | Low | Medium-High |
2. Architecture Flow
┌────────────┐ GET /api/v1/events ┌────────────────┐
│ │ ───────────────────────────────▶ │ │
│ │ │ Laravel │
│ Vue 3 │ HTTP 200 │ (PHP Worker) │
│ Browser │ ◀── Content-Type: text/event-... │ │
│ │ │ │
│ │ data: {"msg":"Notif #1"}\n\n │ sleep(2) × 10 │
│ │ data: {"msg":"Notif #2"}\n\n │ │
│ EventSource data: {"msg":"Notif #3"}\n\n │ → event: │
│ .onmessage ... │ closed │
│ │ event: closed\n\n │ │
│ │ ──── connection.close() ──────▶ │ flush() │
└────────────┘ └────────────────┘- The client (Vue) opens a standard HTTP connection to
/api/v1/events. - Laravel does not close the response: it sets the header
Content-Type: text/event-stream. - In a loop, it sends data formatted as
data: {JSON}\n\n. - The browser's
EventSourcereceives each event and executesonmessage. - Upon completion, the server sends an event
event: closedand closes the connection. - The client calls
EventSource.close()to clean up the socket.
3. Backend — Laravel Controller
File: app/Http/Controllers/Api/SSEController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class SSEController extends Controller
{
public function stream(): StreamedResponse
{
return response()->stream(function () {
// Disable PHP buffering to send data instantly
if (ob_get_level() > 0) {
ob_end_flush();
}
flush();
// Simulate sending 10 notifications
for ($i = 1; $i <= 10; $i++) {
if (connection_aborted()) {
break;
}
echo $this->formatEvent($i);
flush();
sleep(2);
}
// Notify client to close the connection cleanly
echo $this->formatClosedEvent();
flush();
}, 200, $this->sseHeaders());
}
/**
* Mandatory SSE format: data: {JSON}\n\n
*/
public function formatEvent(int $iteration): string
{
$data = json_encode([
'message' => "Notification #{$iteration}",
'time' => now()->toTimeString(),
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
return "data: {$data}\n\n";
}
/**
* Final event to close the connection without automatic reconnection.
*/
public function formatClosedEvent(): string
{
return "event: closed\ndata: {\"message\":\"Stream finished\"}\n\n";
}
/**
* SSE Headers: no-cache, keep-alive, Nginx buffering off.
*/
public function sseHeaders(): array
{
return [
'Cache-Control' => 'no-cache',
'Content-Type' => 'text/event-stream; charset=utf-8',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
];
}
}Key Controller Points
1
StreamedResponse instead of regular JSON. The callback runs line by line without closing the HTTP socket.2
echo reaches the browser immediately, rather than at the end of the script.3
4
5
4. Backend — Route
File: routes/api.php
<?php
use App\Http\Controllers\Api\SSEController;
// Server-Sent Events: unidirectional stream of real-time notifications
Route::get('/v1/events', [SSEController::class, 'stream']);Laravel automatically prefixes /api to all routes in routes/api.php, so the final URL is GET /api/v1/events. You can verify this with php artisan route:list --path=events.
Note on connections
The route does not use auth:sanctum middleware so that it can be easily tested with a browser. In production, add authentication to secure the endpoint. Browser's EventSource automatically sends session cookies if the route is on the same domain.
5. Frontend — Vue 3 Component
File: resources/js/vue/componets/SSEComponent.vue
<template>
<div>
<div>
<h1>
Live Notifications (SSE)
</h1>
<div>
<o-button
v-if="!isConnected"
iconLeft="play"
variant="success"
@click="connect"
>Connect</o-button>
<o-button
v-else
iconLeft="stop"
variant="danger"
@click="disconnect"
>Disconnect</o-button>
</div>
<p v-if="isConnected">
Connected to stream...
</p>
<ul v-if="messages.length > 0">
<li
v-for="(msg, index) in messages"
:key="index"
>
<strong>[{{ msg.time }}]:</strong> {{ msg.message }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
messages: [],
eventSource: null,
isConnected: false,
}
},
beforeUnmount() {
this.disconnect()
},
methods: {
connect() {
// EventSource manages automatic connection and reconnection
this.eventSource = new EventSource('/api/v1/events')
this.eventSource.addEventListener('open', () => {
this.isConnected = true
})
// Executes for each data: {JSON}\n\n
this.eventSource.addEventListener('message', (event) => {
this.messages.push(JSON.parse(event.data))
})
// Listen for custom server "closed" event
this.eventSource.addEventListener('closed', () => {
this.disconnect()
})
this.eventSource.addEventListener('error', () => {
if (this.eventSource.readyState === 2) {
this.isConnected = false
}
})
},
disconnect() {
if (this.eventSource) {
this.eventSource.close()
this.eventSource = null
this.isConnected = false
}
},
},
}
</script>Key Component Points
1
data: ... format.2
EventSource retries connection automatically. Upon receiving event: closed, it is manually closed beforehand.3
4
EventSource.CLOSED. We only update isConnected when the connection is truly closed (2), not while reconnecting (0).6. Router and Navigation
Route added in router.js
import SSE from "./componets/SSEComponent.vue";
// Added to the routes list:
{
name: 'sse',
path: '/vue/sse',
component: SSE
}Link in App.vue
<router-link
:to="{ name: 'sse' }"
>
SSE
</router-link>The link is available to all users (without v-if="$root.isLoggedIn" condition).
7. Tests (Pest PHP)
File: tests/Feature/SSEControllerTest.php
<?php
use App\Http\Controllers\Api\SSEController;
describe('SSEController', function () {
describe('stream', function () {
it('returns an SSE response with the correct headers', function () {
$response = $this->get('/api/v1/events');
$response->assertOk();
expect($response->headers->get('Cache-Control'))
->toContain('no-cache');
$response->assertHeader(
'Content-Type',
'text/event-stream; charset=utf-8'
);
$response->assertHeader('Connection', 'keep-alive');
$response->assertHeader('X-Accel-Buffering', 'no');
});
it('streams events in the SSE format', function () {
$controller = new SSEController;
$event = $controller->formatEvent(1);
expect($event)
->toMatch('/^data: \{"message":"Notification #1",/')
->toEndWith("\n\n");
});
it('encodes JSON without escaping accents', function () {
$controller = new SSEController;
$json = trim(substr(
$controller->formatEvent(2),
strlen('data: ')
));
expect(json_decode($json, true))
->toMatchArray([
'message' => 'Notification #2',
]);
});
it('formats the closing event with the closed name', function () {
$controller = new SSEController;
expect($controller->formatClosedEvent())
->toStartWith('event: closed')
->toContain('"message":"Stream finished"');
});
});
});Run tests:
php artisan test --compact --filter=SSEControllerTest
8. Verification with curl
To check the stream from terminal:
# View first events (~6 seconds)
curl -sN --max-time 6 http://larafirststep.test/api/v1/events
# Expected output:
data: {"message":"Notification #1","time":"13:18:42"}
data: {"message":"Notification #2","time":"13:18:44"}
# View full 10 events + closure (~24 seconds)
curl -sN --max-time 24 http://larafirststep.test/api/v1/events | tail -3
# Last lines:
event: closed
data: {"message":"Stream finished"}The -N flag
The -N flag (equivalent to --no-buffer) tells curl to output each line as it arrives, without waiting for full server buffering.
9. Production Notes
Worker blocking
During the stream, the PHP worker remains blocked (busy) for the entire loop (sleep(2) × 10 = 20s). In production with few workers (FPM), this can saturate the server.
Solutions:
- • Use Swoole or Octane (async server)
- • Publish events via Redis Pub/Sub with
Redis::subscribe() - • Use Queues to write to a file/Redis and stream reads it in real time
Rate limiting
SSE connections last much longer than regular requests. Add throttle or limit by IP/user to prevent abuse.
Authentication
EventSource automatically sends cookies if the route is on the same domain. For external APIs, use tokens in the URL or in a custom header (requires configuring withCredentials: true).
Scalability
If you need broadcasting to thousands of simultaneous clients, consider Laravel Reverb with WebSockets or services like Pusher. SSE is ideal for internal dashboards, admin notifications, or demos where concurrent volume is moderate.
10. Created or Modified Files
M resources/js/vue/App.vue ← "SSE" link in nav
M resources/js/vue/router.js ← route /vue/sse
M routes/api.php ← GET /api/v1/events
?? app/Http/Controllers/Api/SSEController.php ← SSE controller
?? resources/js/vue/componets/SSEComponent.vue ← Vue component
?? tests/Feature/SSEControllerTest.php ← Pest tests (4)Steps to Test
- Ensure Vite is running:
npm run dev - Open the app in your browser:
http://larafirststep.test/vue/sse - Click Connect
- Watch notifications arriving every 2 seconds
- Upon completion (
closedevent), the connection closes automatically