DEV Community

HoudaifaDevBS
HoudaifaDevBS

Posted on

From Polling to Real-Time: Building a Laravel 12 Chat with WebSockets & Reverb

Have you ever wondered how chat applications deliver messages instantly without overwhelming the server?

polling to real-time

I recently worked on a legacy Laravel application that included a built-in chat system. At first glance, everything seemed functional — messages were sent and received, and the feature "worked."

But the experience felt off.

Messages were delayed. The interface became slower after a few minutes. Opening multiple tabs made things worse. Something clearly wasn't right.

After digging into the code, I discovered the issue: the application was repeatedly requesting new messages every few seconds. It was using a technique known as polling.

In other words, every connected user was continuously asking the server:

"Do you have new messages now?"
"What about now?"
"Now?"

This approach might work for small internal tools, but it doesn't scale. With enough users, the number of HTTP requests explodes — consuming server resources while still delivering a delayed "real-time" experience.

That's when I decided to rebuild the entire chat system using a proper WebSocket-based architecture with Laravel Reverb.

The difference was dramatic:

  • Latency dropped significantly
  • HTTP request volume decreased drastically
  • The UI became truly real-time

In this article, I'll walk you through how I built a production-ready WebSocket solution using Laravel 12, Reverb, and Nginx.


1. Why Polling Fails at Scale

Polling seems simple. The client sends an HTTP request every few seconds asking: "Do you have new messages?"

If there are new messages, the server responds with them. If not, it returns an empty response.

At small scale, this works. But let's look at the math.

If one user polls every 3 seconds:

  • 20 requests per minute
  • 1,200 requests per hour

Now imagine 100 concurrent users:

  • 2,000 requests per minute
  • 120,000 requests per hour

And most of those requests return… nothing.

This creates three major problems:

1. Massive Waste of Resources — Every single HTTP request requires the server to accept the connection, bootstrap the Laravel framework, hit the routing layer, check authentication, and likely query the database — just to return an empty JSON array. This eats up CPU and RAM for zero actual payload.

2. Artificial Latency — If a user polls every 3 seconds, and a message is sent right after a poll finishes, the receiving user won't see it for almost 3 seconds. In a modern chat application, a 3-second delay feels like an eternity.

3. Client-Side Degradation — Opening multiple tabs means multiplying those requests. Browsers limit the number of concurrent connections to the same domain. If your polling clogs up those connections, the rest of your application (like loading images or saving form data) starts to crawl.


2. The WebSocket Solution: From Asking to Listening

Instead of the client repeatedly asking the server for updates, what if the server just told the client when something happened?

Enter WebSockets.

A WebSocket creates a persistent, bi-directional, full-duplex connection between the client and the server.

  • Connect Once — The client makes a single HTTP request to establish the connection, which is then "upgraded" to a WebSocket.
  • Listen Continuously — The connection stays open with minimal overhead. The client simply waits.
  • Push Instantly — When User A sends a message, the server instantly pushes that payload through the open connection directly to User B.

Zero wasted requests. Zero artificial delay. True real-time communication.

polling vs websockets


3. Laravel Real-Time Options

Historically, achieving this in Laravel meant making a tough choice.

For years, the standard approach was relying on third-party SaaS platforms like Pusher. While incredibly easy to set up, third-party services can become prohibitively expensive as your concurrent connections and daily message limits scale up.

If you wanted to self-host to save costs, you had to venture outside the PHP ecosystem. You might have used Laravel WebSockets (which required running a long-lived PHP process that wasn't natively optimized for it and is now mostly abandoned).

Then came Laravel Reverb.

Introduced as a first-party package, Reverb is a blazing-fast, highly scalable WebSocket server written entirely in PHP using ReactPHP. It integrates perfectly with Laravel's existing broadcasting system.

pusher vs reverb


4. Setting Up Reverb and Broadcasting

Since we are using Laravel 12, installing and configuring Reverb is incredibly streamlined. Laravel 12 doesn't enable broadcasting by default, but it provides a single, powerful Artisan command to scaffold everything we need.

# Install Broadcasting + Reverb + Node dependencies (Pusher, Echo)
php artisan install:broadcasting
Enter fullscreen mode Exit fullscreen mode
INFO  Published 'broadcasting' configuration file.
INFO  Published 'channels' route file.

┌ Which broadcasting driver would you like to use? ────────────┐
│ Laravel Reverb                                               │
└──────────────────────────────────────────────────────────────┘

┌ Would you like to install Laravel Reverb? ───────────────────┐
│ Yes                                                          │
└──────────────────────────────────────────────────────────────┘

┌ Would you like to install and build the Node dependencies required for broadcasting? ┐
│ Yes                                                                                   │
└───────────────────────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

If you check your .env file, you'll see Laravel has automatically generated the keys for your local Reverb server:

BROADCAST_CONNECTION=reverb

REVERB_APP_ID=your_generated_id
REVERB_APP_KEY=your_generated_key
REVERB_APP_SECRET=your_generated_secret
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http

VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
Enter fullscreen mode Exit fullscreen mode

Now spin up the actual WebSocket server. Open a new terminal tab and run:

php artisan reverb:start
Enter fullscreen mode Exit fullscreen mode

🔥 You now have a blazing-fast, self-hosted WebSocket server running on port 8080.


5. Building the Chat System & Firing Events

The Data Layer

First, set up a simple Message model linked to our User.

php artisan make:model Message -m
Enter fullscreen mode Exit fullscreen mode

In your migration, keep it lean — just the essentials:

Schema::create('messages', function (Blueprint $table) {
    $table->id();
    $table->foreignId('sender_id')->constrained('users');
    $table->foreignId('receiver_id')->constrained('users');
    $table->text('body');
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

The Broadcast Event

Generate an event that Laravel will automatically push to Reverb.

php artisan make:event MessageSent
Enter fullscreen mode Exit fullscreen mode

Implement the ShouldBroadcastNow interface to tell Laravel to skip the queue and push the payload to Reverb immediately.

class MessageSent implements ShouldBroadcastNow
{
    use Dispatchable, SerializesModels;

    public function __construct(public Message $message) {}

    // Securing the channel so only the receiver user can listen
    public function broadcastOn(): Channel
    {
        return new PrivateChannel('chat.' . $this->message->receiver_id);
    }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ In high traffic, use the ShouldBroadcast interface instead — this lets queue workers handle broadcasting so the server can respond immediately.

Securing Private Channels

Open routes/channels.php and add:

use Illuminate\Support\Facades\Broadcast;

Broadcast::channel('chat.{userId}', function ($user, $userId) {
    return (int) $user->id === (int) $userId;
});
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • Only the authenticated user can listen to their own private chat channel
  • No user can subscribe to another user's channel

Without this, Laravel will reject the WebSocket subscription.

Firing the Event

When a user submits a new message via your controller, save it to the database and fire the event:

public function store(Request $request)
{
    $message = Message::create([
        'sender_id'   => $request->user()->id,
        'receiver_id' => $request->receiver_id,
        'body'        => $request->body,
    ]);

    // This sends the pulse ONLY to the receiver's private channel
    broadcast(new MessageSent($message))->toOthers();

    return response()->json($message);
}
Enter fullscreen mode Exit fullscreen mode

That's it for the backend — Reverb is now catching that event and blasting it out to the connected WebSocket client on the chat.{receiverId} channel.


6. Catching the Event & Updating the UI in Real-Time

The backend is now broadcasting events through Reverb. But broadcasting alone does nothing unless the frontend is actively listening. This is where Laravel Echo comes into play.

Configuring Echo

// 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 ?? 'http') === 'https',
    enabledTransports: ['ws', 'wss'],
});
Enter fullscreen mode Exit fullscreen mode
  • key → Must match your REVERB_APP_KEY
  • wsHost and wsPort → Point to your Reverb server
  • forceTLS → Automatically switches to wss:// in production

At this point, the browser will establish a persistent WebSocket connection to the Reverb server as soon as your app loads.

Listening to the Private Channel

const userId = window.App.user.id;

window.Echo.private(`chat.${userId}`)
    .listen('MessageSent', (event) => {
        console.log('New message received:', event.message);
        appendMessage(event.message);
    });
Enter fullscreen mode Exit fullscreen mode

No polling. No intervals. No repeated API calls. The client is now simply listening.

Updating the UI

function appendMessageToUI(message) {
    const chatBox = document.getElementById('chat-box');
    const messageElement = document.createElement('div');
    messageElement.classList.add('message');
    messageElement.innerText = message.body;
    chatBox.appendChild(messageElement);
}
Enter fullscreen mode Exit fullscreen mode

The moment MessageSent is broadcast:

  1. Reverb pushes it to the browser
  2. Echo catches it
  3. Your callback executes
  4. The DOM updates instantly

network inspect socket request


7. Real Metrics: Before vs After

When migrating from polling to WebSockets, the improvement isn't theoretical — it's measurable. The comparison below is based on 80 concurrent users.

Metric Polling WebSockets (Reverb)
HTTP Requests/min ~1,600 ~0 (persistent connection)
Message Latency Up to 3 seconds Near-instant
Server Load High (constant requests) Low (event-driven)
Multi-tab Behavior Degrades quickly Stable

8. Real-World Use Cases Beyond Chat

This architecture is not limited to messaging. Once WebSockets are in place, you unlock powerful capabilities:

  • 🔔 Live Notifications — Instant alerts without refreshing the page
  • 📊 Real-Time Dashboards — Admin panels that update metrics instantly
  • 📦 Order Tracking — Delivery status updates pushed in real-time
  • 🧠 Live Analytics — Active user counters, sales metrics, traffic monitoring
  • 🏭 IoT Monitoring — Devices pushing sensor data instantly to dashboards

Any system where "something happens" and users must see it immediately is a perfect candidate.


9. Final Thoughts

Rebuilding the chat system was only half the journey.

Moving from polling to WebSockets solved the performance and latency problems — but running WebSockets in production introduces new challenges:

  • Reverse proxy configuration
  • SSL termination (wss://)
  • Process supervision
  • Firewall rules
  • Scaling strategy

In Part 2, I'll walk through how to deploy Laravel Reverb in production behind Nginx — including the exact configuration, common pitfalls, and issues you may encounter along the way.

🔗 Stay Connected

Follow me for more Laravel tutorials, dev tips, deployment workflows and solving real-world production headaches.

Found this article useful?

🙏 Show your support by clapping 👏, subscribing 🔔, sharing to social networks

Top comments (0)