DEV Community

Cover image for Real-Time Laravel APIs Using Laravel Reverb (WebSockets)
Patoliya Infotech
Patoliya Infotech

Posted on

Real-Time Laravel APIs Using Laravel Reverb (WebSockets)

Modern customers anticipate instant updates—new chat messages that don't need to be refreshed, dashboards that keep up with reality, and notifications that appear when anything changes. Real-time UX is no longer a "nice to have"; it is required for SaaS, markets, analytics, collaboration tools, and other applications.

Laravel Reverb integrates a first-party, high-performance WebSocket server into your Laravel application. It enables you to broadcast Laravel events via WebSockets and consume them in the browser using Laravel Echo—no vendor lock-in, no per-message fees, and tight framework integration.

Below is a simple, beginner-friendly guide to creating real-time APIs in Laravel using Reverb, including setup, code samples for notifications, chat, and dashboards, recommended practices, and a direct comparison to Pusher/Echo.

What we’ll build (and why)

By the end, you’ll be able to:

  • Spin up Reverb and connect a frontend via Laravel Echo.
  • Broadcast server events (orders updating, metrics changing).
  • Push live notifications, a realtime chat, and a live dashboard.
  • Avoid common pitfalls (origins, queues, SSL, scaling).

Prerequisites

  • Laravel 11/12 project
  • Node + Vite (default in new Laravel apps)
  • Redis (recommended for scaling later)

Install & configure Laravel Reverb

Quick install

php artisan install:broadcasting --reverb

This scaffolds broadcasting, installs Reverb + Echo, and injects sensible .env variables.
Prefer manual?

composer require laravel/reverb && php artisan reverb:install

also works.

PHP is still highly relevant for building scalable, dynamic apps—see why PHP is a top choice for e-commerce development

Essential environment variables

Reverb identifies the app with credentials and needs host/port info:

BROADCAST_CONNECTION=reverb

REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret

-  Where Laravel sends broadcast messages (public entrypoint or proxy)
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https

-  Where the Reverb server actually binds (internal)
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
Enter fullscreen mode Exit fullscreen mode

Important distinction: - REVERB_SERVER_HOST/PORT controls the Reverb process binding; REVERB_HOST/PORT tells Laravel where to send messages (often your public hostname through Nginx/ALB).

Allowed Origins (CORS for sockets)

Restrict which frontends may connect:

// config/reverb.php
'apps' => [[
  'app_id' => env('REVERB_APP_ID'),
  'allowed_origins' => ['https://app.example.com'], // or ['*'] during local dev
]],
Enter fullscreen mode Exit fullscreen mode

Requests from other origins will be rejected.

Start Reverb

php artisan reverb:start
# options:
# php artisan reverb:start --host=127.0.0.1 --port=9000
Enter fullscreen mode Exit fullscreen mode

The default is 0.0.0.0:8080. Use php artisan reverb:restart after code changes—Reverb is a long-running process.
Using Herd/Valet locally with HTTPS? You can pass a hostname or a TLS cert so Reverb serves wss:// directly.

Wire up the frontend with Laravel Echo

Reverb uses the Pusher protocol, so the browser uses pusher-js under the hood with Echo’s reverb broadcaster.
Install (already done if you ran the installer):
npm i -D laravel-echo pusher-js
Bootstrap Echo (e.g., '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'],
})
`

The reverb broadcaster requires laravel-echo v1.16.0+.
Build assets:
npm run build # or npm run dev

Broadcasting 101 (server side)

Create a broadcastable event

`
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\PrivateChannel; // for private channels
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;

class OrderShipmentStatusUpdated implements ShouldBroadcast
{
use SerializesModels;

public function __construct(
    public int $orderId,
    public string $status,
) {}

public function broadcastOn(): Channel|array
{
    return new PrivateChannel("orders.{$this->orderId}");
}

public function broadcastAs(): string
{
    return 'OrderShipmentStatusUpdated';
}

public function broadcastWith(): array
{
    return ['orderId' => $this->orderId, 'status' => $this->status];
}
Enter fullscreen mode Exit fullscreen mode

}
`

Authorize channels

routes/channels.php controls who can subscribe:

`
use Illuminate\Support\Facades\Broadcast;
use App\Models\User;

Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
// Only owners (or staff) can listen
return $user->orders()->whereKey($orderId)->exists();
});
`

Emit the event

use App\Events\OrderShipmentStatusUpdated;
event(new OrderShipmentStatusUpdated($orderId, 'shipped'));

Don’t forget your queue worker. Broadcasting is queued so it doesn’t slow down responses. Make sure a worker is running (e.g., php artisan queue:work).

Receive events in the browser


// Private channel requires auth (handled by Laravel)
Echo.private(
orders.${orderId})
.listen('.OrderShipmentStatusUpdated', (e) => {
console.log(e.status) // 'shipped'
})

The leading dot '.EventName' is used when you set a custom broadcastAs.

Real-time feature recipes

Live notifications

Server: use Laravel Notifications with the broadcast channel.

`
use Illuminate\Notifications\Notification;

class NewMessageNotification extends Notification
{
public function via($notifiable): array
{
return ['database', 'broadcast'];
}

public function toBroadcast($notifiable): array
{
    return ['message' => 'You have a new message!'];
}
Enter fullscreen mode Exit fullscreen mode

}
`

Client:


Echo.private(
App.Models.User.${userId})
.notification((n) => {
// render toast/badge/center
console.log(n.message)
})

Notifications over Echo use the user’s private channel automatically.

Realtime chat (private & presence)

Authorize a presence channel to know who’s online and show “typing…”:


// routes/channels.php
Broadcast::channel('chat.{roomId}', function ($user, int $roomId) {
// return user info to other members
return ['id' => $user->id, 'name' => $user->name];
});

Frontend:


// Presence channel
const channel = Echo.join(
chat.${roomId}`)
.here(users => renderOnline(users))
.joining(user => addOnline(user))
.leaving(user => removeOnline(user))

// Typing indicator using client events (no round-trip)
channel.whisper('typing', { name: me.name })
channel.listenForWhisper('typing', (e) => showTyping(e.name))
`

(With Pusher, enabling client events in your Pusher app is required; Reverb handles whispering with Echo locally.)
Broadcast messages from the server:

`
class MessagePosted implements ShouldBroadcast
{
public function __construct(public int $roomId, public array $message) {}

public function broadcastOn(): array
{
    return [new \Illuminate\Broadcasting\PresenceChannel("chat.{$this->roomId}")];
}
Enter fullscreen mode Exit fullscreen mode

}
`

Listen in the browser:


Echo.join(
chat.${roomId})
.listen('MessagePosted', (e) =>
appendMessage(e.message))

Streaming dashboard metrics

Backend:

`
class MetricUpdated implements ShouldBroadcast
{
public function __construct(public string $key, public float $value) {}

public function broadcastOn(): array
{
    return [new \Illuminate\Broadcasting\Channel('metrics')]; // public
}
Enter fullscreen mode Exit fullscreen mode

}
`

Client:


Echo.channel('metrics')
.listen('MetricUpdated', ({ key, value }) => updateChart(key, value))

Learn more about PHP & It's Trending Frameworks

Production checklist & best practices

Run Reverb under a process manager Supervisor/systemd) and increase minfds so it can open enough file descriptors for connections.
[supervisord]
minfds=10000

  • Reverse proxy & SSL. Terminate TLS at Nginx/ALB and proxy to REVERB_SERVER_HOST:REVERB_SERVER_PORT. Use wss:// in production.
  • Allowed origins. Lock down allowed_origins in config/reverb.php to your app domains.
  • Queues are mandatory. Keep a reliable queue worker online (Horizon is great) so broadcasts are sent promptly.
  • Don’t leak secrets. Treat WebSocket payloads like API responses. Never broadcast sensitive data; use private/presence channels with robust auth.
  • Shape payloads intentionally. Use broadcastWith() to send just what the UI needs; use broadcastAs() to control event names.
  • Restart on deploy. Reverb is long-running. Call php artisan reverb:restart during deployments for zero-downtime restarts.
  • Scale horizontally with Redis. Set REVERB_SCALING_ENABLED=true and run Reverb on multiple instances behind a load balancer; Reverb uses Redis pub/sub to fan out messages.

Common pitfalls (and how to fix them)

  • “Connected, but no events arrive.” The queue worker isn’t running or is failing. Check queue:work logs.
  • CORS/Origin errors. Your frontend origin isn’t in allowed_origins. Add the domain or * for local dev.
  • Port/host confusion. REVERB_SERVER_* is where the process binds; REVERB_HOST/PORT is the public address Echo connects to.
  • Mixed content (WS vs WSS). On HTTPS sites you must use secure WebSockets (wss://) or the browser will block the connection.
  • Event name mismatch. If you set broadcastAs(), listen with a leading dot: .YourEventName.

Reverb vs Pusher vs Echo (what’s the difference?)

Tool What it is Pros Cons Best for
Laravel Reverb First-party WebSocket server you host No vendor fees; deep Laravel integration; easy install; Redis-backed horizontal scaling You manage infrastructure and uptime Apps wanting control & predictable cost
Pusher Channels Managed WebSocket service Zero ops; global infrastructure; great tooling Usage-based cost; external dependency Teams prioritizing speed of launch
Ably (Pusher-compatible) Managed realtime platform Global low-latency; rich features Cost; external dependency High-scale global apps
Laravel Echo Client library, not a server Unified API in JS (works with Reverb/Pusher/Ably) N/A—used with one of the above Every Laravel frontend

Reverb and Pusher/Ably are server/broker choices. Echo is the browser client you’ll use with any of them. Reverb uses the Pusher protocol, so Echo’s Pusher adapter works seamlessly with the reverb broadcaster.

Advanced: presence, client events, and model broadcasting

  • Presence channels return member info in the channel auth callback and let you show “who’s online” and online counts.
  • Client events (aka “whispers”) are great for ephemeral UX like typing indicators—sent peer-to-peer via the channel without hitting your Laravel backend.
  • Model broadcasting lets you stream changes to Eloquent models with Echo hooks (useEchoModel) for a delightful DX in React/Vue.

Horizontal scaling in a sentence

Flip REVERB_SCALING_ENABLED=true, point all Reverb instances at the same Redis, run reverb:start on multiple servers, put them behind a load balancer. Done.

Conclusion

Laravel Reverb makes real-time APIs feel like Laravel: events you already use become live updates in the browser; Echo provides a clean, chainable API; and you can self-host or scale out with Redis as demand grows. With the patterns mentioned above—notifications, chat, and live dashboards—you can provide the kind of UX that people increasingly expect.

Top comments (0)