Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Reverb Laravel Package

laravel/reverb

Laravel Reverb is a first-party WebSocket server that brings real-time communication to Laravel apps. It integrates with Laravel broadcasting, supports horizontal scaling via Redis, and can replace external services like Pusher.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and scaffold the config

composer require laravel/reverb
php artisan reverb:install

reverb:install publishes config/reverb.php, generates an app ID/key/secret, and writes the env vars it needs. It also flips BROADCAST_CONNECTION=reverb and adds matching VITE_REVERB_* variables for your frontend build. Review the diff it produces — if you already had a pusher broadcasting connection configured, you now have two ways of talking to the same protocol and you need to pick one deliberately.

2. Look at these three places first

  • config/reverb.php — apps (credentials, capacity, allowed_origins, ping_interval, activity_timeout, max_message_size, TLS options) and servers (host, port 8080, path, max_request_size). This file is the package's API surface; almost everything you'll want to tune lives here.
  • config/broadcasting.php → the reverb connection. This is how your Laravel app publishes events (it speaks the Pusher HTTP API to the Reverb process on an internal host/port).
  • https://laravel.com/docs/reverb — the README is intentionally just a pointer; the real docs live on the Laravel site, alongside the Broadcasting docs which cover the server-side half.

3. Start the server and connect a client

php artisan reverb:start --debug
// resources/js/bootstrap.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'],
});

4. First use case: an order status feed

Reverb is a drop-in replacement for the Pusher driver, so your first real feature requires no Reverb-specific code at all — just standard broadcasting:

class OrderStatusUpdated implements ShouldBroadcast
{
    public function __construct(public Order $order) {}

    public function broadcastOn(): array
    {
        return [new PrivateChannel('orders.'.$this->order->user_id)];
    }
}
Echo.private(`orders.${userId}`)
    .listen('OrderStatusUpdated', (e) => updateRow(e.order));

If that round-trip works, everything else in Reverb is configuration and operations. Spend your first hour there, not on internals.

Implementation Patterns

Two connections, two audiences. Your app publishes over HTTP to REVERB_HOST:REVERB_PORT using the app credentials; browsers connect over WebSocket to the public host. In any multi-container or proxied setup these are different values:

# used by the PHP app to publish
REVERB_HOST=reverb.internal
REVERB_PORT=8080
REVERB_SCHEME=http

# used by the browser to subscribe
VITE_REVERB_HOST=ws.example.com
VITE_REVERB_PORT=443
VITE_REVERB_SCHEME=https

Presence channels for "who's here". Reverb implements the Pusher presence protocol faithfully, including here/joining/leaving events:

Echo.join(`room.${roomId}`)
    .here(users => setMembers(users))
    .joining(user => addMember(user))
    .leaving(user => removeMember(user));

Auth for private/presence channels goes through Laravel's /broadcasting/auth with your channels.php definitions — for token-based (Sanctum/API) clients you'll need Broadcast::routes(['middleware' => ['auth:sanctum']]) and a custom Echo authorizer.

toOthers() requires the socket ID to survive the round trip. Echo's whisper-free equivalent is broadcast(new Event(...))->toOthers(), which sends the caller's socket ID with the publish request so Reverb skips that one connection. In a scaled deployment this used to be fragile; as of v1.11.0 the socket ID is carried through pub/sub, so toOthers() works across nodes. Verify your Echo connector is passing socket_id (it does by default) if you see the originator receiving its own event.

Client "whisper" events are constrained. Per the protocol, client events only work on private/presence channels, and since v1.8.0 Reverb only relays them to members of that channel. Since v1.6.1/v1.4.8, data may be empty and JSON-string payloads are decoded. Keep whispers for typing indicators and cursor positions; anything that must be trusted should go through your app.

Scale out with Redis, not with sticky sessions. Set:

REVERB_SCALING_ENABLED=true
REVERB_SCALING_CHANNEL=reverb

Reverb uses Redis pub/sub to fan published messages across nodes, so any node can publish to a subscriber on any other node. Load-balance WebSocket connections freely; just make sure Redis is reachable from every node and that reconnection is handled (that logic has been hardened since v1.5.0). For per-app limits, apps.<app>.capacity (v1.6.0) gives you a hard ceiling on concurrent connections per app — a cheap circuit breaker when a runaway client loops reconnects.

Operate it like a queue worker. Reverb is a long-running process; run it under Supervisor or systemd, one process per core/container, and log to stdout so it lands in your existing log pipeline.

[program:reverb]
command=php /var/www/app/artisan reverb:start
autostart=true
autorestart=true
numprocs=1

Behind Nginx, the only thing that matters is the upgrade hop:

location / {
    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 600s;
}

Terminate TLS at the proxy and set REVERB_SCHEME/VITE_REVERB_SCHEME to https — or run Reverb with its own TLS options and point both at it. If you must mount Reverb under a subpath, use the path server option (v1.4.7) and set REVERB_PATH/Echo's wsPath accordingly; note the v1.8.1 fix for signature verification with a custom path, and that as of v1.10.0 the server path is included in the generated broadcasting connection config.

Observability hooks. Reverb dispatches server-side events you can listen to in a normal service provider — connection pruned (v1.1.0), channel created/removed (v1.2.0). Wire these to metrics for channel churn and unexpected disconnects, which are the two signals that predict incidents. For ad-hoc inspection, Laravel\Reverb\Facades\Reverb::connections() and Reverb::channels() (with a channel filter) let you build an admin "who is connected right now" page, and reverb:start --debug prints every frame in real time.

Rate limiting (v1.9.0). Reverb's own rate limiting is configured in config/reverb.php, separate from Laravel's HTTP rate limiters. Apply it when you have many small clients that can reconnect aggressively; combine with capacity for defense in depth.

Testing. Unit-test the events, not the socket: Event::fake() / Event::assertDispatched on your ShouldBroadcast event covers the channel routing decision. To assert the payload actually published, fake the outgoing HTTP call (Http::fake(['*/apps/*/events' => Http::response([])])) or run with BROADCAST_CONNECTION=null in phpunit.xml so broadcasting becomes a no-op. Reserve a real reverb:start --debug run for manual/E2E verification.

Extension points before forking. Application credential lookup is behind Laravel\Reverb\Contracts\ApplicationProvider, so a multi-tenant setup can resolve apps from the database instead of env. Container bindings around the Pusher router use singletonIf/bindIf (v1.10.0), so your own bindings in a service provider win — you don't need to override the package. And allowed_classes (v1.7.0) is the knob for hardening deserialization of incoming payloads.

Gotchas and Tips

php artisan config:cache is the #1 source of "my env changes did nothing". Reverb reads config/reverb.php and the broadcasting connection from cached config; after editing REVERB_APP_KEY, allowed_origins, or the port, re-run php artisan config:cache (or config:clear) and restart the server process. reverb:install writing env vars does not invalidate an existing cache.

The long-running process does not reload your code. Deploying new listeners or channel auth logic without restarting leaves you running yesterday's PHP. Add the package's restart command to every deploy — it signals the running process to restart cleanly (and handles restarting gracefully rather than dropping sockets mid-cutover). Never rely on autorestart in Supervisor to catch this.

"Connection refused" from PHP but works in the browser means REVERB_HOST is pointing at the public hostname (or 127.0.0.1 from a different container). Publishing is server-to-server over HTTP; set the internal service name and internal port, and remember REVERB_PORT defaults differ between http (80/8080) and https (443).

WebSocket fails with no server-side log — check CORS/origins first. apps.<app>.allowed_origins must contain the exact browser origin including scheme and port; since v1.1.0 wildcards are supported, which is great for local dev (*) and a liability in production. Browser console will show a 403 on the handshake; --debug will show the rejected origin.

Presence/private subscriptions returning 403 while public channels work is almost always auth, not Reverb: session cookie not sent cross-domain (needs withCredentials), auth endpoint behind CSRF without the token, or Echo pointed at /broadcasting/auth under a different hostname than the app.

Custom path breaks signatures. If you mount Reverb under a prefix and publish with a hand-rolled HTTP client rather than the Pusher SDK, you must sign the exact path you request. v1.8.1 fixed this inside the package; anything you wrote yourself needs the same treatment. Since v1.10.2 the signature comparison is timing-safe, so don't reimplement it.

No message persistence, no replay. Reverb is a relay. A client that reconnects has missed everything. Design every channel listener to re-fetch authoritative state on connected/state_change (or on Echo reconnect callbacks) and treat broadcasts as deltas. This is the gotcha that bites people migrating from polling.

Client events are not trusted and not persisted. Whisper events on public channels are rejected outright, and since v1.8.0 non-members can't be targeted. Also, the "data: empty" crash from v1.1.0 and the JSON decode changes in v1.6.1 are reminders: validate everything clients send, and pin a recent patch release.

Watch memory on long-lived processes. A memory leak was fixed in v1.7.0 and the connections() merge on the pub/sub hot path was de-quadratified in v1.10.1 — both mean older 1.x pins degrade badly at scale. Keep Reverb current, set activity_timeout/ping_interval (v1.3.0) to reap dead connections rather than accumulating them, and prefer control frames for ping/pong (v1.4.2) where clients support it.

Rate limiting and capacity are per-app, not per-IP. Enable rate limiting (v1.9.0) and a capacity ceiling if you accept anonymous connections; otherwise one misbehaving tab loop can exhaust connection slots for everyone.

TLS locally. Herd/Valet provide certificates for Reverb on recent versions (Herd gained Windows certificate support in v1.4.4); otherwise run reverb:start --host=0.0.0.0 and let Nginx/Caddy terminate TLS. Mixing http publish with wss subscribe is fine — they're different legs — but a forceTLS: true client pointed at a ws:// port will fail silently in the console.

Debugging workflow that actually works: run reverb:start --debug locally to see raw protocol frames, use Reverb::channels('presence-room.*') in php artisan tinker against a scaled cluster to confirm subscription propagation, and Http::fake around a failing test to confirm whether the publish request was even attempted. Most "Reverb is broken" reports are one of: config cache, wrong host leg, origin rejection, or missing restart.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo