Bekzod Erkinov
Listen to Article
Loading...Laravel Real-Time Notifications: Reverb vs Pusher
A Laravel notification that waits two seconds to reach the browser feels broken, and the cause is usually not Reverb or Pusher. In my runs (n = 10 requests per setup, one run each, so treat the numbers as indicative), moving the queue worker from its default poll to --sleep=0 cut median delivery from 1,805 ms to 302 ms. Separately, a stock Reverb process crashed at 1,013 connections until I installed ext-uv.
Decision rule. Under ~100 concurrent users, use Pusher's free Sandbox unless you already run a server. Between 100 and a few thousand, compare Pusher's $49–$99 plans with the ops hours you would spend (break-even table below). Beyond that, I have no measured Reverb capacity to back a recommendation, so load-test before you commit either way.
Below is a notification pipeline on Reverb: private channel auth, a queued broadcast, and an Echo client you can run from the post alone. Then comes the client config that points the same code at Pusher, and the cost math. Everything ran on 2026-10-07 in one Docker container with the client on a Windows host (so read timings as relative). I had no Pusher account, so the Pusher sections rely on its documentation and are untested.
Versions, as printed by composer show and npm ls on this run:
$ composer show laravel/framework | grep versions
versions : * v13.35.0
$ composer show laravel/reverb | grep versions
versions : * v1.12.0
$ php -v | head -1
PHP 8.5.11 (cli) (built: Sep 24 2026 19:10:01) (NTS)
$ npm ls
+-- [email protected]
`-- [email protected]
Doc links point at the 13.x pages. For a Pusher-only, production-flavoured take, see Building a Real-Time Notification System with Laravel and Pusher.
Install Reverb for Laravel real-time notifications
composer create-project laravel/laravel demo
cd demo
php artisan install:broadcasting --reverb --without-node --no-interaction
php artisan install:api --no-interaction # Sanctum, used for the token auth below
This installs laravel/reverb, writes config/broadcasting.php, config/reverb.php and routes/channels.php, and fills .env with BROADCAST_CONNECTION=reverb and the REVERB_* keys. I used --without-node because I drove Echo from a plain Node script. In a normal app, drop that flag. Add the HasApiTokens trait to User and set QUEUE_CONNECTION=database (the install already created the jobs table).
Reverb needs ext-pcntl. My bare composer:2 image lacked it, so I ran docker-php-ext-install pcntl. Then:
php artisan reverb:start --debug # Starting server on 0.0.0.0:8080
--debug prints every frame, so leave it off for load tests (docs).
Authorize the private channel
Notifications go out on App.Models.User.{id} (notifications docs). The installer already wrote the rule in routes/channels.php:
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Echo calls POST /broadcasting/auth before subscribing. By default that route uses the web middleware (session cookie and CSRF), which is awkward for a mobile app or a script. I moved it behind Sanctum bearer tokens:
// bootstrap/app.php
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up', // no `channels:` key any more
)
->withBroadcasting(
__DIR__.'/../routes/channels.php',
['middleware' => ['auth:sanctum']],
)
For a classic session-based browser app, keep the channels: line instead. The callback is what matters either way.
Send the notification, and queue it correctly
class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(public int $invoiceId, public int $amount) {}
public function via(object $notifiable): array
{
return ['broadcast'];
}
public function toBroadcast(object $notifiable): BroadcastMessage
{
return (new BroadcastMessage([
'invoice_id' => $this->invoiceId,
'amount' => $this->amount,
]))->onQueue('broadcasts');
}
}
// routes/api.php
Route::post('/invoices/{id}/pay', function (Request $request, int $id) {
$request->user()->notify(new InvoicePaid($id, 4900));
return ['queued' => true];
})->middleware('auth:sanctum');
That looks like one queued job. It is two, and the worker log from this run shows it:
App\Notifications\InvoicePaid ............ 160.34ms DONE
Illuminate\Notifications\Events\BroadcastNotificationCreated 112.81ms DONE
Gotcha: two hops. The notification runs on default, then queues BroadcastNotificationCreated onto broadcasts. A worker listening on only one of them silently stalls the other. Run php artisan queue:work --queue=broadcasts,default.
What the queue costs in latency
I timed "client sends POST /pay" to "Echo callback fires", n = 10 requests per setup, one run each. The time includes the HTTP round trip through Docker Desktop. The harness is below, so you can re-time your own setup:
// bench.mjs -- node bench.mjs 10 (needs ids.json from the next section)
import { readFileSync } from 'node:fs';
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
const { me, token } = JSON.parse(readFileSync('ids.json', 'utf8'));
const api = 'http://localhost:8000';
const N = Number(process.argv[2] ?? 10);
const echo = new Echo({
broadcaster: 'reverb', Pusher, key: process.env.REVERB_APP_KEY,
wsHost: 'localhost', wsPort: 8080, forceTLS: false, enabledTransports: ['ws'],
authEndpoint: `${api}/broadcasting/auth`,
auth: { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } },
});
let t0, done;
const times = [];
echo.
Unlock Premium Content
You've read 30% of this article
What's in the full article
- Complete step-by-step implementation guide
- Working code examples you can copy-paste
- Advanced techniques and pro tips
- Common mistakes to avoid
- Real-world examples and metrics
Don't have an account? Start your free trial
Join 10,000+ developers who love our premium content
Keep reading
Decentralized Finance Protocol Comparison: Uniswap V3 vs SushiSwap vs Curve Finance - Performance, Security, and Use Cases
16 min · 879 views
AI WorkflowsOrbit Determination Program (ODP) vs ORBIT14 vs ESOC: What Public Papers Say, Plus a Python Batch Fit
10 min · 280 views
Operating SystemsAdvanced Laravel Tutorial: Queues, Events and Pest Tests
22 min · 243 views
Bekzod Erkinov
AuthorFounder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.
Get the AI-Assisted Developer's Field Guide
The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.
Comments (0)
Please log in to leave a comment.
Log In