Real-time WebSockets in Laravel: Reverb, Pusher, or Vask

You have a Laravel app and you want real-time. A notifications drawer that updates without a page refresh. A presence indicator on a document. A live dashboard. A typing indicator in a chat surface. The pattern fits broadcasts and channels, and the question is which substrate carries them.

The good news in 2026 is that the Laravel ecosystem gives you three honest options. Reverb is the official Laravel path. Pusher and Vask use the Pusher Channels protocol and the Pusher-compatible SDK ecosystem. The decision is mostly operating model and bill shape.

This page is the honest answer. Read the three options, find the one that matches the shape of your project, and stop there.

Get started

Realtime made simple.

Free Tier: 500K broadcasts/mo and 100 concurrent across unlimited apps. $10/mo when you outgrow it.

Option 1: Laravel Reverb (first-party, in-process)

Laravel Reverb is the WebSocket server shipped by the Laravel team. Current Laravel docs install broadcasting with php artisan install:broadcasting, configure Reverb-specific environment variables, and run Reverb as a long-lived process alongside your application. Use the official Reverb docs as the source of truth for new Laravel scaffolds.

Official docs: Laravel Broadcasting and Laravel Reverb.

What it gives you:

  • A first-party, free, open-source WebSocket server maintained by the people who maintain the framework.
  • Pusher Channels protocol on the wire, so every existing laravel-echo example on the internet applies unchanged.
  • Zero per-message billing. The cost is your server, which you are already paying for.
  • The full power of being in-process with your application: presence channels, private channels, channel auth, all backed by your existing User model and your existing auth pipeline.

When it is the right call:

  • Low to medium traffic, single-region apps. If most of your users are inside one continent and a single VPS or small cluster can hold the concurrency, the latency story is fine.
  • Teams comfortable running a long-lived process and the supervision around it: a supervisor or systemd unit, a restart strategy, a deploy story that does not drop active WebSocket connections.
  • Projects where the real-time feature is one part of a larger Laravel app, not the product itself, and you want the smaller stack with fewer vendors.
  • Local development for any Laravel app, including apps that run a hosted service in production. Reverb on your laptop, hosted Pusher protocol in production, same wire format.

When you'd reach for something else:

  • You want to ship multi-region without operating a multi-region WebSocket fleet yourself.
  • You do not want to be on call for the WebSocket process. (Same code, different on-call shape.)
  • Your concurrency or broadcast volume has grown to where a single in-process server is becoming a scaling project, not a deploy step.

If Reverb fits, use Reverb. The remainder of this page is for the cases where it does not, or for teams already on a hosted Pusher service evaluating where to go next.

Option 2: Hosted Pusher Channels (the incumbent)

Pusher Channels is the original hosted Pusher-protocol service. It is the reason the Pusher protocol exists in the first place. The SDKs are mature, the documentation is good, and for years it was the obvious choice for a Laravel app that did not want to run its own WebSocket server.

What it gives you:

  • A fully managed, hosted Pusher-protocol service. You point PUSHER_HOST at the Pusher cluster you pick at app creation time and it works.
  • A mature ecosystem: pusher-js, laravel-echo, pusher-http-php, debug consoles, official SDKs in every working language.
  • Presence channels, private channels, channel auth all work the standard way.

The thing nobody warns you about until you cross a threshold:

The bill scales with the number of subscribers on each channel, not with the number of broadcasts you publish. The category has a name for this workload (fan-out) and the surcharge attached to it is the fan-out tax. One broadcast to a channel with 100 subscribers counts as 101 billable messages on the standard pricing model. Run a presence channel with 1,000 connected users and a single broadcast becomes 1,001 billable messages. Ship a typing indicator over a busy room and the math compounds quickly.

This is not a quirk. It is the priced unit of the product. The model dates from a previous era of real-time when broadcasts were rare and connections were the scarce resource. Modern apps fan out constantly; the model has not caught up.

When it is still a reasonable call:

  • You are already on Pusher, the bill is fine for your traffic shape, and the integration is tuned. Switching costs you migration time. If the math does not justify it, do not switch.
  • Your traffic profile happens to be broadcast-light per subscriber (very large channels, very few publishes), which the per-message model handles fine.

If your fan-out factor is high or growing, the calculator on /compare/pusher-vs-vask shows what the per-fan-out multiplier is doing to your specific bill.

Option 3: Vask (hosted Pusher protocol on Cloudflare's edge)

Vask is the option that did not exist a few years ago and now does. A managed Pusher-protocol WebSocket service running on Cloudflare's edge network, billed per broadcast (not per fan-out copy), with connections terminating at the closest of 330+ edge cities.

What it gives you:

  • The Pusher Channels protocol on the wire, kept on purpose. Your laravel-echo client, your pusher-php-server backend, your Broadcast events, your channel auth callback, your private and presence channel naming: all unchanged.
  • Cloudflare's edge as the substrate. Connections terminate on Cloudflare's edge network, not a single home region.
  • Broadcast-priced billing. One broadcast to a channel is one broadcast on the bill, regardless of how many subscribers are on it. No per-fan-out multiplier. No presence-channel surcharge. No surprise line item when the typing indicator ships.
  • Drop-in compatibility. If you are already on a Pusher-protocol service today, the cutover is a host and credential change in .env plus a restart.

When it is the right call:

  • You are on hosted Pusher today, your bill is dominated by fan-out, and the math has stopped working. The receipt is mechanical: same protocol, broadcast-priced billing.
  • You want multi-region edge presence without operating a multi-region WebSocket fleet yourself.
  • You want hosted Pusher protocol without running a long-lived process and the supervision around it.

When it is not the right call:

  • Reverb fits, and the answer is Reverb. (Repeated because it matters.)
  • You are below the free tier on your current service and the bill is zero. Switch when the bill shows up, not before.
  • Your real-time feature does not fit the Pusher Channels protocol (WebRTC media routing, MQTT-shaped IoT messaging, a proprietary wire format). The protocol-compatibility wedge is not a wedge for you.

If you are migrating off hosted Pusher specifically, the step-by-step Laravel recipe is at /migrate/pusher-to-vask-laravel. The head-to-head comparison with calculator is at /compare/pusher-vs-vask.

What the Laravel code actually looks like

For Pusher and Vask, the application code is the same because both speak the Pusher Channels protocol. Reverb is the Laravel-first path and current Laravel scaffolds use Reverb-specific env names, so do not blindly copy a Pusher config into a Reverb app.

Client setup (laravel-echo)

For Vask, the Echo client uses Pusher-compatible settings:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
    wsHost: import.meta.env.VITE_PUSHER_HOST,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});

For Reverb, follow Laravel's generated Echo config instead of renaming these variables by hand.

Server config (config/broadcasting.php)

'connections' => [
    'pusher' => [
        'driver' => 'pusher',
        'key' => env('PUSHER_APP_KEY'),
        'secret' => env('PUSHER_APP_SECRET'),
        'app_id' => env('PUSHER_APP_ID'),
        'options' => [
            'cluster' => env('PUSHER_APP_CLUSTER'),
            'host' => env('PUSHER_HOST'),
            'port' => env('PUSHER_PORT', 443),
            'scheme' => env('PUSHER_SCHEME', 'https'),
            'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
        ],
    ],
],

For Vask, the important piece is the explicit host. Vask gives you a key and secret; if the Laravel/Pusher config requires PUSHER_APP_ID, use the Vask key for that field. Vask does not route by cluster.

Broadcast event

app/Events/OrderShipped.php:

<?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 OrderShipped implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Order $order)
    {
    }

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

    public function broadcastAs(): string
    {
        return 'order.shipped';
    }
}

broadcast(new OrderShipped($order)) fires it. Same code, all three options.

Presence channel with auth callback

routes/channels.php:

<?php

use App\Models\Document;
use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('document.{documentId}', function ($user, int $documentId) {
    $document = Document::find($documentId);

    if (! $document || ! $user->can('view', $document)) {
        return false;
    }

    return [
        'id' => $user->id,
        'name' => $user->name,
        'avatar_url' => $user->avatar_url,
    ];
});

The closure returns the array shape that becomes the presence member payload. The /broadcasting/auth route handles the signed-request validation. Same code, all three options. (For the term itself, see /glossary/channel-auth and /glossary/presence-channel.)

Client subscription

resources/js/document.js:

window.Echo.join(`document.${documentId}`)
    .here((members) => {
        // initial list of users present in the document
    })
    .joining((member) => {
        // a user just joined
    })
    .leaving((member) => {
        // a user just left
    })
    .listen('.cursor.moved', (event) => {
        // a presence member broadcasted their cursor
    });

.here(), .joining(), .leaving() are presence-channel lifecycle events. .listen() subscribes to a broadcast on the channel. Same code, all three options.

Picking between the three: a flowchart

Do you have a Laravel app?
                              |
                            YES
                              |
       Is Reverb in-process a viable operating model?
       (low-medium traffic, single region, ops capacity)
                              |
                ┌─────────────┴─────────────┐
               YES                          NO
                |                            |
              REVERB.            Are you OK with the
              Done.              per-fan-out billing
                                 model on hosted Pusher?
                                          |
                              ┌───────────┴───────────┐
                             YES                       NO
                              |                         |
                           PUSHER.                   VASK.
                           Done.        (or another  Done.
                                         hosted
                                         Pusher-protocol
                                         service)

Three honest answers, all of which use the same laravel-echo client and the same Broadcast event surface. The decision is operating model and bill shape, not application code.

When NOT to switch from Reverb

This is worth saying out loud because the temptation when reading a vendor page is to assume the vendor's option is always the right one. It is not. If you are running Reverb in-process, the traffic fits, and the ops are fine, stay on Reverb. We will not pretend otherwise.

Vask is the hosted Pusher-protocol option for teams that want the protocol without operating their own WebSocket server, or that need edge delivery beyond what an in-process server can give them. It is not the "next step" from Reverb. It is a different shape of answer for a different shape of project.

The cases where moving off Reverb actually makes sense:

  • You have outgrown a single-region operating model and your users are in multiple continents.
  • You no longer want to run a long-lived WebSocket process alongside your app and the supervision around it.
  • Your concurrency is high enough that horizontal scaling of Reverb is becoming a project rather than a configuration knob.

None of those are about Reverb being a bad fit. They are about the project changing shape. If your project has not changed shape, do not migrate.

What's the simplest way to add WebSockets to a Laravel app in 2026?
For a new Laravel app that can run its own WebSocket process, start with Laravel Reverb and the official Laravel broadcasting docs. Reverb is the first-party path. Vask is the hosted Pusher-protocol option when you want the Pusher SDK ecosystem, edge delivery, or do not want to operate the socket process yourself.
When does a hosted WebSocket service make sense over Reverb?
Three scenarios. First, you do not want to operate a long-running WebSocket process and the associated supervision, restarts, and scaling story. Second, your users are spread across multiple regions and you want connections to terminate at the closest edge rather than a single home region. Third, your concurrency or broadcast volume has grown to where horizontal scaling of a single in-process server is becoming a project. Outside those cases, in-process Reverb is the smaller, simpler answer.
How is Vask different from Pusher in a Laravel app?
Both speak the Pusher Channels protocol on the wire, so your laravel-echo client and your pusher-php-server backend work identically against either. The differences are where the connections terminate and how the bill is calculated: Vask bills per broadcast, while per-message models multiply by subscriber count. Use the calculator for your own fan-out factor instead of trusting a generic average.
Do private and presence channels work the same way across all three?
Yes. Private channels (`private-*` prefix), presence channels (`presence-*` prefix), and channel auth (the `/broadcasting/auth` callback in Laravel) are part of the Pusher Channels protocol. Reverb, Pusher, and Vask all implement the protocol, so the same Echo client code, the same BroadcastServiceProvider channel definitions, and the same channel auth callback work against any of them. Switching is a credentials change in `.env`, not a code change.
What if I'm using Laravel Echo with Socket.IO instead of pusher-js?
Echo supports a `socket.io` broadcaster as an alternative to the `pusher` one. That broadcaster does not speak the Pusher protocol; it speaks the Socket.IO protocol. Reverb, Pusher, and Vask all assume the Pusher protocol broadcaster. If you are on the Socket.IO side, you would either switch your Echo config to `pusher-js` (the more common modern setup) or you would not be a candidate for any of the three options on this page.
Can I run Reverb locally and Vask in production?
Yes, but keep the configs explicit. Use the official Reverb environment variables for local Reverb, and use Vask's Pusher-compatible credentials in production. Vask does not issue a separate app ID or cluster; if a Pusher-compatible SDK requires app_id, set it to the Vask key.

Get going

If you want Reverb, the Laravel Reverb documentation is the canonical reference. If you want hosted Pusher protocol on Cloudflare's edge with broadcast-priced billing, the recipe below gets you there.

Hosted Pusher protocol on the edge

Same Laravel code. Different substrate.

Use Vask credentials, keep laravel-echo and your Broadcast events, and point the Pusher-compatible config at wss.vask.dev.