Skip to content

PHP WebSockets — Complete Guide to Real-Time Communication

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about PHP WebSockets. We cover key concepts, practical examples, and best practices to help you master this topic.

PHP WebSockets enable real-time bidirectional communication between a server and clients, allowing live chat applications, push notifications, collaborative editing, and real-time dashboards without polling. This tutorial covers installing Ratchet, building a WebSocket server, handling connections and events, implementing a chat room, managing topic subscriptions, broadcasting messages, and deploying WebSocket servers with Supervisor.

What You'll Learn

  • Understanding the WebSocket protocol and upgrade handshake
  • Installing Ratchet for PHP WebSocket servers
  • Building event handlers for onOpen, onMessage, onClose, and onError
  • Implementing a multi-room chat server
  • Broadcasting messages to connected clients
  • Using ZeroMQ for cross-Process message publishing
  • Handling connection heartbeats and stale connection cleanup
  • Deploying WebSocket servers with Supervisor process manager

Why It Matters

Traditional PHP uses request-response cycles: the browser sends a request, PHP processes it, and returns HTML. This does not work for real-time features like live chat or notifications. WebSockets provide a persistent connection that the server can push data through at any time, enabling interactive applications that users expect today.

Real-World Use

DodaTech's live support chat system uses Ratchet WebSockets to connect customers with support agents in real time. When a customer types a message, it appears instantly on the agent's screen without page refreshes. The system pushes notifications when support tickets are updated, reducing average response time from minutes to seconds.

Learning Path

flowchart LR
  A[File Manager Project] --> B[WebSockets\nYou are here]
  B --> C[Guzzle HTTP]
  style B fill:#f90,color:#fff

Installing Ratchet

Ratchet is a PHP WebSocket server built on ReactPHP:

composer require cboden/ratchet

Simple WebSocket Server

Create chat-server.php:

<?php
require __DIR__ . '/vendor/autoload.php';

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

class ChatServer implements MessageComponentInterface
{
  protected \SplObjectStorage $clients;

  public function __construct()
  {
    $this->clients = new \SplObjectStorage();
    echo "Chat server started.\n";
  }

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

  public function onMessage(ConnectionInterface $from, $msg): void
  {
    $data = json_decode($msg, true);
    if (!$data || !isset($data['type'])) return;

    switch ($data['type']) {
      case 'chat':
        $message = json_encode([
          'type' => 'chat',
          'username' => $data['username'] ?? 'Anonymous',
          'message' => $data['message'] ?? '',
          'timestamp' => date('H:i:s'),
        ]);
        foreach ($this->clients as $client) {
          $client->send($message);
        }
        break;

      case 'typing':
        $message = json_encode([
          'type' => 'typing',
          'username' => $data['username'] ?? 'Anonymous',
          'is_typing' => $data['is_typing'] ?? false,
        ]);
        foreach ($this->clients as $client) {
          if ($client !== $from) {
            $client->send($message);
          }
        }
        break;
    }
  }

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

  public function onError(ConnectionInterface $conn, \Exception $e): void
  {
    echo "Error: {$e->getMessage()}\n";
    $conn->close();
  }
}

$server = IoServer::factory(
  new HttpServer(new WsServer(new ChatServer())),
  8080
);

echo "Listening on port 8080...\n";
$server->run();

Run the server:

php chat-server.php

Output:

Chat server started.
Listening on port 8080...

Client-Side HTML and JavaScript

Create chat-client.html:

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Chat</title>
  <style>
    #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; }
    .message { margin-bottom: 5px; }
    .username { font-weight: bold; color: #3498db; }
    .timestamp { font-size: 11px; color: #999; }
    .typing { font-style: italic; color: #888; }
  </style>
</head>
<body>
  <h1>Chat Room</h1>
  <div id="messages"></div>
  <input type="text" id="username" value="User" placeholder="Your name">
  <input type="text" id="message-input" placeholder="Type message..." style="width:300px">
  <button id="send-btn">Send</button>
  <div id="typing-indicator" class="typing"></div>

  <script>
    const ws = new WebSocket('ws://localhost:8080');
    const messages = document.getElementById('messages');
    const input = document.getElementById('message-input');
    const username = document.getElementById('username');
    const typingDiv = document.getElementById('typing-indicator');
    let typingTimer = null;
    let isTyping = false;

    ws.onmessage = function(event) {
      const data = JSON.parse(event.data);
      if (data.type === 'chat') {
        const div = document.createElement('div');
        div.className = 'message';
        div.innerHTML = '<span class="username">' + escapeHtml(data.username) + '</span>: '
          + escapeHtml(data.message)
          + ' <span class="timestamp">' + data.timestamp + '</span>';
        messages.appendChild(div);
        messages.scrollTop = messages.scrollHeight;
      } else if (data.type === 'typing') {
        typingDiv.textContent = data.is_typing ? data.username + ' is typing...' : '';
      }
    };

    function sendMessage() {
      const msg = input.value.trim();
      if (!msg) return;
      ws.send(JSON.stringify({ type: 'chat', username: username.value, message: msg }));
      input.value = '';
    }

    input.addEventListener('keydown', function(e) {
      if (e.key === 'Enter') { sendMessage(); return; }
      if (!isTyping) { isTyping = true; ws.send(JSON.stringify({ type: 'typing', username: username.value, is_typing: true })); }
      clearTimeout(typingTimer);
      typingTimer = setTimeout(function() {
        isTyping = false;
        ws.send(JSON.stringify({ type: 'typing', username: username.value, is_typing: false }));
      }, 2000);
    });

    document.getElementById('send-btn').addEventListener('click', sendMessage);

    function escapeHtml(text) {
      var d = document.createElement('div');
      d.textContent = text;
      return d.innerHTML;
    }
  </script>
</body>
</html>

Open chat-client.html in two browser tabs. Messages typed in one tab appear instantly in the other.

Room-Based Chat

Extend the server with rooms. Clients can join and leave rooms, and messages go only to clients in the same room:

<?php
// room-chat-server.php (partial — room handling)
protected array $rooms = [];

public function onOpen(ConnectionInterface $conn): void
{
  $this->clients->attach($conn, ['room' => 'lobby']);
  $this->rooms['lobby'][] = $conn->resourceId;
}

public function onMessage(ConnectionInterface $from, $msg): void
{
  $data = json_decode($msg, true);
  if (!$data || !isset($data['type'])) return;

  if ($data['type'] === 'join') {
    $this->joinRoom($from, $data['room'] ?? 'lobby');
  } elseif ($data['type'] === 'chat') {
    $this->sendToRoom($from, $data);
  }
}

private function joinRoom(ConnectionInterface $conn, string $room): void
{
  $oldRoom = $this->clients->offsetGet($conn)['room'];
  $this->removeFromRoom($conn, $oldRoom);
  $this->clients->offsetSet($conn, ['room' => $room]);
  $this->rooms[$room][] = $conn->resourceId;
  $conn->send(json_encode(['type' => 'joined', 'room' => $room]));
}

private function sendToRoom(ConnectionInterface $from, array $data): void
{
  $room = $this->clients->offsetGet($from)['room'];
  $message = json_encode([
    'type' => 'chat',
    'username' => $data['username'] ?? 'Anonymous',
    'message' => $data['message'] ?? '',
    'timestamp' => date('H:i:s'),
  ]);
  foreach ($this->clients as $client) {
    if ($this->clients->offsetGet($client)['room'] === $room) {
      $client->send($message);
    }
  }
}

Deploying with Supervisor

WebSocket servers run continuously. Use Supervisor to keep them alive:

Create /etc/supervisor/conf.d/chat-server.conf:

[program:chat-server]
command=php /var/www/chat-server.php
directory=/var/www
user=www-data
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/chat-server.err.log
stdout_logfile=/var/log/chat-server.out.log

Restart Supervisor:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start chat-server

Common Mistakes

  1. Not handling connection limits: PHP's default max connections setting may be too low. Increase ulimit -n and configure the OS for higher connection counts.
  2. Blocking the event loop: Long-running PHP operations (database queries, file reads) block ReactPHP's event loop. Use asynchronous database drivers or offload work to a queue.
  3. Not cleaning up stale connections: Clients can disconnect without sending a close frame. Implement heartbeat pings every 30 seconds and close unresponsive connections.
  4. Sending too much data in a single message: WebSocket frames have size limits. For large payloads, split data into chunks.
  5. Not validating incoming messages: A malicious client can send arbitrary data. Validate JSON structure and sanitize content before broadcasting.

Practice Questions

  1. How does the WebSocket handshake work?

    • The client sends an HTTP Upgrade request. The server responds with 101 Switching Protocols and the connection upgrades to WebSocket.
  2. What is the difference between Ratchet and traditional PHP?

    • Ratchet runs a persistent event loop handling multiple connections. Traditional PHP terminates after sending a response.
  3. How do you broadcast to all connected clients?

    • Iterate over SplObjectStorage and call send() on each ConnectionInterface.
  4. What is the purpose of a typing indicator?

    • It sends typing events to the server when the user is typing and stops after inactivity. Other clients display "User is typing..."
  5. Challenge: Build a WebSocket-based collaborative whiteboard where multiple users draw simultaneously, broadcasting draw events to all connected clients.

Mini Project

Build a real-time notification system:

  • Create a WebSocket server subscribing to a Redis pub/sub channel.
  • PHP scripts publish notifications to the channel when events occur.
  • The WebSocket server pushes notifications to the appropriate client by user ID.
  • Include notification types: success, warning, error, info.
  • Implement notification history via REST API for reconnecting users.

FAQ

What is Ratchet?

Ratchet is a PHP WebSocket library on ReactPHP. It provides a WebSocket server, client, and middleware components.

Can PHP handle many concurrent WebSocket connections?

With ReactPHP's event loop, a single PHP process can handle thousands of concurrent connections. For more, use multiple processes behind a load balancer.

How do I broadcast messages from my main PHP application?

Use a message broker like Redis pub/sub or ZeroMQ. Your application publishes to a channel; the WebSocket server subscribes and forwards to clients.

Do I need a separate server for WebSockets?

Yes. WebSocket servers run continuously and are separate from your traditional PHP web server. They share data through a message broker.

What's Next

Now learn how to make HTTP requests to external APIs with Guzzle HTTP.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro