14 min read
Lead Full-Stack Developer

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.
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.
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.
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.
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
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
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.
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
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,
];
}
}
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');
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.
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.
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) withREVERB_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.
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).
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!
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.
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.
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.
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.
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.
Last updated on Jul 30, 2026 by

We hosted the first official Laravel Meetup in Georgia, bringing together more than 100 attendees for an evening dedicated to Laravel, engineering, and community.
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.

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.

