Real-Time PHP with WebSockets
Reviewed & published by Brayan K
WebSockets keep a single connection open so your server can push updates to clients the instant they happen — no polling. In this final lesson you'll see the protocol vs HTTP, build a server with Ratchet, broadcast events with Laravel Reverb and Echo, and use the publish/subscribe pattern for chat and live updates.
Part of the free PHP course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Why WebSockets beat HTTP polling for real-time data
- How the HTTP-to-WebSocket upgrade handshake works
- Build a WebSocket server in PHP with Ratchet
- Broadcast events with Laravel Reverb and Echo
- Use the publish/subscribe (channels) pattern
- Run a long-lived socket server, not request-per-request
1️⃣ The Problem: HTTP Polling
HTTP is request/response: the client asks, the server answers, the connection closes. To fake real-time updates, apps resort to polling — re-requesting on a timer to ask "anything new?". The trouble is that most of those requests come back empty, wasting bandwidth and server work, and there's always a gap (up to the polling interval) between an event happening and the user seeing it. For chat or live data, that's both inefficient and noticeably laggy.
<?php
// THE OLD WAY: HTTP polling. The browser ASKS again and again on a timer,
// hoping for new data. Most requests come back empty -> wasted round trips.
// ---- pseudo client-side loop (e.g. setInterval every 3 seconds) ----
// every 3s: GET /api/messages?since=<lastId>
// if new messages -> show them
// else -> nothing, but we still paid for the request
// ---- server endpoint it hits ----
// Stand-ins for the query and the request, so this runs as it stands:
function fetchMessagesAfter(int $since): array {
$all = [['id' => 7, 'text' => 'hi']]; // your messages table
return array_values(array_filter($all, fn($m) => $m['id'] > $since));
}
$_GET['since'] = 6; // the id the client last saw
$since = (int)($_GET['since'] ?? 0);
$new = fetchMessagesAfter($since); // often returns []
header('Content-Type: application/json');
echo json_encode($new);
?>The fundamental limit is that the server can't speak first over plain HTTP — it can only answer when asked. WebSockets remove exactly that limit.
2️⃣ The WebSocket Handshake
A WebSocket starts as an ordinary HTTP request carrying an Upgrade: websocket header. If the server agrees, it replies 101 Switching Protocols and the connection switches from HTTP to the persistent WebSocket protocol. From that point the socket stays open and full-duplex: either side can send a message at any time until someone closes it. Use ws:// for plain and wss:// for TLS-encrypted connections.
# A WebSocket connection starts life as an HTTP request,
# then is UPGRADED to the persistent ws:// protocol.
# ---- client's opening request (sent by the browser) ----
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket # <-- "please switch protocols"
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZQ==
Sec-WebSocket-Version: 13
# ---- server agrees, switching protocols ----
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
# From here the socket stays OPEN. Either side can send a message
# at any time (full-duplex) until someone closes it. Use wss:// for TLS.Because it begins as HTTP, a WebSocket travels over the same ports (80/443) and through the same proxies as normal web traffic — it just refuses to hang up afterward.
3️⃣ A WebSocket Server with Ratchet
In classic PHP, Ratchet (composer require cboden/ratchet) lets you build a WebSocket server. You implement four lifecycle hooks — onOpen, onMessage, onClose, onError — and keep the set of connected clients in memory. The crucial difference from normal PHP is that this is a long-running process: you start it once with php server.php and it stays alive holding every socket, rather than running one request and exiting like PHP-FPM.
<?php
// A WebSocket SERVER in classic PHP using Ratchet.
// composer require cboden/ratchet
// NOTE: this is a LONG-RUNNING process. You start it once with
// php server.php
// and it stays alive holding every open socket — unlike PHP-FPM,
// which runs one request and exits.
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface
{
private \SplObjectStorage $clients; // every connected client
public function __construct() {
$this->clients = new \SplObjectStorage();
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn); // remember the new connection
}
public function onMessage(ConnectionInterface $from, $msg) {
// BROADCAST: push this message to everyone else (pub/sub style)
foreach ($this->clients as $client) {
if ($client !== $from) {
$client->send($msg);
}
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn); // forget a disconnected client
}
public function onError(ConnectionInterface $conn, \Exception $e) {
$conn->close();
}
}
?>The onMessage hook here broadcasts — it loops over the stored clients and pushes the message to each one. That's publish/subscribe in miniature: one sender, many receivers, no polling.
4️⃣ The Browser Client
PHP runs the server; the browser opens the connection. The native WebSocket object connects to your wss:// URL and exposes event handlers: onopen, onmessage, and onclose. The key one is onmessage — it fires the instant the server pushes data, with no timer and no polling, which is the whole point.
// THE CLIENT (browser JavaScript). PHP runs the server;
// the browser opens the socket and reacts to pushed messages.
const socket = new WebSocket("wss://example.com/chat"); // wss:// = encrypted
socket.onopen = () => {
socket.send("Hello, everyone!"); // send a message up to the server
};
socket.onmessage = (event) => {
// Fired the INSTANT the server pushes data -- no polling, no delay.
console.log("New message:", event.data);
};
socket.onclose = () => console.log("Connection closed");Send with socket.send() and receive in onmessage — a clean, symmetric, two-way channel between browser and PHP server.
5️⃣ Broadcasting with Laravel Reverb & Echo
In Laravel, you rarely write socket plumbing by hand. Reverb is Laravel's first-party WebSocket server (php artisan install:broadcasting, then php artisan reverb:start), and Echo is the matching browser client. You define an event that implements ShouldBroadcast and declares the channel it broadcasts on; firing that event pushes it to every subscriber in real time. This is the publish/subscribe model wired straight into the framework.
<?php
// THE MODERN LARAVEL WAY: broadcasting with Laravel Reverb + Echo.
// php artisan install:broadcasting (sets up Reverb + Echo)
// php artisan reverb:start (runs the WebSocket server)
//
// 1) Make an event that BROADCASTS itself when fired.
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MessageSent implements ShouldBroadcast // <-- marks it for broadcast
{
public function __construct(public string $text) {}
// Which CHANNEL subscribers listen on (publish/subscribe).
public function broadcastOn(): Channel
{
return new Channel('chat');
}
}
// 2) Fire it anywhere -- every subscriber to "chat" gets it in real time:
// event(new MessageSent('Hello from Laravel!'));
?>Now you try the two core skills — broadcasting an event and opening a client socket. Fill in each ___ using the 👉 hint, then check it against the Output panel.
<?php
// 🎯 YOUR TURN — turn an event into a real-time broadcast.
// Fill in each blank marked ___ , then fire it.
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
// 1) Implement the interface that marks this event for broadcasting
class OrderShipped implements ___ // 👉 the interface: ShouldBroadcast
{
public function __construct(public int $orderId) {}
// 2) Broadcast on a channel named "orders"
public function broadcastOn(): Channel
{
return new Channel('___'); // 👉 the channel name: orders
}
}
// ✅ event(new OrderShipped(42)) now reaches every "orders" subscriber.
?>One more. Open a socket in the browser and react to pushed messages — no polling.
// 🎯 YOUR TURN — open a socket and react to pushed messages.
// No timers, no polling. Fill the two ___ blanks.
// 1) Open an ENCRYPTED WebSocket connection
const socket = new ___("wss://example.com/feed"); // 👉 the constructor: WebSocket
// 2) Run code every time the server PUSHES a message
socket.___ = (event) => { // 👉 the event handler: onmessage
console.log("Update:", event.data);
};
// ✅ The instant the server sends data, "Update: ..." is logged.Common Errors (and the fix)
- The server dies when the page that started it closes — you ran it like a normal script. A WebSocket server is long-lived: start it from the CLI and keep it running under a process manager like Supervisor so it restarts if it crashes.
- Connection works locally but fails in production behind HTTPS — a page served over https:// can't open an insecure ws:// socket. Use wss:// and terminate TLS at your proxy (Nginx) in front of the server.
- "Upgrade Required" / handshake fails through a proxy — your reverse proxy isn't forwarding the upgrade. Configure it to pass Upgrade and Connection headers for the WebSocket location.
- Messages broadcast to no one in Laravel — the event doesn't implement ShouldBroadcast, or Reverb isn't running. Add the interface, start php artisan reverb:start, and check your broadcast driver in .env.
- Memory grows forever as users come and go — you attached connections but never detached them. Remove each socket from your client store in onClose so closed connections are freed.
Pro Tips
- 💡 Supervise the server. Run your socket process under Supervisor or systemd so it auto-restarts — a crashed server drops every connection.
- 💡 Use channels for scope. Publish to named channels (per room, per user) so clients only receive the events they care about, not everything.
- 💡 Consider server-sent events for one-way streams. If you only push from server to client (a live feed), SSE is simpler; reach for WebSockets when you need true two-way traffic.
📋 Quick Reference — Real-Time PHP
| Concept / Snippet | What It Means |
|---|---|
| Upgrade: websocket | HTTP header that starts the handshake |
| 101 Switching Protocols | Server agrees; connection becomes a socket |
| ws:// vs wss:// | Plain vs TLS-encrypted WebSocket URL |
| cboden/ratchet | Build a WebSocket server in plain PHP |
| onOpen / onMessage / onClose | Ratchet connection lifecycle hooks |
| ShouldBroadcast | Marks a Laravel event for broadcasting |
| php artisan reverb:start | Run Laravel's WebSocket server |
| Echo.channel('chat') | Subscribe to a channel in the browser |
| new WebSocket(url) | Open a socket from JavaScript |
Mini-Challenge: A Live Notification Feed
No code is filled in this time — just a brief and an outline. Build a real-time feed with either Ratchet or Laravel Reverb: run a long-lived server, broadcast events to subscribers, and render each pushed message live in the browser. This is the publish/subscribe loop behind every chat and notifications system.
<?php
// 🎯 MINI-CHALLENGE: A real-time notification feed.
// No code is filled in -- work from the steps below.
//
// 1. Pick a stack:
// - Plain PHP: composer require cboden/ratchet, run a long-lived
// php server.php that keeps an SplObjectStorage of clients.
// - Laravel: php artisan install:broadcasting, then reverb:start.
// 2. Maintain the set of connected clients (or a "notifications" channel).
// 3. When something happens (a new order, a comment), BROADCAST it:
// - Ratchet: loop over clients and $client->send($json).
// - Laravel: fire an event implementing ShouldBroadcast on
// new Channel('notifications').
// 4. On the browser, open new WebSocket("wss://.../notifications")
// (or Echo.channel('notifications')) and render each pushed message.
// 5. Remember: the server is a LONG-RUNNING process -- start it once and
// keep it alive (e.g. under Supervisor), not per-request like PHP-FPM.
//
// ✅ Expected: trigger an event on the server and watch it appear live
// in every open browser, with zero polling.
// your code here
?>🎉 Lesson Complete!
- ✅ WebSockets hold one persistent, full-duplex connection so the server can push instantly
- ✅ HTTP polling wastes requests and adds latency; an Upgrade handshake switches to ws://
- ✅ Ratchet builds a server in plain PHP via onOpen/onMessage/onClose hooks
- ✅ Laravel Reverb + Echo broadcast events that implement ShouldBroadcast
- ✅ The publish/subscribe pattern sends events to everyone subscribed to a channel
- ✅ The socket server is a long-running process — start it once and supervise it, unlike request-per-request PHP
- 🎓 That's the course! You can now build real-time, production-grade PHP applications end to end.
Practice quiz
What is the key advantage of WebSockets over HTTP polling?
- They keep one persistent, two-way connection open so the server can push data instantly
- They require no server at all
- They encrypt data that HTTP cannot
- They use less memory on the client
Answer: They keep one persistent, two-way connection open so the server can push data instantly. A WebSocket holds a single long-lived, full-duplex connection, so the server can push messages the moment something happens, instead of the client repeatedly asking.
Why is HTTP polling inefficient for real-time updates?
- It cannot send JSON
- It only works over HTTPS
- The client must repeatedly request, wasting requests when nothing has changed
- It blocks the database
Answer: The client must repeatedly request, wasting requests when nothing has changed. Polling re-requests on a timer whether or not there is new data, creating wasted round trips and latency between an event and the client learning about it.
How does a WebSocket connection begin?
- With a database transaction
- With an HTTP request that is upgraded to the WebSocket protocol
- With a raw TCP packet only
- With an FTP handshake
Answer: With an HTTP request that is upgraded to the WebSocket protocol. The client sends an HTTP request containing an Upgrade: websocket header; if the server agrees, the connection switches to the ws:// (or wss://) protocol.
Which classic PHP library lets you build a WebSocket server?
- Twig
- Carbon
- PHPUnit
- Ratchet
Answer: Ratchet. Ratchet is a long-standing PHP library for building WebSocket servers on top of an event loop (ReactPHP).
What does Laravel Reverb provide?
- A first-party WebSocket server for Laravel broadcasting
- A CSS framework
- A templating engine
- A database migration tool
Answer: A first-party WebSocket server for Laravel broadcasting. Reverb is Laravel's official, self-hosted WebSocket server that powers real-time broadcasting; the client side is handled by Laravel Echo.
In the publish/subscribe pattern, what does a client do to receive messages about a topic?
- Polls the database directly
- Subscribes to a channel and receives every event published to it
- Opens a new connection per message
- Sends an email
Answer: Subscribes to a channel and receives every event published to it. Clients subscribe to named channels; when the server publishes an event to a channel, every subscriber receives it without asking.
Why can't a standard request-per-request PHP setup hold WebSocket connections by default?
- WebSockets require Python
- PHP cannot parse JSON
- PHP has no networking functions
- The usual model starts a process, handles one request, and exits - it isn't a long-running server
Answer: The usual model starts a process, handles one request, and exits - it isn't a long-running server. Classic PHP (via PHP-FPM) is request-per-request: a script runs and exits. WebSockets need a persistent, long-running process with an event loop to keep sockets open.
In Laravel, how do you push a real-time update to subscribed clients?
- Run a database migration
- Echo it to standard output
- Broadcast an event that implements ShouldBroadcast
- Write it to a log file
Answer: Broadcast an event that implements ShouldBroadcast. You fire a Laravel event that implements ShouldBroadcast; the broadcasting system sends it over the WebSocket server to clients subscribed to its channel.
Which use case is the best fit for WebSockets?
- A one-time file download
- A live chat application with instant message delivery
- Generating a static PDF
- A nightly batch report
Answer: A live chat application with instant message delivery. Chat, live dashboards, notifications, and multiplayer features all need instant, bidirectional updates - exactly what persistent WebSocket connections provide.
What is the correct URL scheme for an encrypted WebSocket connection?
- wss://
- https://
- ftp://
- ws://
Answer: wss://. ws:// is a plain WebSocket connection; wss:// is the TLS-encrypted version, the WebSocket equivalent of HTTPS.
Continue this course
- Previous: GraphQL APIs in PHP
- Next: Namespaces & PSR-4 Autoloading — Organize code with namespaces and Composer's PSR-4 autoloader
- Quick reference: PHP cheat sheet
Frequently asked questions
What is the difference between WebSockets and HTTP polling?
HTTP polling has the client repeatedly send requests on a timer to ask 'is there anything new?', so most requests return nothing and there is always a delay between an event and the client learning about it. A WebSocket opens a single persistent, full-duplex connection: once established, the server can push data the instant something happens and the client can send at any time too. Polling wastes requests and adds latency; WebSockets are efficient and effectively instant, which is why real-time features use them.
Why does PHP need a special long-running server for WebSockets?
The traditional PHP model (via PHP-FPM) is request-per-request: a script starts, handles one HTTP request, returns a response, and exits. That lifecycle can't hold a connection open. WebSockets need a process that stays alive indefinitely, runs an event loop, and keeps every open socket in memory. Libraries like Ratchet (on ReactPHP) and Laravel Reverb provide exactly that — a long-lived server you start once (often supervised) and that holds all the connections.
Should I use Ratchet or Laravel Reverb?
Ratchet is a mature, framework-agnostic library for building a WebSocket server by hand in plain PHP — great when you want full control or aren't using Laravel. Laravel Reverb is the first-party option for Laravel apps: it's a self-hosted WebSocket server wired into Laravel's broadcasting system, with Laravel Echo on the client side, so you broadcast events with very little code. If you're in Laravel, Reverb (or a compatible service like Pusher) is the smoother path; otherwise Ratchet is a solid choice.
What is the publish/subscribe pattern in real-time apps?
Publish/subscribe decouples senders from receivers using named channels. Clients subscribe to a channel they care about (say 'chat' or 'orders'), and when the server publishes an event to that channel, every subscriber receives it automatically — no one polls and the publisher doesn't need to know who is listening. This is how broadcasting works: in Laravel you fire an event on a channel; in Ratchet you loop over the connections subscribed to a topic and send to each.
What are typical use cases for WebSockets?
Anything that needs instant, two-way, or server-pushed updates: live chat and messaging, real-time notifications, collaborative editing, live dashboards and metrics, sports scores or stock tickers, multiplayer game state, and progress indicators for long-running jobs. If a feature would otherwise rely on the browser polling every few seconds, it's usually a candidate for WebSockets. For simpler one-way streams, server-sent events are a lighter alternative, but WebSockets cover the full bidirectional case.