Polling is a crime against bandwidth. Every two seconds, thousands of clients ask the server if anything changed. The server says no, every single time, until that one time it says yes. HTTP was not designed for real-time communication. WebSockets were. A single persistent connection. Full-duplex communication. Zero polling overhead.
If you are polling in 2026, you are doing it wrong.
The WebSocket handshake looks like HTTP but upgrades the connection. After the upgrade, the protocol switches to a lightweight binary framing. Messages have a 2-byte header. Compare that to HTTP polling where every request carries headers, cookies, and connection overhead. For a chat application with 10,000 concurrent users, the difference is megabytes per second versus kilobytes.
<?php
// Ratchet WebSocket server
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class ChatServer implements MessageComponentInterface
{
    protected SplObjectStorage $clients;

    public function __construct()
    {
        $this->clients = new SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);
        echo "New connection: {$conn->resourceId}\n";
    }

    public function onMessage(ConnectionInterface $from, $msg)
    {
        $data = json_decode($msg, true);

        // Broadcast to all connected clients
        foreach ($this->clients as $client) {
            $client->send(json_encode([
                'user' => $data['user'],
                'message' => $data['message'],
                'timestamp' => time()
            ]));
        }
    }

    public function onClose(ConnectionInterface $conn)
    {
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} closed\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e)
    {
        echo "Error: {$e->getMessage()}\n";
        $conn->close();
    }
}
Connection management is the hard part. WebSockets are stateful. If your server restarts, every connection drops. If you scale horizontally, clients connected to Server A cannot receive messages broadcast from Server B. You need a message broker — Redis Pub/Sub, RabbitMQ, or a dedicated WebSocket manager — to synchronize state across instances.
<?php
// Scaling with Redis Pub/Sub
class ScalableChatServer implements MessageComponentInterface
{
    private Redis $redis;
    private SplObjectStorage $clients;

    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
        $this->clients = new SplObjectStorage();

        // Subscribe to Redis channel for cross-server messages
        $this->redis->subscribe(['chat-broadcast'], function($redis, $channel, $message) {
            $this->broadcastLocal($message);
        });
    }

    public function onMessage(ConnectionInterface $from, $msg)
    {
        // Publish to Redis — all servers receive it
        $this->redis->publish('chat-broadcast', $msg);
    }

    private function broadcastLocal(string $message): void
    {
        foreach ($this->clients as $client) {
            $client->send($message);
        }
    }
}
Heartbeat is non-negotiable. TCP connections can hang indefinitely if a client disconnects uncleanly — power loss, network switch, browser crash. Without heartbeats, your server thinks dead connections are still alive, leaking memory until it crashes. Ping every 30 seconds. If no pong returns within 60 seconds, close the connection and clean up.
// Client-side heartbeat
const ws = new WebSocket('wss://api.example.com/chat');

ws.onopen = () => {
    // Send ping every 30 seconds
    setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: 'ping', time: Date.now() }));
        }
    }, 30000);
};

ws.onmessage = (event) => {
    const data = JSON.parse(event.data);

    if (data.type === 'pong') {
        const latency = Date.now() - data.time;
        console.log(`Latency: ${latency}ms`);
        return;
    }

    // Handle actual messages
    appendMessage(data);
};

// Reconnect with exponential backoff
ws.onclose = () => {
    let delay = 1000;
    const reconnect = () => {
        setTimeout(() => {
            console.log(`Reconnecting in ${delay}ms...`);
            // new WebSocket(...)
            delay = Math.min(delay * 2, 30000);
        }, delay);
    };
    reconnect();
};
WebSockets are not always the answer. Server-Sent Events are simpler for one-way streaming — live scores, stock tickers, notifications. They use standard HTTP, work through most proxies automatically, and reconnect transparently. Use WebSockets for true bidirectional communication. Use SSE for server-to-client streams. Use HTTP polling only as a last resort for environments that block everything else.
Real-time is not a feature you bolt on. It is an architectural decision that affects everything — state management, error handling, scaling, and testing. But when done right, it transforms user experience. A chat that updates instantly. A dashboard that reflects live data. A game that feels responsive. WebSockets make the web feel alive. That is worth the complexity.