Laravel Broadcasting: Real-time Events
Modern web applications are expected to update in real time. Users want to see new messages without refreshing, receive live notifications the moment something happens, and watch dashboards that reflect current data. Polling the server every few seconds is wasteful and slow — what you really need is push. Laravel Broadcasting is the framework's answer to this problem: a clean, driver-based API that lets you broadcast server-side events over persistent WebSocket connections directly to your frontend, with minimal boilerplate and full authentication support.
Under the hood, Broadcasting builds on top of Laravel's event system. When something meaningful happens in your application — a new order is placed, a support ticket is updated, a user sends a message — you fire a regular Laravel event that implements ShouldBroadcast. Laravel serializes that event, dispatches it through a queue worker, and forwards it to a WebSocket server (Pusher, Reverb, or others). The browser receives the payload instantly via a persistent connection established by Laravel Echo. The result is a clean separation between your server-side logic and the real-time delivery layer.
HTTP polling checks for updates every N seconds, wasting bandwidth even when nothing changed. WebSocket broadcasting delivers updates in under 50ms the moment they happen, uses a single persistent connection, and scales to thousands of concurrent users. Use broadcasting whenever you need sub-second latency or per-user event delivery.
How Laravel Broadcasting Works
Before writing any code it helps to understand the data flow. Every broadcast involves five layers working together:
- Event class: A standard Laravel event that implements
ShouldBroadcast(orShouldBroadcastNowfor immediate dispatch) - Queue worker: Picks up the broadcast job and forwards it to the driver
- Broadcasting driver: Pusher, Ably, Laravel Reverb, or Redis — the WebSocket server that manages persistent connections
- Laravel Echo: A JavaScript library that subscribes to channels and maps incoming payloads to callbacks
- Channel authorization: Server-side callbacks in
routes/channels.phpthat control which users can subscribe to private or presence channels
Laravel Broadcasting data flow — from event fire to browser callback
Choosing a Broadcasting Driver
Laravel supports four broadcasting drivers out of the box. Your choice affects cost, hosting requirements, and latency:
| Driver | Hosting | Best For | Cost |
|---|---|---|---|
| Laravel Reverb | Self-hosted (PHP) | Full control, no third-party dependency | Free (server costs only) |
| Pusher | Managed cloud | Fast setup, generous free tier | Free up to 200k daily messages |
| Ably | Managed cloud | Global edge network, high throughput | Free up to 6M messages/month |
| Redis + Socket.io | Self-hosted | Custom Node.js server setups | Free (infrastructure costs) |
For new projects in 2026, Laravel Reverb is the recommended choice. It's a first-party WebSocket server written in PHP that runs alongside your Laravel application — no external service accounts, no rate limits, and no additional latency from a third-party edge node. Pusher remains excellent for teams that want a fully managed zero-ops solution.
Installation and Configuration
Installing Laravel Reverb
Install Reverb via Artisan
Run the install:broadcasting command. It installs the Reverb server, publishes its config, and wires up the Echo scaffolding automatically.
# Install Reverb (first-party WebSocket server)
php artisan install:broadcasting
# This installs reverb, publishes config/reverb.php,
# and sets BROADCAST_CONNECTION=reverb in .env
# Install frontend dependencies
npm install --save-dev laravel-echo
npm install --save pusher-js # Reverb uses Pusher protocol
# Start Reverb server (separate terminal)
php artisan reverb:start
# Start queue worker (events are queued by default)
php artisan queue:work
Configure .env
Set your broadcasting connection and Reverb credentials. The installer pre-fills most of these — verify they are correct.
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}"
Enable the BroadcastServiceProvider
Uncomment App\Providers\BroadcastServiceProvider::class in config/app.php if it isn't already enabled (older Laravel versions).
Broadcastable events are dispatched through Laravel's queue by default. Make sure your QUEUE_CONNECTION is set to database, redis, or another real driver — not sync — otherwise events will block the request cycle and you won't see real-time behavior.
Using Pusher Instead
If you prefer a managed service, swap Reverb for Pusher in three steps:
# Install Pusher PHP SDK
composer require pusher/pusher-php-server
# .env settings
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}"
Creating Broadcastable Events
A broadcastable event is any Laravel event class that implements the ShouldBroadcast interface. The interface requires a single method, broadcastOn(), that returns the channels the event should be sent to. Use ShouldBroadcastNow if you want to skip the queue and broadcast synchronously — useful for critical alerts, but use sparingly since it blocks the HTTP response.
php artisan make:event OrderStatusUpdated
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderStatusUpdated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Order $order)
{
//
}
/**
* Channels this event should broadcast on.
* PrivateChannel requires the subscriber to be authorized.
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('orders.' . $this->order->id),
];
}
/**
* Override the event name seen by the frontend.
* Default would be "App\\Events\\OrderStatusUpdated".
*/
public function broadcastAs(): string
{
return 'order.updated';
}
/**
* Customize the payload sent to the frontend.
* By default, all public properties are included.
*/
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
'total' => $this->order->total,
];
}
/**
* Conditional broadcasting — only fire when truly changed.
*/
public function broadcastWhen(): bool
{
return $this->order->wasChanged('status');
}
}
Firing the event from a controller or service is identical to any other Laravel event:
public function updateStatus(Request $request, Order $order): JsonResponse
{
$order->update(['status' => $request->status]);
// This queues the broadcast — non-blocking
OrderStatusUpdated::dispatch($order);
return response()->json(['message' => 'Status updated']);
}
Channel Types and Authorization
Laravel Broadcasting supports three channel types, each with different access semantics. Choosing the right type is important both for security and for the features available on the frontend.
Public Channels
Any client can subscribe to a public channel without authentication. Use them for truly public data: live sports scores, stock tickers, public chat rooms.
public function broadcastOn(): array
{
return [
new Channel('announcements'),
];
}
Private Channels
Private channels require the subscriber to pass an authorization check on your server before the WebSocket server allows the connection. This is where you enforce business rules: "only the order owner can listen to order updates."
Authorization callbacks live in routes/channels.php. Return true to grant access, false to deny:
<?php
use App\Models\Order;
use Illuminate\Support\Facades\Broadcast;
// Private channel: user can only subscribe to their own orders
Broadcast::channel('orders.{orderId}', function ($user, $orderId) {
$order = Order::find($orderId);
return $order && $order->user_id === $user->id;
});
// Private channel: only admins can subscribe
Broadcast::channel('admin.dashboard', function ($user) {
return $user->hasRole('admin');
});
// Using a Gate policy for cleaner authorization
Broadcast::channel('orders.{order}', function ($user, Order $order) {
return $user->can('view', $order);
});
Presence Channels
Presence channels extend private channels with membership awareness. When a user subscribes, all other subscribers are notified. You can see who is currently online in a room — perfect for collaborative tools, live dashboards, and "X is typing" indicators.
// Return user data (not just true/false) for presence channels
Broadcast::channel('chat.{roomId}', function ($user, $roomId) {
if ($user->canJoinRoom($roomId)) {
// Returned array is shared with all members in the channel
return [
'id' => $user->id,
'name' => $user->name,
'avatar' => $user->avatar_url,
];
}
});
// Presence event
class UserJoinedRoom implements ShouldBroadcast
{
public function broadcastOn(): array
{
return [
new PresenceChannel('chat.' . $this->roomId),
];
}
}
Frontend Integration with Laravel Echo
Laravel Echo is a JavaScript library that wraps the WebSocket driver (Pusher JS) and provides an expressive API for subscribing to channels and listening to events. Configure it once in your resources/js/bootstrap.js or resources/js/echo.js:
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'],
});
Listening to Events
// Public channel — no auth required
Echo.channel('announcements')
.listen('AnnouncementPublished', (e) => {
console.log('New announcement:', e.title);
showToast(e.title, e.message);
});
// Private channel — server authorizes via routes/channels.php
// broadcastAs() name used here with leading dot
Echo.private(`orders.${orderId}`)
.listen('.order.updated', (e) => {
document.getElementById('order-status').textContent = e.status;
updateProgressBar(e.status);
});
// Presence channel — membership events included
Echo.join(`chat.${roomId}`)
.here((members) => {
// Called immediately with current member list
updateOnlineList(members);
})
.joining((member) => {
// Called when a new user joins
addToOnlineList(member);
showSystemMessage(`${member.name} joined`);
})
.leaving((member) => {
// Called when a user disconnects
removeFromOnlineList(member.id);
})
.listen('MessageSent', (e) => {
appendMessage(e.user, e.message);
})
.whisper('typing', { name: currentUser.name }); // client-only events
Real-Time Notifications
Laravel's notification system integrates directly with Broadcasting. Implement toBroadcast() in your notification class and Echo can listen on the user's personal notification channel with zero extra setup:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class InvoicePaid extends Notification
{
use Queueable;
public function __construct(private readonly float $amount) {}
public function via($notifiable): array
{
// Send both email and real-time broadcast
return ['mail', 'broadcast'];
}
public function toBroadcast($notifiable): BroadcastMessage
{
return new BroadcastMessage([
'amount' => $this->amount,
'message' => "Your invoice of \${$this->amount} has been paid.",
]);
}
}
// Laravel maps this to the authenticated user's private channel automatically
Echo.private(`App.Models.User.${userId}`)
.notification((notification) => {
if (notification.type === 'App\\Notifications\\InvoicePaid') {
showNotificationBadge(notification.message);
}
});
Production Deployment
When deploying Reverb to production, run it as a supervised process and put Nginx in front of it to handle TLS termination:
[program:reverb]
command=php /var/www/myapp/artisan reverb:start --host=0.0.0.0 --port=8080
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/reverb.log
server {
listen 443 ssl;
server_name myapp.com;
# Proxy WebSocket connections to Reverb
location /app/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_read_timeout 60s;
}
}
Key Takeaways
- Driver-based: Swap between Reverb, Pusher, and Ably by changing one
.envvalue — your event code stays identical - Queue your broadcasts: Use
ShouldBroadcast(queued) overShouldBroadcastNowin production to avoid blocking HTTP responses - Authorize private channels in
routes/channels.php— never skip this step for user-specific data - Use
broadcastAs()to give events short, stable names that won't break if you rename the PHP class - Presence channels give you real-time membership lists with almost no extra code — great for collaborative features
- Notifications integrate natively — add
'broadcast'tovia()and implementtoBroadcast()for instant push notifications
Laravel Broadcasting elegantly bridges your server-side events and your users' browsers. Once the infrastructure is in place — a running Reverb server and a queue worker — adding real-time behavior to any feature is as simple as implementing ShouldBroadcast and calling Echo.private(...).listen(...). The days of polling are behind you.