Skip to content
Realtime

Pusher + Laravel Broadcasting, Including the Unhappy Paths

Events with explicit payloads on a dedicated queue, channel auth done like tenant isolation, Echo on the client — and the reconnect-reconcile pattern that survives tunnels, sleeps and deploys.

4 min read Updated Sep 2, 2026
Pusher + Laravel Broadcasting, Including the Unhappy Paths

Realtime features have a habit of being demanded in the same sentence that dismisses them: "just make the dashboard update live, shouldn't be hard, right?" And with Laravel's broadcasting layer plus Pusher, the happy path genuinely isn't — event to browser in an afternoon. The unhappy paths (auth on private channels, queue interactions, the client reconnecting into silence) are where afternoons go to die. Here's the full wiring, happy and unhappy paths both.

A Laravel event flowing through the broadcast driver and channel auth to an Echo client

The architecture in one breath

Laravel doesn't talk WebSockets itself. Your app fires an event → the broadcast driver POSTs it to Pusher (over HTTPS, usually via a queued job) → Pusher pushes it down persistent WebSocket connections to subscribed browsers → Laravel Echo, the JS client, hands it to your frontend code. Your servers never hold a socket; Pusher holds ~all of them. That's the entire value proposition: connection state, scaling and reconnection are somebody else's pager. (Whether to make them your pager again is the next post.)

Server side: an event that broadcasts

final class OrderStatusUpdated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('merchant.'.$this->order->merchant_id)];
    }

    public function broadcastWith(): array          // explicit payload — ALWAYS
    {
        return [
            'order_id' => $this->order->id,
            'status'   => $this->order->status,
            'total'    => $this->order->total_minor,
        ];
    }

    public function broadcastQueue(): string { return 'broadcasts'; }
}

Two deliberate choices there. broadcastWith() is not optional in my codebases — the default serializes your whole model, which is how internal columns leak to browsers and how a model refactor becomes a frontend incident. Broadcast payloads are a public API; shape them like one. And a dedicated queue: broadcasts share workers with everything else by default, which means a backed-up report queue delays "live" updates by four minutes — the least live thing imaginable. Give realtime its own fast lane and its own lag alert.

Channel auth: where the security actually lives

Public channels are for public data, full stop. Everything else is private/presence, and subscription rights are decided by your server — Echo hits your /broadcasting/auth endpoint, your callback rules:

// routes/channels.php
Broadcast::channel('merchant.{merchantId}', function (User $user, int $merchantId) {
    return $user->merchant_id === (int) $merchantId;      // boolean = allowed?
});

Broadcast::channel('order.{order}', function (User $user, Order $order) {
    return $user->can('view', $order);                    // policies work here too
});

The mistakes I keep auditing out of codebases: channel callbacks that return true "temporarily" (temporary is forever), channel names carrying data the callback never checks (merchant.{id} where the callback ignores $id — everyone authed can hear everyone), and sensitive payloads on public channels because "the channel name is unguessable" (it isn't; it's in the JS bundle). Treat channel design with tenant-isolation seriousness — it's the same problem wearing WebSockets.

Client side: Echo, and the reconnect problem nobody mentions

// bootstrap.js
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: 'eu',
    forceTLS: true,
});

// Somewhere in the dashboard
Echo.private(`merchant.${merchantId}`)
    .listen('OrderStatusUpdated', (e) => orderStore.applyUpdate(e));

Now the part the tutorials skip: WebSockets miss things. Laptops sleep, trains enter tunnels, deploys restart things. Pusher reconnects automatically — but events published during the gap are simply gone. If your UI's correctness depends on receiving every event, you've built on sand. The robust pattern is realtime as an accelerator, polling as the guarantee: on reconnect (and on tab-visibility regain), refetch current state from a plain HTTP endpoint; let sockets merely make the common case instant. Ten extra lines, and the "dashboard was stale for an hour" ticket never gets filed. For chat-like cases where every message matters, add sequence numbers and fetch-missed-since on resume.

Production notes from the scar-tissue file

  • Payload limit is 10KB. Broadcast the fact and the IDs; let clients fetch the fat. (Thin events age better anyway — same rule as internal events.)
  • toOthers() saves the classic double-update: the user who triggered the change gets the optimistic UI, everyone else gets the broadcast.
  • Watch your Pusher bill's real driver: connections, not messages. Every open tab is a connection; an audience of 5,000 dashboards is 5,000+ concurrent connections before anyone sends anything. Model this before the invoice does.
  • Presence channels (who's online) are delightful and slightly eventually-consistent — treat join/leave events as UI garnish, not authorization.

Wired this way — explicit payloads, dedicated queue, honest auth, reconcile-on-reconnect — Pusher integration is genuinely one of the best effort-to-wow ratios in web development. The dashboard updates itself in the demo, the room nods, and nobody knows about the polling safety net underneath. Which is exactly how good engineering should feel.

Next: Reverb, Soketi, or keep paying Pusher? — the self-hosting decision with real numbers. Or bring me the dashboard that should've been live yesterday.

Keep reading

Related articles

Realtime 5 min read

Scaling WebSockets Past One Box

Sockets are tenants, not confetti: the pub/sub backplane, stateless nodes and rolling drains, the reconnect stampede and its jittered cure, missed-message recovery by data shape, and the capacity cliffs to test first.