
September 08, 2026
11 min read
By Kokil Thapa | Last reviewed: September 2026
Choosing between Server-Sent Events vs WebSockets vs Long Polling is one of the first architecture calls on any real-time feature. Dashboards, order trackers, chat widgets, and booking status pages all need the server to push updates. Yet each transport behaves differently under proxies, PHP-FPM, and mobile networks. This guide compares all three with production patterns from API development work on Laravel and Symfony stacks, so you pick the right tool instead of defaulting to WebSockets.
What Is the Difference Between Server-Sent Events, WebSockets, and Long Polling?
All three solve the same problem: the browser cannot wait forever for the user to refresh. They differ in protocol, direction, and infrastructure cost.
Long polling is the oldest pattern. The client sends an HTTP request. The server holds it until an event occurs or a timeout expires. Then the client immediately opens a new request. It works everywhere HTTP works, but each cycle carries full request headers.
Server-Sent Events (SSE) use a single long-lived HTTP connection. The server streams text/event-stream frames to the browser. The connection is unidirectional: server to client only. Browsers reconnect automatically with the Last-Event-ID header.
WebSockets start as an HTTP upgrade handshake. After upgrade, both sides send binary or text frames over one TCP socket. Full duplex. Ideal when the client must send frequent messages without opening new HTTP requests.
On a legal-tech portal I built, booking status needed one-way updates only. SSE cut complexity compared to running a WebSocket daemon. Chat on the same platform would have needed WebSockets or a third-party service instead.
| Criteria | Long Polling | Server-Sent Events | WebSockets |
|---|---|---|---|
| Direction | Effectively server → client | Server → client only | Full duplex |
| Protocol | Standard HTTP | HTTP (text/event-stream) | WS/WSS after HTTP upgrade |
| Browser API | fetch / XHR loop | EventSource | WebSocket |
| Auto reconnect | Manual | Built in | Manual |
| Proxy/CDN friendly | High | Moderate (needs no buffering) | Moderate (upgrade required) |
| PHP-FPM fit | Poor (ties workers) | Poor unless offloaded | Requires async server |
| Overhead per message | High (full HTTP each cycle) | Low (stream frames) | Lowest (frame header only) |
| Binary data | Via HTTP body | Text only (Base64 if needed) | Native binary frames |
How Does Long Polling Work in Practice?
Long polling predates HTML5 streaming APIs. You still see it in legacy integrations and environments where SSE headers get stripped by a proxy.
Client-side loop
The browser polls an endpoint. The server waits until data exists or a timeout fires. Then the client reopens immediately.
async function longPoll(since) {
while (true) {
const response = await fetch(`/api/events?since=${since}`, {
signal: AbortSignal.timeout(30000),
});
const payload = await response.json();
if (payload.events.length) {
payload.events.forEach(handleEvent);
since = payload.cursor;
}
}
} Server-side hold pattern
In Laravel 13 on PHP 8.5, a naive long-poll controller blocks a PHP-FPM worker for the entire hold window. That is acceptable at low concurrency. It fails fast under load.
public function poll(Request $request)
{
$since = $request->integer('since');
$deadline = now()->addSeconds(25);
while (now()->lt($deadline)) {
$events = Event::where('id', '>', $since)->limit(50)->get();
if ($events->isNotEmpty()) {
return response()->json([
'events' => $events,
'cursor' => $events->last()->id,
]);
}
usleep(500_000);
}
return response()->json(['events' => [], 'cursor' => $since]);
} This pattern appears in older admin dashboards. I've replaced it with Redis pub/sub plus SSE on several custom software projects where worker exhaustion caused 502 errors during peak hours.
- Works through any HTTP proxy without special config.
- No persistent connection state on the server.
- High latency between poll cycles if timeouts are long.
- Header overhead adds up with frequent small messages.
- PHP-FPM worker pool drains quickly under concurrent users.
How Do You Implement Server-Sent Events Correctly?
SSE is the sweet spot for one-way feeds: live order status, notification bells, stock tickers, and dashboard metrics. The MDN Server-Sent Events documentation covers the wire format. Production success depends on headers and a process model that does not block PHP workers.
Wire format and headers
Each event is plain text. Fields include event:, data:, id:, and optional retry:.
id: 1042
event: order.updated
data: {"order_id":881,"status":"shipped"}
The response must disable buffering so nginx or Cloudflare does not batch chunks.
return response()->stream(function () {
while (connection_aborted() === 0) {
$message = redis()->blpop('stream:orders', 5);
if ($message) {
echo "id: {$message['id']}\n";
echo "event: order.updated\n";
echo 'data: ' . json_encode($message['data']) . "\n\n";
ob_flush();
flush();
}
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
]); Browser consumption
const source = new EventSource('/sse/orders', { withCredentials: true });
source.addEventListener('order.updated', (event) => {
const payload = JSON.parse(event.data);
updateOrderRow(payload.order_id, payload.status);
});
source.onerror = () => {
console.warn('SSE reconnect will happen automatically');
}; For treks booking on Adventure Third Pole Trek, Livewire handled most UI updates. SSE would suit a read-only availability strip that refreshes every few seconds without re-rendering the full component tree.
Validate payloads with your JSON formatter during development. Malformed data: lines break client parsers silently.
When Should You Choose WebSockets Over SSE or Long Polling?
Pick WebSockets when the client sends messages often: chat, collaborative editing, multiplayer games, or live cursor sharing. The WebSocket protocol (RFC 6455) defines the upgrade handshake and frame format.
Laravel Reverb and broadcasting
Laravel 13 ships with first-class broadcasting. Laravel Reverb is a first-party WebSocket server built on PHP. Pair it with Redis 8.10 for horizontal scale across nodes.
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=my-app
REVERB_APP_KEY=local-key
REVERB_APP_SECRET=local-secret
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http php artisan reverb:start --host=0.0.0.0 --port=8080 Frontend clients subscribe through Laravel Echo:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT,
forceTLS: false,
});
Echo.private(`booking.${bookingId}`)
.listen('BookingStatusUpdated', (event) => {
refreshTimeline(event.status);
}); Read the companion guide on real-time Laravel with Reverb and WebSockets for nginx proxy_pass config. Also see building real-time features with WebSockets and Redis for multi-server setups.
WebSockets add operational surface area. You need a running daemon, firewall rules, and TLS termination for WSS. For many SMB sites in Nepal, that cost exceeds the benefit unless chat or live collaboration is core to the product.
How Do You Run Real-Time Transports on a Standard PHP Stack?
Most Laravel apps I maintain run Apache or nginx with PHP-FPM 8.3 or 8.4 on Ubuntu 24. That stack is request-response by design. Long-lived connections do not fit the default model.
Architecture options for PHP backends
- Offload to a Node or Go sidecar. Laravel publishes to Redis. A small Node 26 LTS service handles SSE or WebSocket fan-out.
- Use Laravel Octane with Swoole or FrankenPHP. Octane keeps the app in memory and supports concurrent streams without spawning a worker per connection.
- Run Laravel Reverb as a systemd service. Treat it like MySQL or Redis — always on, monitored, restarted on failure.
- Fall back to polling with short intervals. Acceptable for admin panels with five to ten concurrent users.
Server hardening matters once you expose new ports. Follow Ubuntu server hardening for web servers and restrict WebSocket ports through UFW firewall rules. Base PHP app setup is covered in the Ubuntu server setup for PHP apps in 2026 guide.
nginx SSE proxy snippet
location /sse/ {
proxy_pass http://octane_backend;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
proxy_read_timeout 86400s;
} nginx WebSocket proxy snippet
location /app/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s;
} On shared hosting without root access, SSE through a managed edge function often beats self-hosted WebSockets. Hosting choices directly limit which transports you can run in production.
What Are Common Production Mistakes With These Transports?
Transport choice is only half the work. The other half is connection limits, auth, and failure modes.
Authentication and authorisation
SSE passes cookies on same-origin requests. Cross-origin SSE needs CORS and careful credential flags. WebSocket auth typically uses a signed token in the query string during handshake. Never embed long-lived secrets in JavaScript.
For client portals like Mijar Law Associates, private channels must verify user identity before subscription. Laravel broadcasting channels handle this with Broadcast::channel() callbacks.
Rate limiting and abuse
Real-time endpoints attract abuse. Apply connection limits per IP and per user. See the guide on API rate limiting and abuse prevention for throttle patterns that work with streaming routes.
Reconnection and idempotency
SSE sends Last-Event-ID on reconnect. Your server must replay missed events or return a snapshot. WebSocket clients need explicit heartbeat and backoff logic. Long polling should pass a cursor token to avoid duplicate delivery.
Monitoring open connections
Track active SSE and WebSocket counts alongside PHP-FPM queue depth. Tools like Netdata help spot connection leaks before they take down the box. The Netdata zero-config monitoring guide covers baseline setup.
Alternative patterns exist. HTMX with Laravel pushes HTML fragments over standard requests — no persistent socket at all. For event-driven backends, EventBridge and SQS patterns decouple producers from delivery entirely.
Key Takeaways
- Use SSE for one-way server push — notifications, live status, metrics — when you can run an async worker or Octane.
- Choose WebSockets only when the client sends frequent messages or you need binary frames at low overhead.
- Reserve long polling for legacy proxies or trivial admin tools with very low concurrency.
- Never block PHP-FPM workers on long holds; offload streams to Octane, Reverb, or a Redis-backed sidecar.
- Disable proxy buffering for SSE, configure WebSocket upgrade headers in nginx, and monitor open connection counts.
- Start with the simplest transport that meets latency requirements — upgrade to WebSockets when metrics prove you need duplex traffic.
People Also Ask
Is Server-Sent Events better than WebSockets?
SSE is better for one-way feeds because it uses plain HTTP, auto-reconnects, and needs no separate daemon. WebSockets win when the client must send messages frequently or you need binary data at scale. Neither is universally superior — direction and ops cost decide.
Does long polling still make sense in 2026?
Yes, but only as a fallback. Restrictive corporate proxies and legacy APIs sometimes block streaming upgrades. For greenfield Laravel 13 apps with control over nginx, SSE or Reverb is almost always the better default.
Can PHP handle WebSockets natively?
Standard PHP-FPM cannot hold thousands of concurrent WebSocket connections efficiently. Laravel Reverb, Octane with Swoole, or an external Node service handle the persistent socket layer. Laravel still owns business logic and broadcasting.
How many concurrent SSE connections can one server handle?
It depends on the process model. PHP-FPM might manage dozens. An Octane or Node fan-out service on a modest VPS can handle thousands if you disable buffering and tune file descriptor limits. Load-test your exact nginx and kernel config before launch.
Pick the Right Transport and Ship With Confidence
The Server-Sent Events vs WebSockets vs Long Polling decision is an engineering trade-off, not a popularity contest. SSE covers most dashboard and notification cases with less ops burden. WebSockets earn their keep in chat and collaboration. Long polling remains a compatibility escape hatch.
If you are planning real-time features on a Laravel or Symfony app and want the transport choice baked into architecture from day one, enterprise application development and ongoing support and maintenance are where I help teams ship without surprise 502s. Browse the portfolio for live booking and portal work, or contact us to review your stack before you commit to a WebSocket daemon you may not need.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

