laravel 6 min read

Real-Time Laravel: WebSockets, Broadcasting, and Laravel Echo for Startups

Add real-time features — live notifications, collaborative editing indicators, chat, dashboard counters — to your Laravel SaaS using Laravel Broadcasting, Pusher or Soketi, and Laravel Echo. No Node.js server required.

G
Gurpreet Singh
March 10, 2026

Why Real-Time Features Differentiate Your SaaS

Real-time UI is no longer a premium feature — users expect it. When a team member updates a record, other users should see the change immediately. When a background job completes, the UI should update without requiring a page refresh. When a new message arrives, it should appear instantly.

These interactions feel like magic to end users, and they are one of the clearest ways to make your SaaS feel polished and modern compared to competitors built on older architectures. The good news: Laravel's broadcasting system makes real-time features remarkably straightforward to add — you do not need a separate Node.js server, a different programming model, or specialist knowledge.

This guide covers the full stack: Laravel Broadcasting on the server side, your choice of WebSocket provider (Pusher managed or Soketi self-hosted), and Laravel Echo on the client.

How Laravel Broadcasting Works

The mental model is simple: your Laravel application broadcasts events onto named channels. Clients subscribe to those channels via WebSockets. When an event is broadcast, every subscribed client receives it in real time.

Channels come in three types:

  • Public channels — anyone can subscribe, no authentication required. Good for public activity feeds or counters.
  • Private channels — subscription requires authentication. Good for user-specific notifications or team data.
  • Presence channels — like private channels, but they also track who is currently subscribed. Good for "who's online" indicators and collaborative features.

Choosing a WebSocket Provider

Option A: Pusher (Managed)

Pusher's free tier (200 concurrent connections, 200,000 messages/day) is enough for most early-stage SaaS products. Zero infrastructure to manage — you just set API keys and go. The right choice for an MVP or a product with under a few hundred concurrent users.

Option B: Soketi (Self-Hosted)

Soketi is a free, open-source, Pusher-compatible WebSocket server you host yourself. It uses the same Pusher API, so your Laravel and Echo code does not change — you just point it at your own server. The right choice when you need to control costs, eliminate per-message pricing, or keep data on your own infrastructure.

# Install Soketi on your server (requires Node.js 16+)
npm install -g @soketi/soketi

# Start (in production, manage with PM2 or Supervisor)
soketi start

Server-Side Setup

composer require pusher/pusher-php-server
# .env — Pusher
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=eu

# .env — Soketi (same keys, different host)
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=app-id
PUSHER_APP_KEY=app-key
PUSHER_APP_SECRET=app-secret
PUSHER_HOST=soketi.yourdomain.com
PUSHER_PORT=6001
PUSHER_SCHEME=https

Uncomment the Broadcast service provider in config/app.php and the broadcast routes in routes/channels.php.

Broadcasting Your First Event

Create a broadcastable event:

php artisan make:event TaskUpdated
namespace App\Events;

use App\Models\Task;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;

class TaskUpdated implements ShouldBroadcast
{
    use SerializesModels;

    public function __construct(public Task $task) {}

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('team.' . $this->task->team_id),
        ];
    }

    public function broadcastWith(): array
    {
        return [
            'id'     => $this->task->id,
            'title'  => $this->task->title,
            'status' => $this->task->status,
            'updated_by' => $this->task->updatedBy->name,
        ];
    }
}

Dispatch the event wherever the task is updated — in your controller, action class, or model observer:

// In TaskController::update()
$task->update($validated);
broadcast(new TaskUpdated($task))->toOthers(); // toOthers() skips the sender

Authorising Private Channels

Private channels require a server-side authorisation check. Define channel authorisation in routes/channels.php:

use App\Models\Team;

// Only authorise if the authenticated user belongs to this team
Broadcast::channel('team.{teamId}', function ($user, $teamId) {
    return $user->teams->contains($teamId);
});

Laravel handles the auth handshake automatically — when Echo subscribes to a private channel, it sends a POST to /broadcasting/auth, which runs your channel authorisation callback and returns a signed token.

Client-Side Setup with Laravel Echo

npm install --save-dev laravel-echo pusher-js
// resources/js/bootstrap.js
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,
    forceTLS:    true,

    // For Soketi: add these overrides
    // wsHost:   import.meta.env.VITE_PUSHER_HOST,
    // wsPort:   import.meta.env.VITE_PUSHER_PORT,
    // wssPort:  import.meta.env.VITE_PUSHER_PORT,
    // enabledTransports: ['ws', 'wss'],
});

Listen for your event in a Blade view or Alpine.js component:

// In a script block or Alpine component
window.Echo.private(`team.${teamId}`)
    .listen('TaskUpdated', (event) => {
        console.log('Task updated:', event);

        // Update the task in your local state or re-fetch from the server
        updateTaskInUI(event.id, event.status, event.updated_by);

        // Show a toast notification
        showToast(`${event.updated_by} updated "${event.title}"`);
    });

Presence Channels: Who Is Online

Presence channels are private channels that also broadcast a membership list — you can show which team members are currently viewing the same page:

// channels.php
Broadcast::channel('task.{taskId}', function ($user, $taskId) {
    $task = Task::find($taskId);
    if ($user->can('view', $task)) {
        return ['id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar];
    }
});
// Echo client
window.Echo.join(`task.${taskId}`)
    .here((users) => {
        // Initial list of who is already viewing
        setViewers(users);
    })
    .joining((user) => {
        addViewer(user); // someone arrived
    })
    .leaving((user) => {
        removeViewer(user); // someone left
    });

This gives you the "3 people are viewing this" indicators you see in Notion, Linear, and Figma — a collaborative UI pattern that is now expected in team productivity tools.

Practical Patterns for SaaS Products

Live Dashboard Counters

// Broadcast a metrics update every minute from a scheduled command
broadcast(new DashboardMetricsUpdated($team, [
    'mrr'            => $team->monthlyRecurringRevenue(),
    'active_users'   => $team->activeUsersThisMonth(),
    'open_tickets'   => $team->openSupportTickets(),
]));

Background Job Progress

// Inside a long-running queued job
class ProcessImport implements ShouldQueue
{
    public function handle(): void
    {
        $total = $this->file->rowCount();

        foreach ($this->rows as $i => $row) {
            $this->processRow($row);

            if ($i % 50 === 0) {
                broadcast(new ImportProgress($this->team, [
                    'processed' => $i,
                    'total'     => $total,
                    'percent'   => round(($i / $total) * 100),
                ]));
            }
        }

        broadcast(new ImportCompleted($this->team, $this->file));
    }
}

In-App Notifications

// In a Notification class
public function toBroadcast($notifiable): BroadcastMessage
{
    return new BroadcastMessage([
        'title'   => $this->title,
        'body'    => $this->body,
        'url'     => $this->url,
        'read'    => false,
        'created_at' => now()->toIsoString(),
    ]);
}

Frequently Asked Questions

Do I need Node.js to run Laravel Echo Server or Soketi?

Soketi requires Node.js on your server (it is a Node.js application), but your Laravel application itself remains pure PHP. If you want to avoid Node.js entirely, you can use Pusher managed service — it is a cloud WebSocket provider with a generous free tier. A third option is laravel-websockets, a PHP-native WebSocket server built on ReactPHP that runs as a Laravel application without any Node.js dependency.

How many concurrent WebSocket connections can I handle?

Pusher's free plan supports 200 concurrent connections. Soketi on a single $12/month DigitalOcean droplet can handle thousands of concurrent connections. For most early-stage SaaS products, Pusher's free tier is more than sufficient at launch — upgrade to a paid Pusher plan or migrate to Soketi when you have the metrics to justify it.

Can I broadcast from a queued job?

Yes — the broadcast() helper works anywhere in your application, including queued jobs, scheduled commands, and event listeners. The broadcast itself is dispatched via the queue by default (look for ShouldBroadcastNow if you want synchronous broadcasting). Broadcasting from queued jobs is the recommended pattern for progress indicators on long-running operations.

#Laravel #WebSockets #Broadcasting #Pusher #Laravel Echo #Real-Time #SaaS #Soketi
G
Gurpreet Singh

Senior Full Stack Developer — Laravel, Vue.js, Nuxt.js & AI. Available for freelance projects.

Hire Me for Your Project

Related Articles