For years, the standard PHP request-response lifecycle (the “share-nothing” architecture) has served us well. However, as modern web applications demand instantaneous feedback—think live notifications, collaborative editing, and chat apps—the traditional HTTP model struggles.
If you are still using AJAX Long Polling in 2025 to check for new messages every 3 seconds, it is time to upgrade.
In this guide, we are going to break out of the traditional synchronous PHP mindset. We will build a fully functional, real-time chat application using PHP WebSockets. We will utilize Ratchet, a popular PHP library that wraps ReactPHP’s event loop, allowing us to handle persistent connections efficiently.
Why WebSockets in PHP? #
Before we write code, we need to understand why we are doing this. In a standard PHP script (like Laravel or Symfony running on FPM), the script dies after sending the response. WebSockets, however, keep the connection open.
Here is a comparison of methods for real-time data:
| Feature | AJAX Polling | Server-Sent Events (SSE) | WebSockets |
|---|---|---|---|
| Communication | Unidirectional (Client request) | Unidirectional (Server to Client) | Bi-directional (Full Duplex) |
| Latency | High (depends on interval) | Low | Real-time (Lowest) |
| Overhead | High (Headers sent every time) | Medium | Low (Single handshake) |
| Server Load | High (Constant connections) | Low | Efficient |
| Best Use Case | Legacy systems | News feeds, stock tickers | Chat, Games, Collaboration |
As you can see, for a chat application where users need to both send and receive data instantly, WebSockets are the undisputed king.
Architecture Overview #
Before diving into the IDE, let’s visualize how our PHP WebSocket server handles messages. Unlike a standard Apache/Nginx setup, our PHP script will run as a daemon (a long-running process) listening on a specific port.
Prerequisites #
To follow this tutorial, you need a modern development environment.
- PHP 8.2 or higher: We will use typed properties and modern syntax.
- Composer: The standard dependency manager.
- Terminal/CLI Access: You cannot run a WebSocket server through a standard web host’s cPanel interface easily; you need shell access.
- Ports allowed: Ensure port
8080(or your chosen port) is open on your firewall.
Step 1: Setting Up the Environment #
First, create a new directory for your project and initialize Composer.
mkdir php-chat-app
cd php-chat-app
composer init --no-interaction --name="phpdevpro/chat-app"Next, we need to install Ratchet. Ratchet is a PHP library that allows developers to create real-time, bi-directional applications between clients and servers over WebSockets.
composer require cboden/ratchetWe also recommend adding the PSR-4 autoloading standard to your composer.json if it wasn’t added automatically.
File: composer.json
{
"name": "phpdevpro/chat-app",
"require": {
"cboden/ratchet": "^0.4"
},
"autoload": {
"psr-4": {
"MyApp\\": "src/"
}
}
}Note: After editing composer.json, always run composer dump-autoload.
Step 2: Creating the Chat Logic #
In Ratchet, the logic for handling connections resides in a class that implements the MessageComponentInterface. This interface requires four methods:
onOpen: Triggered when a new client connects.onMessage: Triggered when a client sends data.onClose: Triggered when a connection drops.onError: Triggered when a socket error occurs.
Create a folder named src and a file named Chat.php.
File: src/Chat.php
<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
protected \SplObjectStorage $clients;
public function __construct() {
// Initialize the storage for client connections
$this->clients = new \SplObjectStorage;
echo "Server Started. Listening for connections...\n";
}
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients->attach($conn);
echo "New connection! ({$conn->resourceId})\n";
// Optional: Send a welcome message to the user
$conn->send(json_encode([
'type' => 'system',
'message' => 'Welcome to the PHP DevPro Chat!'
]));
}
public function onMessage(ConnectionInterface $from, $msg) {
$numRecv = count($this->clients) - 1;
echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n"
, $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');
// Parse the incoming message (assuming JSON)
$data = json_decode($msg, true);
// Sanitize input to prevent XSS in the chat
$safeMessage = htmlspecialchars($data['message'] ?? $msg);
// Broadcast the message to everyone connected
foreach ($this->clients as $client) {
// In a real app, you might exclude the sender:
// if ($from !== $client) { ... }
$client->send(json_encode([
'type' => 'chat',
'user' => "User {$from->resourceId}",
'message' => $safeMessage,
'timestamp' => date('H:i:s')
]));
}
}
public function onClose(ConnectionInterface $conn) {
// The connection is closed, remove it, as we can no longer send it messages
$this->clients->detach($conn);
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}Key Technical Concepts #
SplObjectStorage: This is a standard PHP library class specifically designed to store objects. It is faster and more memory-efficient than a standard array when using objects (like connections) as keys.- JSON Protocol: Notice we are encoding/decoding JSON. Never send raw text in a production app; using a structured format like JSON allows you to add metadata (message type, timestamp, user info) easily.
Step 3: Wiring the Server #
Now we need an entry point script to run from the command line. This script will set up the Ratchet IoServer and bind it to a port.
Create a bin directory and a server.php file.
File: bin/server.php
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;
require dirname(__DIR__) . '/vendor/autoload.php';
// Create the application stack
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
8080 // The port to listen on
);
echo "Starting WebSocket server on port 8080...\n";
// Run the event loop
$server->run();To start your server, open your terminal and run:
php bin/server.phpYou should see: Starting WebSocket server on port 8080... and Server Started. Listening for connections....
Step 4: The Frontend Client #
A server is useless without a client. Let’s create a simple HTML file to connect to our PHP WebSocket.
File: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP Real-Time Chat</title>
<style>
body { font-family: 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f4f4f9; }
#chat-container { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }
#messages { height: 400px; overflow-y: auto; padding: 20px; border-bottom: 1px solid #eee; }
.message-row { margin-bottom: 10px; }
.meta { font-size: 0.8em; color: #888; margin-right: 5px; }
.content { background: #eef; padding: 5px 10px; border-radius: 4px; display: inline-block; }
.system-msg { color: #888; font-style: italic; text-align: center; }
#input-area { padding: 20px; display: flex; gap: 10px; }
input[type="text"] { flex-grow: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 10px 20px; background: #4a90e2; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #357abd; }
</style>
</head>
<body>
<h2>PHP WebSocket Chat</h2>
<div id="chat-container">
<div id="messages"></div>
<div id="input-area">
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off">
<button onclick="sendMessage()">Send</button>
</div>
</div>
<script>
// Connect to the PHP WebSocket Server
const conn = new WebSocket('ws://localhost:8080');
const messagesDiv = document.getElementById('messages');
conn.onopen = function(e) {
console.log("Connection established!");
addSystemMessage("Connected to chat server.");
};
conn.onmessage = function(e) {
const data = JSON.parse(e.data);
if (data.type === 'system') {
addSystemMessage(data.message);
} else if (data.type === 'chat') {
addChatMessage(data.user, data.message, data.timestamp);
}
};
conn.onclose = function(e) {
addSystemMessage("Disconnected from server.");
};
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value;
if (message.trim() === "") return;
const payload = {
message: message
};
conn.send(JSON.stringify(payload));
input.value = '';
}
function addChatMessage(user, text, time) {
const div = document.createElement('div');
div.className = 'message-row';
div.innerHTML = `<span class="meta">[${time}] ${user}:</span> <span class="content">${text}</span>`;
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function addSystemMessage(text) {
const div = document.createElement('div');
div.className = 'message-row system-msg';
div.innerText = text;
messagesDiv.appendChild(div);
}
// Allow pressing Enter to send
document.getElementById('messageInput').addEventListener('keypress', function (e) {
if (e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>Step 5: Testing the Application #
- Keep your terminal running with
php bin/server.php. - Open
index.htmlin your browser (you can drag the file into Chrome/Firefox, or serve it via a local Apache/Nginx server). - Open the same
index.htmlin a second browser window (or Incognito mode). - Type a message in window A. You should see it appear instantly in Window B.
- Check your terminal; you will see the logs of connections opening and messages being routed.
Performance & Production Considerations #
While the code above works perfectly for a demo, running WebSockets in production requires addressing several challenges.
1. The Blocking IO Problem #
PHP is single-threaded by nature. Ratchet simulates async behavior using an Event Loop. However, if you perform a blocking operation inside onMessage, the entire server pauses for everyone.
The Anti-Pattern:
public function onMessage(ConnectionInterface $from, $msg) {
// DO NOT DO THIS
sleep(5); // The server freezes for 5 seconds for ALL connected clients
$from->send("Done");
}The Solution: If you need to perform heavy tasks (sending emails, complex DB queries), push the job to a queue (like Redis or RabbitMQ) and let a separate worker process handle it.
2. Handling Deployment (Supervisor) #
If your terminal closes, the server dies. In production, use Supervisor or Systemd to keep your PHP script running as a daemon and restart it if it crashes.
Example Supervisor configuration:
[program:ratchet]
command=php /var/www/html/bin/server.php
autostart=true
autorestart=true
stderr_logfile=/var/log/ratchet.err.log
stdout_logfile=/var/log/ratchet.out.log3. SSL/TLS (WSS) #
Modern browsers require secure WebSockets (wss://) if the site is served over HTTPS. Ratchet can handle SSL, but it is better to use Nginx or HAProxy as a reverse proxy.
Nginx Configuration Concept: Your users connect to Nginx on port 443 (WSS), and Nginx proxies the traffic to your PHP script on localhost:8080.
location /ws/ {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}Conclusion #
Building a real-time chat application in PHP is not only possible but also surprisingly efficient with tools like Ratchet. We have moved from the resource-heavy “refresh and check” model to a persistent, event-driven architecture.
What we covered:
- The difference between HTTP and WebSockets.
- Setting up a Ratchet server with Composer.
- Handling message broadcasting logic.
- Frontend integration with JavaScript.
- Production pitfalls like blocking I/O and SSL.
As you advance, consider exploring Swoole or OpenSwoole for even higher performance (C-based extension), but Ratchet remains the most accessible entry point for PHP developers entering the async world.
Ready to scale? Try integrating Redis Pub/Sub to allow multiple WebSocket servers to talk to each other, enabling your chat to scale to thousands of concurrent users.
Happy coding!