DEV Community

Marcc Atayde
Marcc Atayde

Posted on

WebSocket vs Server-Sent Events: Choosing the Right Real-Time Pattern for Your App

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');
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
// 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...');
};
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
eventSource.addEventListener('order_update', (event) => {
  const order = JSON.parse(event.data);
  updateOrderUI(order);
});
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
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 off in your Nginx config (or use X-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)