Laravel Real-Time Notifications: Pusher vs FCM vs Soketi vs Ably (2026)

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

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 tierUnlimited — you host it
PaidOnly your server costs
Self-hostedYes
Laravel EchoYes — 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 tierUnlimited — you host it
PaidOnly your server costs
Self-hostedYes
Laravel EchoYes — Pusher compatible
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 tierUnlimited messages (free)
PaidFree forever
Self-hostedNo (Google infrastructure)
Laravel EchoNo — 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
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:

# 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.

Need Real-Time Features in Your Laravel App?

I've implemented WebSocket broadcasting and FCM push notifications in production SaaS platforms and e-commerce systems. Happy to help you choose and set up the right stack for your project.

Based in Bangladesh · Remote worldwide · Fast turnaround

About the Author

Kamruzzaman Polash — Software Engineer specialising in Laravel, REST APIs, and scalable backend systems. 10+ projects delivered for clients worldwide.