Real-time notifications are one of the most commonly requested features in modern web and mobile apps — and also one of the most confusing to set up in Laravel. Should you use Pusher? FCM? Self-host with Soketi or Laravel Reverb? And what even is Laravel Echo?
This guide cuts through the confusion. I'll explain what each option does, which scenario it's best for, what it actually costs, and show you the implementation code.
Key distinction first: Pusher/Soketi/Reverb/Ably handle WebSocket notifications — real-time updates while a user has your app open in a browser. FCM handles push notifications — delivered to mobile devices and browsers even when the app is closed. Most serious apps need both.
1. Understanding Laravel Echo
Before diving into services, let's clarify what Laravel Echo is — because it's not a notification service itself.
Laravel Echo is a JavaScript client library that subscribes to WebSocket channels and listens for events broadcast from your Laravel backend. It's the frontend piece. You still need a backend broadcaster — Pusher, Soketi, Ably, or Reverb — for Echo to connect to.
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
});
Echo.private(`user.${userId}`)
.notification((notification) => {
console.log('New notification:', notification);
showToast(notification.message);
});
The beauty of Echo is that you can swap broadcasters with almost no frontend code changes — just update your .env credentials. The same Echo setup works with Pusher, Soketi, Ably, or Reverb.
2. All Options Compared
The most popular hosted WebSocket service for Laravel. Easiest setup, massive community, official Laravel support. Gets expensive fast at scale.
The official Laravel WebSocket server introduced in Laravel 11. Self-hosted, free, extremely fast (built on ReactPHP), and Pusher-protocol compatible so Echo works out of the box. The best default choice in 2026.
Open-source, self-hosted, Pusher-compatible WebSocket server. Existed before Reverb and is still widely used. Runs on Node.js. Great if you're already running a Node environment or need horizontal scaling across multiple servers.
Enterprise-grade hosted WebSocket platform. More reliable and better priced than Pusher at scale. Global edge network with 99.999% uptime SLA. Ideal for high-traffic apps that need managed infrastructure.
Google's push notification service. Delivers notifications to Android, iOS, and web browsers even when the app is closed. Completely different from WebSocket broadcasting — this is for mobile push notifications. Free with no meaningful limits.
3. Full Comparison Table
| Service |
Type |
Cost |
Works offline? |
Mobile push? |
Self-hosted? |
Echo support? |
Best for |
| Laravel Reverb |
WebSocket |
Free |
No |
No |
Yes |
Yes |
Most Laravel apps in 2026 |
| Soketi |
WebSocket |
Free |
No |
No |
Yes |
Yes |
Node.js environments, multi-server |
| Pusher |
WebSocket |
$49+/mo |
No |
Beams (paid) |
No |
Yes |
Quick prototypes, small teams |
| Ably |
WebSocket |
$29+/mo |
No |
Add-on |
No |
Yes |
High-traffic, enterprise |
| FCM |
Push |
Free |
Yes |
Yes |
No |
No |
Mobile apps, background notifications |
4. Which One for Which Scenario?
Scenario
Standard Laravel web app
Use Laravel Reverb — free, official, fast. No third-party dependency.
Scenario
Mobile app (Android/iOS)
Use FCM — free, works when app is closed, industry standard for push.
Scenario
Rapid prototype / startup MVP
Use Pusher free tier — zero server setup, get running in 10 minutes.
Scenario
High-traffic SaaS (10K+ users)
Use Ably — better reliability and 20–30% cheaper than Pusher at scale.
Scenario
Budget-conscious production app
Use Reverb or Soketi self-hosted — $0 service cost, just your VPS.
Scenario
Full-featured app (web + mobile)
Use Reverb + FCM together — WebSocket for in-app, FCM for background push.
5. Implementation: Laravel Reverb (Recommended)
Reverb is the right default for most Laravel projects in 2026. Here's the complete setup:
php artisan install:broadcasting
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
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'],
});
php artisan make:event OrderStatusUpdated
<?php
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public readonly Order $order
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'status' => $this->order->status,
'message' => "Your order #{$this->order->id} is now {$this->order->status}",
];
}
public function broadcastAs(): string
{
return 'order.updated';
}
}
OrderStatusUpdated::dispatch($order);
Echo.private(`orders.${userId}`)
.listen('.order.updated', (event) => {
showNotification(`Order #${event.order_id}: ${event.message}`);
});
php artisan reverb:start
php artisan reverb:start --host=0.0.0.0 --port=8080
6. Implementation: FCM Push Notifications
FCM handles push notifications — delivering to mobile devices and browsers even when your app is closed. Use the kreait/laravel-firebase package:
composer require kreait/laravel-firebase
FIREBASE_CREDENTIALS=/path/to/firebase-service-account.json
<?php
use Kreait\Firebase\Contract\Messaging;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\Notification;
class NotificationService
{
public function __construct(private Messaging $messaging) {}
public function sendPush(string $deviceToken, string $title, string $body): void
{
$message = CloudMessage::withTarget('token', $deviceToken)
->withNotification(
Notification::create($title, $body)
)
->withData([
'order_id' => '123',
'type' => 'order_update',
]);
$this->messaging->send($message);
}
public function sendToMultiple(array $tokens, string $title, string $body): void
{
$message = CloudMessage::new()
->withNotification(Notification::create($title, $body));
$this->messaging->sendMulticast($message, $tokens);
}
}
import { getMessaging, getToken } from 'firebase/messaging';
const messaging = getMessaging();
const token = await getToken(messaging, { vapidKey: 'YOUR_VAPID_KEY' });
await axios.post('/api/device-token', { token });
7. Using Pusher (Quick Setup)
If you want zero server setup and don't mind the cost, Pusher gets you running fastest:
composer require pusher/pusher-php-server
BROADCAST_CONNECTION=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
Pusher pricing warning: The free sandbox allows 200 daily connections and 200K messages/day — fine for development. But paid plans start at $49/month. At mid-scale (thousands of concurrent users), expect $500–2000/month. If cost matters, migrate to Reverb early.
8. Switching from Pusher to Reverb (Zero Code Changes)
One of the best things about this stack: because Reverb is Pusher-protocol compatible, you can migrate from Pusher to Reverb by changing only your .env file — no code changes required.
BROADCAST_CONNECTION=pusher
PUSHER_APP_KEY=abc123...
BROADCAST_CONNECTION=reverb
REVERB_APP_KEY=my-own-key
9. Best Practices
Always queue your broadcast events
Broadcasting involves network calls to Pusher/Reverb. Don't do it synchronously in your request cycle — implement ShouldBroadcast and ensure your queue worker is running:
class OrderStatusUpdated implements ShouldBroadcast
class OrderStatusUpdated implements ShouldBroadcastNow
Use private channels for user-specific notifications
Never broadcast sensitive data on public channels. Use private or presence channels and define authorization in routes/channels.php:
Broadcast::channel('orders.{userId}', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
Store FCM tokens per user, handle token rotation
FCM tokens expire and rotate. Always upsert tokens on login, and handle messaging/registration-token-not-registered errors by deleting invalid tokens from your database.
Combine WebSockets + FCM for complete coverage
WebSockets (Reverb/Pusher) only work when the user has your app open. FCM covers background delivery. For the best experience, fire both:
public function notifyUser(User $user, string $message): void
{
OrderStatusUpdated::dispatch($user->id, $message);
if ($user->device_token) {
$this->fcm->sendPush($user->device_token, 'Update', $message);
}
}
10. Summary: What to Choose in 2026
- Building a new Laravel app? Start with Reverb — free, official, zero external dependency
- Need mobile push? Add FCM — free forever, works when app is closed
- Want zero server management? Pusher for small scale, Ably for large scale
- On a budget but need production WebSockets? Soketi or Reverb self-hosted on your existing VPS
- Already on Pusher and want to cut costs? Migrate to Reverb — just swap the
.env
For related topics, see the Laravel Queues guide — queue workers are essential for broadcasting events without slowing down your HTTP responses.