14 min read

How Laravel Reverb Facilitates Real-Time Communication for Developers

Andria Toria

Lead Full-Stack Developer

Laravel Reverb Websocket

We build production Laravel apps that need to feel instant: live chat, trading balances that tick without a refresh, collaborative dashboards. Since Laravel shipped its own first-party WebSocket server, we've moved that real-time layer onto Laravel Reverb across several of our products, from a dedicated chat app to a crypto-wallet with live balances and our internal automations platform running Reverb in Docker behind Filament.

This guide is the version of the docs we wish we'd had on day one: not just how to send your first event, but how to authorise private channels, scale across servers with Redis, and survive a production deployment. Every command below is verified against the current Laravel 13.x / Reverb docs.

Laravel Reverb: Exploring The Key Features

With Laravel Reverb, you can now accomplish more with your Laravel-based projects thanks to a fresh set of features and capabilities. As mentioned earlier, Reverb’s main goal is to ensure smooth communication between the web application’s client and server sides through real-time interaction. Here’s a quick rundown of what Laravel Reverb can do for you:

Event Broadcasting: Reverb simplifies real-time data broadcasting which enables applications to easily update the user interface without needing to reload the page.

WebSockets Integration: Reverb offers an effective method to create bidirectional, real-time communication channels between clients and servers by using WebSockets.

Elegant API: It offers a fantastic, expressive API that follows the Laravel philosophy and makes it straightforward for developers to construct sophisticated real-time features with little to no code.

Seamless Frontend Integration: Reverb has been designed to integrate effortlessly with well-known frontend frameworks. It makes it easier for developers to add real-time data to their Laravel applications without having to make significant changes.

Speed: Since Reverb is built for rapid communication, it efficiently manages thousands of connections to ensure real-time responsiveness.

Scalability: It is built to scale easily, using Redis for the effective distribution of connections and data across multiple servers.

Laravel Reverb: What Are The Real-World Use Cases?

Because of its adaptability, Laravel Reverb can handle a wide range of tasks, from small projects to large enterprise-level systems. Here are some practical use cases:

E-Commerce Platforms: For live product updates, inventory management, and instant notifications on customer orders and shipping status.

Social Networking Sites: Implementing real-time feeds, notifications, and chat systems, enhancing user engagement and interactivity.

Online Collaboration Tools: Creating platforms where multiple users can work on documents or projects simultaneously, with changes reflected in real time.

Live Sports Updates and Streaming Services:  Providing users with instant updates on scores, events, or streaming content without latency.

Implementing Laravel Reverb Into a Chat Project

To learn more about Laravel Reverb, let’s create a mini-chat project. The project will include a list of chat rooms, and users will be able to join any of the rooms and then start chatting with each other.

Step 1: Setting Up Laravel Project

First, make sure you have Composer installed on your system. Then, create a new Laravel project by running the following:

composer create-project --prefer-dist laravel/laravel laravel-reverb-chat

Navigate to your project directory:

cd laravel-reverb-chat

Step 2: Installing Laravel Reverb

Install Laravel Reverb by running the following command:

php artisan install:broadcasting
npm install --save laravel-echo pusher-js

Once you’ve installed Reverb, you can now modify its configuration from the `config/reverb.php` file. In order to establish a connection to Reverb, a set of Reverb “application” credentials must be exchanged between the client and server. These credentials are configured on the server and are used to verify the request from the client. You can define these credentials using the following environment variables:

BROADCAST_DRIVER=reverb
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret

Step 3: Running Server

You can launch the Reverb server by using the reverb:start Artisan command:

php artisan reverb:start

By default, the Reverb server will be started at 0.0.0.0:8080, which makes it accessible from all network interfaces.

If you want to set a specific host or port, you can use the –host and –port options when starting the server.

php artisan reverb:start --host=127.0.0.1 --port=9000

You can also define REVERB_SERVER_HOST and REVERB_SERVER_PORT environment variables in your application’s .env configuration file.

Step 4: Setup Database

Open your .env file and adjust the settings to set up your database. Here’s an example using SQLite for simplicity:

DB_CONNECTION=sqlite
DB_DATABASE=/path/to/database.sqlite

You can create an SQLite database by simply running:

touch /path/to/database.sqlite

For this demo, we’ll create five predefined rooms. Let’s start by generating a migration for a rooms table.

php artisan make:model Room --migration

To make it simpler, only create name attributes for this model and migrate it.

Schema::create('rooms', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->timestamps();
});
php artisan migrate

After that, seed the database with five rooms. Create a seeder:

php artisan make:seeder RoomsTableSeeder 

In the RoomsTableSeeder, add:

DB::table('rooms')->insert([
    ['name' => 'Room 1'],
    ['name' => 'Room 2'],
    ['name' => 'Room 3'],
    ['name' => 'Room 4'],
    ['name' => 'Room 5'],
]);

Run seeder:

php artisan db:seed --class=RoomsTableSeeder

Step 5: Create Event

Inside the `app/Events` directory, create a new MessageSent.php file. This file is responsible for broadcasting new messages to dedicated chat rooms. Here’s a basic template:

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $userName;
    public $roomId;
    public $message;

    public function __construct($userName, $roomId, $message)
    {
        $this->userName = $userName;
        $this->roomId = $roomId;
        $this->message = $message;
    }

    public function broadcastOn() : Channel
    {

        return new Channel('chat.' . $this->roomId);
    }

    public function broadcastWith()
    {
        return [
            'userName' => $this->userName,
            'message' => $this->message,
        ];
    }
}

Step 6: Create Pages

In this project, we’ll have two pages: one to display a list of rooms and another for individual chat rooms. We’ll start by creating Blade templates to show the rooms. Let’s name these views index.blade.php and chat.blade.php and store them in a rooms directory under resources/views. Next, we’ll create a controller and a route to navigate to these pages.

index.blade.php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chat Rooms</title>
</head>
<body>
<div id="app">
    <h1>Chat Rooms</h1>
    <ul>
        @foreach($rooms as $room)
            <li>
                <a href="{{ route('rooms.show', $room->id) }}">Join {{ $room->name }}</a>
            </li>
        @endforeach
    </ul>
</div>
</body>
</html>

chat.blade.php

Set up a basic form to show chats and a simple input field for messaging. Make sure that you have Echo and Pusher imported in an app.js file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Room: {{ $room->name }}</title>
    @vite(['resources/css/app.css'])
    @vite(['resources/js/app.js'])
</head>
<body>
<div id="app">
    <h2>Chat Room: {{ $room->name }}</h2>
    <div id="messages"
         style="border: 1px solid #ccc; margin-bottom: 10px; padding: 10px; height: 300px; overflow-y: scroll;">
        <!-- Messages will be displayed here -->
    </div>
    <input type="text" id="messageInput" placeholder="Type your message here..." autofocus>
    <button onclick="sendMessage()">Send</button>
</div>

<script>
    document.addEventListener('DOMContentLoaded', function () {
        const roomId = "{{ $room->id }}";
        Echo.channel(`chat.${roomId}`)
            .listen('MessageSent', (e) => {
                const messages = document.getElementById('messages');
                const messageElement = document.createElement('div');
                messageElement.innerHTML = `<strong>${e.userName}:</strong> ${e.message}`;
                messages.appendChild(messageElement);
                messages.scrollTop = messages.scrollHeight; // Scroll to the bottom
            });
    })

    function sendMessage() {
        const messageInput = document.getElementById('messageInput');
        const message = messageInput.value;
        messageInput.value = ''; // Clear input
        const roomId = "{{$room->id}}"
        fetch(`/rooms/${roomId}/message`, {
            method: 'POST',
            headers: {
                'X-CSRF-TOKEN': '{{ csrf_token() }}',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({message: message})
        }).catch(error => console.error('Error:', error));
    }

</script>
</body>
</html>
php artisan make:controller RoomsController

Importing Echo and Pusher:

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 ?? 80,
    wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});

Now, let’s setup view methods:

class RoomsController extends Controller
{
    public function index()
    {
        $rooms = Room::all();
        return view('rooms.index',[
            'rooms' => $rooms
        ]);
    }

    public function show(Room $room)
    {
        return view('rooms.chat', [
            'roomId' => $room->id,
            'messages' => []
        ]);
    }
}

To keep things simple, let’s create a postMessage endpoint and add it to web.php.

php artisan make:controller ChatController
class ChatController extends Controller
{
    public function postMessage(Request $request, $roomId)
    {
        $userName = 'User_' . Str::random(4);
        $messageContent = $request->input('message');
        MessageSent::dispatch($userName, $roomId, $messageContent);
        return response()->json(['status' => 'Message sent successfully.']);
    }
}

routes/web.php file:

Route::get('/rooms', [RoomsController::class, 'index'])->name('rooms.index');
Route::get('/rooms/{room}', [RoomsController::class, 'show'])->name('rooms.show');
Route::post('/rooms/{roomId}/message', [ChatController::class, 'postMessage'])->name('api.rooms.message.post');

Step 7: Run Project

To run Laravel project, we need to execute the following command:

php artisan serve

For starting front:

npm run dev

Start queue:

php artisan queue:listen

Run reverb:

php artisan reverb:start

Here’s a link to the repository.

How do you scale Laravel Reverb across multiple servers?

A single Reverb instance handles a lot - but past one server's ceiling you scale horizontally with Redis pub/sub. When one Reverb server receives a message, it publishes it over Redis so every other Reverb server can relay it to their connected clients. Enable it with one env var:

REVERB_SCALING_ENABLED=true

Then stand up a dedicated central Redis instance that all Reverb servers share (Reverb uses your app's default Redis connection), run php artisan reverb:start on each server, and put them behind a load balancer that spreads connections evenly. The relevant slice of config/reverb.php - the exact structure we run - looks like this:

'scaling' => [
    'enabled' => env('REVERB_SCALING_ENABLED', false),
    'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
    'server' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'port' => env('REDIS_PORT', '6379'),
        'password' => env('REDIS_PASSWORD'),
        'database' => env('REDIS_DB', '0'),
    ],
],

If you'd rather not run this infrastructure yourself, Laravel Cloud offers fully-managed WebSocket infrastructure powered by Reverb clusters - you ship the app and skip the ops.

How Do You Deploy Reverb To Production?

Deploying Reverb means three things: a reverse proxy for WebSocket upgrades, a process manager to keep it alive, and raising OS file limits - because every open connection is an open file.

1. Nginx reverse proxy. Reverb runs on a non-public port; Nginx upgrades and proxies the connection:

location / {
    proxy_http_version 1.1;
    proxy_set_header Host $http_host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://0.0.0.0:8080;
}

Reverb listens for WebSocket traffic at /app and API requests at /apps - make sure your proxy serves both. (Laravel Forge configures this for you.)

2. Supervisor keeps the long-running process up and lets it open enough files:

[supervisord]
minfds=10000

3. Raise open-file limits in/etc/security/limits.conf, and remember to restart the server after any code change - Reverb is a long-running process, so use php artisan reverb:restart (it drains connections gracefully) rather than expecting hot-reload.

The gotcha that will bite you at ~1,000 connections. By default, Reverb's ReactPHP event loop is powered bystream_select, which is capped at 1,024 open files - so your shiny WebSocket server silently stops accepting connections at roughly a thousand concurrent clients, no matter how much RAM you have. The fix is to install the ext-uv PHP extension; Reverb automatically switches to the uv-powered loop when it's present:

pecl install uv

This one line is the difference between a load test that plateaus at ~1k and one that keeps climbing. [AUTHOR INPUT NEEDED: drop in our real concurrency numbers from the crypto-wallet / chat load test before vs after ext-uv, if we're willing to publish them]

One more trap: don't confuse REVERB_SERVER_HOST/REVERB_SERVER_PORT (where the Reverb process actually binds - e.g. 0.0.0.0:8080) with REVERB_HOST/REVERB_PORT (the public hostname/port Laravel sends broadcasts to - e.g. ws.example.com:443). In production they are different values, and mixing them up is the most common "it works locally but not in prod" Reverb bug.

How do you monitor Reverb in production?

Reverb integrates with Laravel Pulse so you can watch live connection and message counts on a dashboard. After installing Pulse, register the recorders in config/pulse.php:

use Laravel\Reverb\Pulse\Recorders\ReverbConnections;
use Laravel\Reverb\Pulse\Recorders\ReverbMessages;

'recorders' => [
    ReverbConnections::class => ['sample_rate' => 1],
    ReverbMessages::class    => ['sample_rate' => 1],
],

Add the cards to your Pulse view and run the pulse:check daemon on the Reverb server (only one server if you're horizontally scaled).

Laravel Reverb vs Pusher vs Soketi: which should you choose?

Choose Reverb when you want to own your real-time layer with zero per-message fees; choose Pusher when you'd rather pay to never think about WebSocket infrastructure; choose Soketi if you specifically want a raw-speed, Pusher-compatible self-hosted server outside the Laravel toolchain. All three speak the Pusher protocol, so Laravel Echo works with any of them and switching is largely a config change.

Laravel Reverb Pusher Soketi
Hosting Self-hosted (or Laravel Cloud) Fully managed SaaS Self-hosted
Maintained by Laravel core team Pusher Community/open-source
Cost model Infra only, no per-message fee Per-connection + per-message Infra only
Protocol Pusher-compatible Native Pusher Pusher-compatible
Scaling Redis pub/sub + load balancer Handled for you Redis / NATS adapter
Best for Laravel teams wanting control + margin Early-stage, ops-averse teams Max throughput, framework-agnostic

The trade-off is really about your stage: Pusher buys speed and peace of mind; Reverb buys margin and control. For teams already all-in on Laravel, Reverb removes a vendor, a bill, and an SDK, which is exactly why we standardized on it. Third-party migrations commonly report real-time cost reductions of ~90% moving off Pusher.

For more in-depth information, you can check the official documentation of Laravel Reverb.

Want a closer look at what we can build with Laravel? Visit the page for more details about our offer. If you have any questions or need advice, don’t hesitate to reach out - we’d love to help!

 

Frequently asked questions

Is Laravel Reverb production-ready?

Yes, it's been stable since it shipped with Laravel 11 and is maintained by the core team. Its production characteristics depend on your OS tuning (open files, ext-uv) and, past a single server, on Redis-based horizontal scaling.

Do I need Pusher to use Reverb?

No. You install pusher-js on the client because Reverb speaks the Pusher protocol, but you send zero traffic to Pusher's servers and pay them nothing.

Can I migrate from Pusher to Reverb without rewriting my app?

Largely yes, because both speak the Pusher protocol, the change is mostly broadcaster config and environment variables; your events, channels and Echo listeners stay the same.

How many connections can one Reverb server handle?

A single instance handles a large number of connections, but the practical ceiling depends on your server resources, message volume, and, critically, whether you've moved off the default stream_select loop to ext-uv past ~1,000 connections.

Updated on Jul 30, 2026 by

Andria Toria

First published on Aug 14, 2025

Andria Toria

About the author

Andria Toria

Lead Full-Stack Developer at Redberry

Led Medlocums, a UK Health Tech Platform

Andria is a Lead Full-Stack Developer at Redberry with 8+ years specializing in Laravel and Vue.js. He architects scalable systems and leads cross-functional teams across SaaS, fintech, and iGaming, including Medlocums (UK health tech), Rochservice (public-lighting infrastructure), and Garantme's authentication and subscription services.

Trending News

Jun
22

Hosting the First Official Laravel Meetup in Georgia

We hosted the first official Laravel Meetup in Georgia, bringing together more than 100 attendees for an evening dedicated to Laravel, engineering, and community.

Jun
22

Hosting a RDBR Meetup on Our Tavistock Protect Partnership

At our latest RDBR Meetup, we looked inside our two-year partnership with Tavistock Protect and the product we have been building together: PP Mobius.

img

Meet the authors

We are a 200+ people agency and provide product design, software development, and creative growth marketing services to companies ranging from fresh startups to established enterprises. Our work has earned us 100+ international awards, partnerships with Laravel, Vue, Meta, and Google, and the title of Georgia’s agency of the year in 2019 and 2021.

CONTACT US
img

Get in touch

Dati Chkhikvishvili

Chief Business Officer