There's a moment in every web developer's career when a client says, "Can we make this update in real time?" — and you realize polling every five seconds isn't going to cut it anymore. That moment is when you need to make a clear-headed decision: WebSocket or Server-Sent Events (SSE)? Both solve real-time communication, but they do it differently, and picking the wrong one creates architectural headaches down the road.
This article breaks down both technologies with practical code examples, real trade-offs, and clear guidance on when to use each.
What Problem Are We Actually Solving?
Traditional HTTP is a request-response protocol. The browser asks, the server answers, the connection closes. For live dashboards, notifications, collaborative tools, or live feeds, this model falls apart. You need the server to push data to the client without waiting for a request.
Two mature solutions exist for this:
- WebSocket — a full-duplex, persistent TCP connection
- Server-Sent Events (SSE) — a unidirectional, HTTP-based stream from server to client
WebSocket: Full-Duplex Communication
WebSocket upgrades an HTTP connection into a persistent, bidirectional channel. Both the client and server can send messages at any time. This is what powers chat apps, multiplayer games, and collaborative editors.
How WebSocket Works
// Client-side WebSocket
const socket = new WebSocket('wss://yourapp.com/ws');
socket.addEventListener('open', () => {
console.log('Connected');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }));
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
});
socket.addEventListener('close', () => {
console.log('Disconnected — consider reconnect logic here');
});
On the Laravel side, using Laravel Reverb (the official first-party WebSocket server), you can broadcast events directly:
// app/Events/OrderStatusUpdated.php
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel('orders.' . $this->order->id)];
}
public function broadcastWith(): array
{
return [
'status' => $this->order->status,
'updated_at' => $this->order->updated_at->toISOString(),
];
}
}
// Dispatch from a controller or job
OrderStatusUpdated::dispatch($order);
This integrates cleanly with Laravel Echo on the frontend for channel subscriptions and authentication.
WebSocket Trade-offs
Pros:
- True bidirectional — client can also send data
- Low latency, minimal overhead after handshake
- Ideal for high-frequency updates
Cons:
- Requires a persistent connection server (Reverb, Pusher, Soketi)
- More complex infrastructure — doesn't work behind some proxies without configuration
- Stateful connections are harder to scale horizontally without sticky sessions or a pub/sub broker like Redis
Server-Sent Events: Simple, Underrated, Powerful
SSE is HTTP-based streaming. The server keeps the connection open and pushes text/event-stream formatted data. The client can't send messages back through this channel — it's one-way, server to client.
That sounds limiting, but for a huge class of problems — notifications, live feeds, progress bars, log streaming — it's exactly what you need, with far less infrastructure overhead.
SSE in Laravel
// routes/web.php
Route::get('/stream/notifications', function () {
return response()->stream(function () {
$i = 0;
while (true) {
$notifications = Auth::user()
->unreadNotifications()
->since(now()->subSeconds(5))
->get();
foreach ($notifications as $notification) {
echo "data: " . json_encode($notification->toArray()) . "\n\n";
ob_flush();
flush();
}
if (connection_aborted()) break;
sleep(3);
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no', // Important for Nginx
]);
})->middleware('auth');
// Client-side SSE
const eventSource = new EventSource('/stream/notifications');
eventSource.onmessage = (event) => {
const notification = JSON.parse(event.data);
displayNotification(notification);
};
eventSource.onerror = () => {
// Browser auto-reconnects by default
console.warn('SSE connection lost, retrying...');
};
Note the X-Accel-Buffering: no header — without it, Nginx will buffer your stream and your "real-time" events will arrive in batches. This is a common production gotcha.
You can also use named events for finer control:
echo "event: order_update\n";
echo "data: " . json_encode(['id' => 42, 'status' => 'shipped']) . "\n\n";
eventSource.addEventListener('order_update', (event) => {
const order = JSON.parse(event.data);
updateOrderUI(order);
});
SSE Trade-offs
Pros:
- Works over standard HTTP/1.1 and HTTP/2
- Auto-reconnect built into the browser spec
- No special server infrastructure needed
- Plays nicely with existing load balancers and proxies
- Simple to implement and debug
Cons:
- Unidirectional only — client communication needs separate AJAX/fetch calls
- Limited to ~6 concurrent connections per domain in HTTP/1.1 (non-issue with HTTP/2)
- Not ideal for very high-frequency updates (sub-100ms intervals)
The Decision Framework
Here's a practical decision tree:
Do you need the CLIENT to send data through the real-time channel?
├── YES → WebSocket
└── NO → Does the update frequency exceed ~10/second?
├── YES → WebSocket
└── NO → SSE (simpler, cheaper to run)
| Use Case | Recommended |
|---|---|
| Live chat / messaging | WebSocket |
| Multiplayer game state | WebSocket |
| Collaborative document editing | WebSocket |
| Order status updates | SSE |
| Notification feeds | SSE |
| Build/deployment log streaming | SSE |
| Live sports scores | SSE |
| Real-time analytics dashboard | Depends on frequency |
A Note on Livewire and the TALL Stack
If you're building with the TALL stack, Livewire 3 introduces wire:poll and the #[On] attribute with Laravel Echo, which abstracts much of this complexity. For many admin dashboards and notification panels, Livewire's built-in polling or event broadcasting handles real-time needs without writing WebSocket or SSE code manually.
That said, understanding the underlying protocols matters when you need to optimize performance or handle edge cases — something the team at HanzWeb encounters regularly when building data-intensive dashboards for clients across industries.
Production Considerations
For WebSocket:
- Use Laravel Reverb with Redis as the pub/sub backend for horizontal scaling
- Configure proper SSL termination at the load balancer level
- Implement heartbeat/ping-pong to detect dead connections
For SSE:
- Set
fastcgi_buffering offin your Nginx config (or useX-Accel-Buffering: no) - For long-running PHP processes, ensure
set_time_limit(0)and monitor memory usage - Consider using a queue worker to push events to a Redis channel, then have the SSE endpoint subscribe — this avoids database polling in the stream loop
Conclusion
WebSocket and SSE aren't competing technologies — they're complementary tools for different shapes of problems. SSE is dramatically underused; it covers a large portion of real-time use cases with far less operational complexity than a full WebSocket setup. If your server just needs to tell the client something happened, SSE is often the cleaner, more pragmatic choice.
Reach for WebSocket when you genuinely need bidirectionality or sub-second, high-frequency updates. Both are well-supported, production-proven, and worth having in your toolkit.
Top comments (0)