Skip to content
ExponaBLOG
PHP & LARAVEL DEVELOPMENT · DAY 055 / 100

Real-time features with Laravel Reverb, explained simply

WebSockets built into Laravel.

Expona Admin7 min read

The WebSocket gap and why Reverb solves it

Real-time features—live notifications, collaborative editing, activity feeds, presence indicators—have become table stakes for modern applications. Yet building them traditionally means reaching for external services like Pusher or Socket.io, managing additional infrastructure, and wrestling with authentication across boundaries. Real-time features with Laravel Reverb, explained simply: WebSockets built into Laravel removes that friction entirely. Reverb ships as a first-party WebSocket server that speaks the same language your Laravel app does.

If you've felt the pain of coordinating real-time updates across your stack, you understand the appeal. Reverb lets you broadcast events directly from your application code using Laravel's familiar broadcasting system, handle authentication without external boilerplate, and scale with Redis when you need it. This is how we'll walk through getting it production-ready.

Broadcast events to channels

At its core, Reverb is a channel-based pub/sub system. You define channels, publish events to them, and clients listening on those channels receive updates. Let's say we're building a real-time order dashboard for a ride-hailing platform. When an order status changes, we want all relevant users to see it instantly.

First, define a broadcastable event:

php
// app/Events/OrderStatusChanged.php
namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class OrderStatusChanged implements ShouldBroadcast
{
    use InteractsWithSockets;

    public function __construct(
        public Order $order,
        public string $newStatus
    ) {}

    public function broadcastOn(): array
    {
        return [
            new Channel('orders.' . $this->order->id),
        ];
    }

    public function broadcastAs(): string
    {
        return 'status.changed';
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'status' => $this->newStatus,
            'updated_at' => $this->order->updated_at,
        ];
    }
}

The magic happens when you dispatch this event from anywhere in your application—a controller, a job, a command:

php
// In your order status update logic
OrderStatusChanged::dispatch($order, 'in_transit');

Reverb catches this broadcast and pushes it to all connected clients listening on the orders.123 channel. The data flows through your WebSocket server directly to browsers, with no external service call. For high-frequency updates like GPS coordinates on a delivery map, this latency difference is significant.

Public channels like this are fine for data that doesn't need access control. But what about sensitive information? That's where private channels enter the picture.

Private and presence channels with auth

Real-world scenarios almost always require authorization. You don't want driver tracking data broadcast to every connected user—only to the customer and assigned driver for that order.

Private channels enforce authorization through callbacks:

php
// routes/channels.php
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('order.{orderId}', function ($user, $orderId) {
    $order = Order::find($orderId);
    
    return $user->id === $order->customer_id || 
           $user->id === $order->driver_id;
});

When a client attempts to subscribe to private-order.123, Laravel verifies the user is authorized before opening the connection. If authorization fails, the subscription is rejected silently. This keeps your real-time data secure without implementing a separate auth layer.

Presence channels add another dimension: awareness of who's online. Imagine a collaborative CRM where sales reps can see which colleagues are viewing the same lead:

php
// routes/channels.php
Broadcast::channel('leads.{leadId}', function ($user, $leadId) {
    $lead = Lead::find($leadId);
    
    if ($user->team_id !== $lead->team_id) {
        return false;
    }
    
    return [
        'id' => $user->id,
        'name' => $user->name,
        'avatar' => $user->avatar_url,
    ];
}, ['guards' => ['web']]);

The presence channel broadcasts user metadata when someone joins or leaves. On the client, you can display active viewers in real time. This isn't just UI sugar—it prevents conflicts and shows teams where attention is focused.

When you broadcast an event from within a presence channel context, you can access presence information:

php
class LeadCommentAdded implements ShouldBroadcast
{
    public function broadcastOn(): array
    {
        return [
            new PresenceChannel('leads.' . $this->lead->id),
        ];
    }
}

Laravel Echo on the client

Broadcasting events server-side is half the story. On the browser, you need to listen for incoming messages. Laravel Echo is a JavaScript library that handles WebSocket subscriptions, reconnection logic, and message handling.

Install it in your front-end project:

php
npm install laravel-echo

Configure Echo to connect to your Reverb server:

php
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'reverb',
    key: import.meta.env.VITE_REVERB_APP_KEY,
    wsHost: import.meta.env.VITE_REVERB_HOST,
    wsPort: import.meta.env.VITE_REVERB_PORT,
    wssPort: import.meta.env.VITE_REVERB_PORT,
    forceTLS: import.meta.env.VITE_REVERB_SCHEME === 'https',
    enabledTransports: ['ws', 'wss'],
});

Now subscribe to channels and listen for events in your Vue or React component:

php
// Listen on a public channel
Echo.channel('orders.123')
    .listen('OrderStatusChanged', (event) => {
        console.log('Order status:', event.status);
        updateOrderUI(event);
    });

// Listen on a private channel (requires authentication)
Echo.private('order.123')
    .listen('OrderAssigned', (event) => {
        showNotification('Driver assigned: ' + event.driver_name);
    });

// Subscribe to presence channel with callbacks
Echo.join('leads.456')
    .here((users) => {
        // This callback fires when subscription succeeds
        // users contains everyone already in the channel
        updateActiveViewers(users);
    })
    .joining((user) => {
        // Someone joined
        addViewerBadge(user);
    })
    .leaving((user) => {
        // Someone left
        removeViewerBadge(user);
    })
    .listen('LeadCommentAdded', (event) => {
        appendComment(event);
    });

Echo handles reconnection automatically when the network drops. It queues subscriptions and re-establishes them when the connection restores. For teams building SaaS platforms where connection stability matters, this built-in resilience is valuable.

Scale with Redis

In development or small deployments, Reverb works fine with its in-memory state. But when you're running multiple instances—which you are in production—in-memory state becomes a liability. A client connected to instance A won't receive events broadcast from instance B.

Redis solves this. Reverb uses Redis as a distributed message broker. All instances publish events to Redis; all instances subscribe to Redis and replay messages to their connected clients.

Configure it in .env:

php
REVERB_BROADCAST_DRIVER=redis
REDIS_HOST=redis.internal
REDIS_PORT=6379

That's it. Reverb automatically uses Redis for inter-instance communication. A payment confirmation event broadcast from your API instance will reach clients connected to your web UI instance. This scales transparently to dozens of instances without code changes.

In our experience with trading platforms handling high-frequency price updates, Redis with Reverb kept latency consistent even during traffic spikes. The overhead is minimal—Redis is already in most production stacks for caching, sessions, and queues.

For extremely high throughput scenarios (millions of events per minute), consider tuning Redis persistence settings. Use appendonly no in your Reverb Redis instance if you're comfortable with potential message loss on crash; persistence guarantees aren't typically needed for real-time updates since they're ephemeral by nature.

Common mistakes

  • Broadcasting heavy data structures. It's tempting to broadcast entire model instances. Serialize only the fields clients need. Fat payloads slow down message delivery and waste bandwidth.
  • Forgetting CSRF protection on private channels. Reverb requires CSRF tokens for WebSocket connections to private channels. This happens automatically with Laravel Echo if your Blade template includes @csrf, but if you're using SPA auth (token-based), double-check the Reverb documentation for your auth guard.
  • Not testing channel authorization logic. A bug in your channel callback silently fails subscription. Clients won't know why they're not receiving data. Write tests for authorization logic as you would for route middleware.
  • Deploying Reverb without Redis in multi-instance setups. This is the silent killer. Everything works in dev, fails mysteriously in production when traffic spreads across instances. Always use Redis in production.
  • Ignoring connection limits. Reverb can hold tens of thousands of concurrent connections per process, but your OS may have file descriptor limits. Check ulimit -n and increase if needed on your Reverb host.

Key takeaways

  • Reverb brings WebSocket broadcasting into Laravel's native ecosystem, eliminating external service dependency for real-time features.
  • Public channels broadcast data to anyone; private channels enforce authorization; presence channels add awareness of who's connected.
  • Laravel Echo on the client handles subscriptions, reconnection, and message routing with minimal setup.
  • Redis scales Reverb across multiple server instances by acting as a distributed message broker.
  • Real-time data is ephemeral—broadcast only what clients need, when they need it, and lean on Echo's reconnection to handle transient network issues.
  • Test channel authorization logic and configure your deployment to use Redis before pushing to production.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles