Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Server-Sent Events vs WebSockets vs Long Polling

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.

Real-Time Transport OverviewLong PollingRepeated HTTPrequest cyclesSSEOne HTTP streamserver to clientWebSocketTCP upgradebidirectionalBrowser ClientReceives live updates without full page reload
Server-Sent Events vs WebSockets vs Long Polling — three ways browsers receive server-pushed data

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.

CriteriaLong PollingServer-Sent EventsWebSockets
DirectionEffectively server → clientServer → client onlyFull duplex
ProtocolStandard HTTPHTTP (text/event-stream)WS/WSS after HTTP upgrade
Browser APIfetch / XHR loopEventSourceWebSocket
Auto reconnectManualBuilt inManual
Proxy/CDN friendlyHighModerate (needs no buffering)Moderate (upgrade required)
PHP-FPM fitPoor (ties workers)Poor unless offloadedRequires async server
Overhead per messageHigh (full HTTP each cycle)Low (stream frames)Lowest (frame header only)
Binary dataVia HTTP bodyText 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');
};
SSE Stream LifecycleBrowserEventSourceNginxno bufferingSSE WorkerLaravel OctaneRedispub/subPersistent text/event-streamAuto reconnect with Last-Event-IDIdle comment frames keep proxy alive
Server-Sent Events lifecycle — one HTTP stream from browser through reverse proxy to an async worker and Redis

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.

WebSocket Duplex FlowBrowserEcho clientReverbWS serverLaravelbroadcastsubscribepush eventRedisClient sends and receives on one socketLowest per-message overhead at high frequencyNeeds dedicated process and proxy upgrade rules
WebSocket full-duplex flow — browser and Laravel Reverb exchange frames over a single upgraded connection

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

  1. Offload to a Node or Go sidecar. Laravel publishes to Redis. A small Node 26 LTS service handles SSE or WebSocket fan-out.
  2. Use Laravel Octane with Swoole or FrankenPHP. Octane keeps the app in memory and supports concurrent streams without spawning a worker per connection.
  3. Run Laravel Reverb as a systemd service. Treat it like MySQL or Redis — always on, monitored, restarted on failure.
  4. 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.

Transport Decision TreeNeed real-time updates?Client sends often?chat, gamesOne-way feed?status, alertsLegacy proxy?no SSE supportWebSocketSSELong PollDefault to SSE for one-way; avoid long poll unless forced
Decision tree for Server-Sent Events vs WebSockets vs Long Polling based on traffic direction and infrastructure

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

Long polling holds a standard HTTP request open until data arrives or a timeout expires, then immediately reopens. SSE keeps one long-lived HTTP connection streaming text/event-stream frames server to client only, with built-in auto-reconnect via Last-Event-ID. WebSockets upgrade HTTP to a full-duplex TCP socket where both sides send text or binary frames. Overhead drops from full HTTP headers per cycle with polling, to stream frames with SSE, to minimal frame headers with WebSockets.

SSE is better for one-way server push: plain HTTP, auto-reconnect, no separate daemon. WebSockets win when the client sends messages frequently or you need binary data at scale.

Yes, but only as a fallback when proxies block streaming upgrades or for legacy APIs. For greenfield Laravel 13 apps with nginx control, SSE or Reverb is the better default.

Standard PHP-FPM cannot hold thousands of concurrent WebSocket connections efficiently. Use Laravel Reverb, Octane with Swoole, or an external Node sidecar while Laravel handles business logic.

It depends on the process model. PHP-FPM might manage dozens before worker exhaustion. An Octane or Node fan-out service on a modest VPS can handle thousands if you disable proxy buffering and tune file descriptor limits. Load-test your exact nginx and kernel configuration before launch rather than trusting generic benchmarks.

Pick WebSockets when the client sends messages often: chat, collaborative editing, multiplayer games, or live cursor sharing. On a legal-tech portal I built, booking status needed one-way updates only, so SSE cut complexity. Chat on the same platform would have needed WebSockets or a third-party service. Full-duplex also wins when binary frames at low overhead matter. For many SMB sites, the daemon, firewall rules, and WSS termination cost exceeds the benefit unless live collaboration is core.

Return text/event-stream with Cache-Control no-cache, Connection keep-alive, and X-Accel-Buffering no so nginx and Cloudflare do not batch chunks. Stream id, event, and data fields as plain text, flushing after each frame. Pair the stream with Redis blpop so PHP-FPM workers are not blocked in a busy loop. On the client, use EventSource with addEventListener per event type. Validate JSON payloads during development because malformed data lines break parsers silently.

A naive long-poll controller blocks one PHP-FPM worker for the entire hold window, often 25 seconds. At low concurrency that is acceptable; under peak load the worker pool drains and users see 502 errors. Each poll cycle also carries full HTTP request headers, adding overhead with frequent small messages. I have replaced this pattern with Redis pub/sub plus SSE on several projects where admin dashboards failed during peak hours. Reserve long polling for trivial admin tools with five to ten concurrent users.

Disable proxy buffering and caching on the SSE location, set proxy_http_version 1.1, clear the Connection header, turn off chunked_transfer_encoding, and set proxy_read_timeout to 86400s for long-lived streams. Without X-Accel-Buffering no on the PHP response and matching nginx settings, events arrive in batches instead of real time. Point proxy_pass at an Octane backend rather than PHP-FPM when possible. Test end to end through your CDN if one sits in front.

WebSocket auth typically uses a signed token in the query string during the handshake. Never embed long-lived secrets in JavaScript. For client portals like Mijar Law Associates, private channels must verify user identity before subscription using Broadcast::channel callbacks. SSE passes cookies on same-origin requests; cross-origin SSE needs CORS and careful credential flags. Both transports attract abuse, so apply connection limits per IP and per user alongside your normal API rate limiting patterns.

Server-Sent Events is the sweet spot for one-way feeds: live order status, notification bells, stock tickers, and dashboard metrics. It uses standard HTTP, so it is more proxy-friendly than WebSockets and lower overhead than long polling. Run the stream from Octane or a Redis-backed async worker rather than blocking PHP-FPM. The browser EventSource API reconnects automatically and sends Last-Event-ID so your server can replay missed events or return a snapshot after disconnect.

Laravel 13 ships first-class broadcasting through Reverb, a first-party WebSocket server built on PHP. Set BROADCAST_CONNECTION=reverb and run php artisan reverb:start as a systemd service treated like MySQL or Redis. Pair Reverb with Redis 8.10 for horizontal scale across nodes. Frontend clients subscribe through Laravel Echo with the reverb broadcaster. You still need nginx proxy_pass with Upgrade and Connection headers, firewall rules for the WebSocket port, and TLS termination for WSS in production.

Blocking PHP-FPM workers on long holds is the most frequent failure I see. Other recurring issues: proxy buffering batching SSE chunks, missing replay logic after SSE reconnect despite Last-Event-ID, WebSocket clients without heartbeat and backoff, long polling without cursor tokens causing duplicate delivery, and skipping connection monitoring. Track active SSE and WebSocket counts alongside PHP-FPM queue depth. Tools like Netdata help spot connection leaks before they take down the box.

SSE proxy and CDN compatibility is moderate compared to long polling, which works everywhere HTTP works. Success depends on disabling buffering at every layer so text/event-stream chunks flush immediately. Some corporate proxies strip streaming headers or buffer responses. Long polling remains the compatibility escape hatch in those environments. On shared hosting without root access, SSE through a managed edge function often beats self-hosting WebSockets because you cannot configure upgrade headers or open custom ports yourself.

HTMX with Laravel pushes HTML fragments over standard requests with no persistent socket at all, which suits many admin panels. For event-driven backends, EventBridge and SQS patterns decouple producers from delivery entirely. Fallback polling with short intervals remains acceptable for admin panels with five to ten concurrent users. Start with the simplest transport that meets latency requirements and upgrade to WebSockets only when metrics prove you need duplex traffic or binary frames.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: