DEV Community

Cover image for Ditch Pusher: First-Party WebSockets with Laravel Reverb 📡
Prajapati Paresh
Prajapati Paresh

Posted on Originally published at smarttechdevs.in

Ditch Pusher: First-Party WebSockets with Laravel Reverb 📡

The High Cost of Real-Time Infrastructure

In the modern enterprise web, real-time communication is no longer a luxury—it is a baseline requirement. Whether you are building a live collaborative document editor, a stock trading dashboard, or a customer support chat interface, users expect to see updates the millisecond they occur. Historically, achieving this in PHP was notoriously difficult due to PHP's synchronous, request-response lifecycle. PHP was designed to boot up, serve a request, and die. It was not designed to hold 10,000 persistent TCP connections open simultaneously.

To bypass this language limitation, the Laravel ecosystem traditionally relied on third-party SaaS providers like Pusher or Ably. While these services are excellent, they introduce severe architectural bottlenecks. First, they are expensive at enterprise scale; sending millions of messages a day can quickly cost thousands of dollars a month. Second, they introduce external latency and privacy concerns, as your sensitive internal data must leave your VPC (Virtual Private Cloud) to bounce off a third-party server before returning to your users.

At Smart Tech Devs, we bring our real-time infrastructure entirely in-house. With the release of Laravel Reverb, an incredibly fast and scalable first-party WebSocket server written purely in PHP, we can now handle thousands of concurrent connections directly on our own infrastructure, eradicating third-party costs and keeping our enterprise data strictly within our own firewalls.

Understanding the WebSocket Handshake

Unlike traditional HTTP where the client must constantly ask the server "Is there new data?" (Polling), WebSockets establish a permanent, bi-directional pipeline. The client sends a standard HTTP request with an Upgrade: websocket header. If the server supports it, it accepts the upgrade. The HTTP connection is kept alive, transforming into a persistent TCP socket. Both the server and the client can now push binary or text data down this open pipeline instantly, with zero HTTP header overhead.

Phase 1: Architecting the Reverb Server

Laravel Reverb is built on top of the powerful ReactPHP event loop. This allows PHP to break free from its synchronous constraints and handle asynchronous, non-blocking I/O operations—meaning a single PHP process can effortlessly juggle thousands of open WebSockets.

Once Reverb is installed, it runs as an independent daemon process alongside your primary Laravel web server (PHP-FPM or Octane). It listens on a dedicated port (usually 8080) strictly for WebSocket traffic.


// config/reverb.php

return [
    'default' => env('REVERB_SERVER', 'reverb'),

    'servers' => [
        'reverb' => [
            'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
            'port' => env('REVERB_SERVER_PORT', 8080),
            'hostname' => env('REVERB_HOST'),
            'options' => [
                // For enterprise SSL termination, you often place Nginx in front of Reverb,
                // but Reverb can handle TLS natively if required.
                'tls' => [],
            ],
            // Defining scaling parameters to prevent connection exhaustion
            'scaling' => [
                'enabled' => env('REVERB_SCALING_ENABLED', false),
                'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
            ],
        ],
    ],
];

Phase 2: Defining the Domain Event

In Laravel, broadcasting over WebSockets is elegantly integrated into the native Event system. To broadcast an event, your event class simply needs to implement the ShouldBroadcast or ShouldBroadcastNow interface.

Let's architect an event for a collaborative workspace. When a user updates a task status, we need to instantly notify every other team member viewing that specific project board.


namespace App\Events;

use App\Models\Task;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class TaskStatusUpdated implements ShouldBroadcastNow
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public Task $task;

    /**
     * Create a new event instance.
     */
    public function __construct(Task $task)
    {
        $this->task = $task;
    }

    /**
     * Get the channels the event should broadcast on.
     * We use a PrivateChannel to ensure only authorized project members can listen.
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('project.' . $this->task->project_id),
        ];
    }

    /**
     * Customize the broadcast name.
     */
    public function broadcastAs(): string
    {
        return 'task.updated';
    }

    /**
     * Control the exact payload sent to the client.
     * Never broadcast the entire Eloquent model, to prevent data leaks.
     */
    public function broadcastWith(): array
    {
        return [
            'id' => $this->task->id,
            'status' => $this->task->status,
            'updated_by' => auth()->user()->name,
        ];
    }
}

Phase 3: Securing the Private Channel

Because we broadcasted to a PrivateChannel, Reverb will aggressively reject any client attempting to listen to it unless they prove they have permission. The client must first send a standard HTTP POST request to your Laravel API to authorize the connection.

We define this authorization logic in our routes/channels.php file. Laravel automatically securely signs the token and hands it back to the client.


use Illuminate\Support\Facades\Broadcast;
use App\Models\User;
use App\Models\Project;

// Only allow the user to listen to this WebSocket channel if they are a member of the project
Broadcast::channel('project.{projectId}', function (User $user, int $projectId) {
    $project = Project::find($projectId);
    
    if (!$project) return false;

    // Return true if authorized, false to instantly sever the WebSocket connection
    return $project->members()->where('user_id', $user->id)->exists();
});

Phase 4: Horizontal Scaling with Redis Pub/Sub

A single Reverb server might comfortably handle 20,000 concurrent connections. But what if your enterprise platform explodes to 250,000 concurrent users? You must scale horizontally by spinning up multiple Reverb servers behind a Load Balancer.

This creates a massive architectural problem: If User A is connected to Reverb Server 1, and User B is connected to Reverb Server 2, how does Server 1 know to send the broadcast to User B? They are completely isolated processes.

The solution is Redis Pub/Sub. By enabling Reverb's scaling feature, all Reverb servers subscribe to a central Redis cluster. When Laravel dispatches the TaskStatusUpdated event, it publishes the payload to Redis. Redis instantly pushes the message to all connected Reverb servers simultaneously. Each Reverb server then checks its local memory to see if it holds any active WebSockets for that specific channel, and if so, fires the data down the pipe. Redis acts as the high-speed nervous system connecting your fleet of WebSocket servers.

The Engineering ROI

Bringing your real-time infrastructure in-house via Laravel Reverb provides a massive return on investment. You immediately eliminate variable third-party billing, stabilizing your monthly infrastructure costs regardless of how many millions of messages your platform processes. You drastically improve security and compliance (like GDPR or HIPAA) by guaranteeing that sensitive real-time data payloads never traverse the public internet or reside on external SaaS servers. Furthermore, by utilizing Redis Pub/Sub for horizontal scaling, you guarantee that your real-time architecture can scale to infinity alongside your core application.

Top comments (0)