Broadcasting & Real-Time

Some updates shouldn't wait for the user to reload the page — an order moving to "shipped," a chat message arriving, a long-running job finishing. The naive fix is polling: have the browser ask "anything new?" every few seconds. Broadcasting is Laravel's alternative — instead of the browser repeatedly asking, the server pushes the update out the instant it happens, over a persistent WebSocket connection.

Scope of this page: this is an introduction to the concept and the pieces involved, not a full production setup guide — getting a broadcasting driver and WebSocket server actually running involves infrastructure decisions (which service, which server, TLS, scaling) that go beyond what a single lesson can cover.

Why Broadcasting Instead of Polling?

PollingBroadcasting
How it worksBrowser repeatedly asks the server "anything new?" on a timerServer pushes the update the moment it happens
LatencyUp to one full polling interval lateNear-instant
Server loadConstant requests, even when nothing changedOne message, sent only when something actually happens

The Architecture, End to End

Broadcasting always involves the same three pieces working together, in this order:

  1. Your Laravel application dispatches an event (the same events covered in Events & Listeners) that's marked as broadcastable.
  2. A broadcasting driver — a hosted service like Pusher, or a self-hosted WebSocket server like Laravel Reverb or Soketi — receives that event from your server and fans it out.
  3. Every browser currently subscribed to the relevant channel, holding open a persistent WebSocket connection, receives the event instantly and reacts to it with JavaScript — no page reload, no polling.

Making an Event Broadcastable

An ordinary Laravel event stays entirely server-side. To let it reach the browser, implement the ShouldBroadcast interface and add a broadcastOn() method describing which channel it should be sent over:

<?php
// app/Events/OrderShipped.php
namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderShipped implements ShouldBroadcast
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order)
    {
    }

    public function broadcastOn()
    {
        return new PrivateChannel('orders.'.$this->order->id);
    }
}

Dispatching it is identical to any other event — Laravel notices ShouldBroadcast and automatically routes it to your configured broadcasting driver in addition to firing any normal listeners:

<?php
event(new OrderShipped($order));

Public, Private, and Presence Channels

Not every broadcast should be visible to every visitor, so channels come in three flavors:

Channel typeWho can listenExample
Public (Channel)Anyone — no authorization check at allA site-wide "new article published" ticker
Private (PrivateChannel)Only authenticated users your app explicitly authorizesA specific customer's own order-tracking updates
Presence (PresenceChannel)Same authorization as private, plus Laravel tracks exactly who is currently subscribedA live chat room showing "who's online right now"

Private and presence channels need an authorization rule so Laravel knows who's allowed to subscribe, defined in routes/channels.php:

<?php
// routes/channels.php
use App\Models\Order;

Broadcast::channel('orders.{orderId}', function ($user, $orderId) {
    return $user->id === Order::findOrFail($orderId)->user_id;
});

Only when this callback returns true does Laravel let that specific user's browser subscribe to orders.42 — anyone else's subscription attempt is rejected.

Laravel Echo: Listening on the Frontend

Laravel Echo is the JavaScript library that pairs with the backend pieces above — it opens and manages the WebSocket connection and gives you a clean, event-driven API for subscribing to channels in the browser:

// resources/js/app.js (conceptual — full Echo setup is its own topic)
Echo.private(`orders.${orderId}`)
    .listen('OrderShipped', (event) => {
        console.log(event.order);
        // update the page — e.g. change a status badge to "Shipped"
    });

For a public channel the call is simply Echo.channel('orders').listen('OrderShipped', ...) — no authorization involved. Presence channels use Echo.join() instead, which adds .here(), .joining(), and .leaving() callbacks for tracking who's currently connected.

A Realistic Use Case: Live Order Tracking

Put together, this is exactly how a customer's order-tracking page can update itself with zero polling: the customer's browser subscribes to their own private orders.{id} channel when the page loads; sometime later, a warehouse process marks the order shipped and dispatches the OrderShipped event; the broadcasting driver delivers it over the open WebSocket connection; Echo's .listen() callback fires in the browser and swaps the status badge from "Processing" to "Shipped" — all without the customer ever refreshing the page.

Next up: With the app now capable of reaching users in real time, the next lesson steps back to harden the application itself — head to Security Best Practices.