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.
// resources/js/bootstrap.js — Echo connects to your chosen broadcaster
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Echo = new Echo({
broadcaster: 'pusher', // works with Pusher, Soketi, Ably, Reverb
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
});
// Listen for events on a private channel
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
Pusher Channels — Freemium
The most popular hosted WebSocket service for Laravel. Easiest setup, massive community, official Laravel support. Gets expensive fast at scale.
- Free tier: 200 connections/day, 200K messages/day
- Paid from: $49/month
- Self-hosted: No
- Laravel Echo: Yes — native
Laravel Reverb — Free / Self-hosted
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.
- Free tier: Unlimited — you host it
- Paid: Only your server costs
- Self-hosted: Yes
- Laravel Echo: Yes — native
Soketi — Free / Self-hosted
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.
- Free tier: Unlimited — you host it
- Paid: Only your server costs
- Self-hosted: Yes
- Laravel Echo: Yes — Pusher compatible
Ably — Freemium
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.
- Free tier: 6M messages/month, 200 peak connections
- Paid from: $29/month
- Self-hosted: No
- Laravel Echo: Yes — official adapter
FCM — Firebase Cloud Messaging (Free)
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.
- Free tier: Unlimited messages (free)
- Paid: Free forever
- Self-hosted: No (Google infrastructure)
- Laravel Echo: No — different protocol
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 | Recommendation |
|---|---|
| Standard Laravel web app | Use Laravel Reverb — free, official, fast. No third-party dependency. |
| Mobile app (Android/iOS) | Use FCM — free, works when app is closed, industry standard for push. |
| Rapid prototype / startup MVP | Use Pusher free tier — zero server setup, get running in 10 minutes. |
| High-traffic SaaS (10K+ users) | Use Ably — better reliability and 20–30% cheaper than Pusher at scale. |
| Budget-conscious production app | Use Reverb or Soketi self-hosted — $0 service cost, just your VPS. |
| 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:
# Install Reverb (Laravel 11+)
php artisan install:broadcasting
# This installs Reverb and publishes config automatically
# .env values are set automatically too
# .env
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}"
// resources/js/bootstrap.js — Echo auto-configured for Reverb
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'],
});
// Create a broadcast event
php artisan make:event OrderStatusUpdated
<?php
// app/Events/OrderStatusUpdated.php
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public readonly Order $order
) {}
// Which channel to broadcast on
public function broadcastOn(): array
{
return [
new PrivateChannel("orders.{$this->order->user_id}"),
];
}
// What data to send
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}",
];
}
// Custom event name (optional)
public function broadcastAs(): string
{
return 'order.updated';
}
}
// Dispatch from anywhere in your app
OrderStatusUpdated::dispatch($order);
// Frontend — listen for it
Echo.private(`orders.${userId}`)
.listen('.order.updated', (event) => {
showNotification(`Order #${event.order_id}: ${event.message}`);
});
# Start Reverb server
php artisan reverb:start
# In production — keep it running with Supervisor
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:
# Install Firebase package
composer require kreait/laravel-firebase
# Download your service account JSON from Firebase Console
# Project Settings → Service Accounts → Generate new private key
# .env
FIREBASE_CREDENTIALS=/path/to/firebase-service-account.json
<?php
// Send a push notification via FCM
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);
}
// Send to multiple devices at once
public function sendToMultiple(array $tokens, string $title, string $body): void
{
$message = CloudMessage::new()
->withNotification(Notification::create($title, $body));
$this->messaging->sendMulticast($message, $tokens);
}
}
// Store device tokens when user logs in (frontend)
import { getMessaging, getToken } from 'firebase/messaging';
const messaging = getMessaging();
const token = await getToken(messaging, { vapidKey: 'YOUR_VAPID_KEY' });
// Send token to your Laravel backend
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:
# Install Pusher PHP SDK
composer require pusher/pusher-php-server
# .env
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.
# Before (Pusher)
BROADCAST_CONNECTION=pusher
PUSHER_APP_KEY=abc123...
# After (Reverb — same frontend Echo code works)
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:
// Use ShouldBroadcastNow only if you NEED synchronous broadcasting
// Otherwise always use ShouldBroadcast (queued)
class OrderStatusUpdated implements ShouldBroadcast // queued ✓
class OrderStatusUpdated implements ShouldBroadcastNow // synchronous ✗ avoid
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:
// 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
{
// WebSocket — instant if user is online
OrderStatusUpdated::dispatch($user->id, $message);
// FCM — delivered even if user closed the app
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.
Work with me
Need help with your Laravel architecture?
I build clean, maintainable Laravel backends for SaaS products — REST APIs, queue systems and production patterns like the one in this article.