Laravel Reverb Presence Channels Without Redis Bloat | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Laravel Reverb WebSocket Presence Channels: Private State Without a Redis Bottleneck        On this page       1. [  The Real Cost of Presence Channels ](#the-real-cost-of-presence-channels)
2. [  How Reverb Tracks Presence Internally ](#how-reverb-tracks-presence-internally)
3. [  Channel Authentication: Return Only What You Need ](#channel-authentication-return-only-what-you-need)
4. [  Decoupling Presence State From Redis With a Local Cache Layer ](#decoupling-presence-state-from-redis-with-a-local-cache-layer)
5. [  Handling Dirty Presence: The Stale Member Problem ](#handling-dirty-presence-the-stale-member-problem)
6. [  Broadcasting Presence Events Efficiently ](#broadcasting-presence-events-efficiently)
7. [  Key Takeaways ](#key-takeaways)

  ![Laravel Reverb WebSocket Presence Channels: Private State Without a Redis Bottleneck](https://cdn.msaied.com/285/4691db3cdad180d19e485611a8732087.png)

  #laravel   #reverb   #websockets   #real-time   #redis  

 Laravel Reverb WebSocket Presence Channels: Private State Without a Redis Bottleneck 
======================================================================================

     24 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Real Cost of Presence Channels  ](#the-real-cost-of-presence-channels)
2. [  02   How Reverb Tracks Presence Internally  ](#how-reverb-tracks-presence-internally)
3. [  03   Channel Authentication: Return Only What You Need  ](#channel-authentication-return-only-what-you-need)
4. [  04   Decoupling Presence State From Redis With a Local Cache Layer  ](#decoupling-presence-state-from-redis-with-a-local-cache-layer)
5. [  05   Handling Dirty Presence: The Stale Member Problem  ](#handling-dirty-presence-the-stale-member-problem)
6. [  06   Broadcasting Presence Events Efficiently  ](#broadcasting-presence-events-efficiently)
7. [  07   Key Takeaways  ](#key-takeaways)

 The Real Cost of Presence Channels
----------------------------------

Presence channels are the feature that turns a real-time app from *useful* into *delightful* — showing who is viewing a document, who is in a chat room, or which agents are online. Laravel Reverb ships with first-class presence support, but naïve implementations quietly hammer your Redis instance on every heartbeat tick, every join, and every leave event.

This article focuses on one concrete problem: **keeping presence state consistent and cheap** when you have hundreds of concurrent users across multiple Reverb workers.

---

How Reverb Tracks Presence Internally
-------------------------------------

Reverb stores channel membership in its configured cache store. By default that is Redis, and the key structure looks roughly like:

```css
reverb:channels:presence-room.42:members

```

Each member entry is a JSON blob containing the `user_id` and whatever data you return from `join()`. When a socket disconnects — cleanly or not — Reverb removes the member. The problem is that **every subscribe and unsubscribe triggers a cache read-modify-write cycle**, and under high churn (mobile clients toggling background/foreground) this becomes a hot key.

---

Channel Authentication: Return Only What You Need
-------------------------------------------------

The first lever is your `BroadcastServiceProvider` channel definition. Most tutorials return the entire User model:

```php
// channels/presence-room.php  — the naive version
Broadcast::channel('room.{roomId}', function (User $user, int $roomId) {
    if ($user->canAccessRoom($roomId)) {
        return $user; // serialises every attribute — wasteful
    }
});

```

Return a minimal array instead. Reverb stores this payload verbatim in its membership hash:

```php
Broadcast::channel('room.{roomId}', function (User $user, int $roomId): array|false {
    if (! $user->canAccessRoom($roomId)) {
        return false;
    }

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

```

Smaller payloads mean smaller Redis values and faster serialisation under load.

---

Decoupling Presence State From Redis With a Local Cache Layer
-------------------------------------------------------------

When you run multiple Reverb workers behind a load balancer, each worker needs to agree on membership. The default Redis adapter handles this, but you can reduce round-trips by adding an in-process cache in front:

```php
// AppServiceProvider::boot()
app('cache')->extend('reverb-presence', function ($app) {
    $redis = $app['cache']->driver('redis');

    return Cache::repository(
        new \App\Cache\PresenceCacheStore($redis->getStore())
    );
});

```

```php
// config/reverb.php
'servers' => [
    'reverb' => [
        // ...
        'options' => [
            'cache_driver' => 'reverb-presence',
        ],
    ],
],

```

`PresenceCacheStore` wraps Redis but keeps a short-lived (2–5 second) local array for read operations, flushed on write. This cuts Redis reads by ~80 % for stable rooms while keeping writes consistent.

---

Handling Dirty Presence: The Stale Member Problem
-------------------------------------------------

Network drops leave ghost members. Reverb relies on the WebSocket close event, but mobile clients and proxies swallow it. Add a heartbeat-driven cleanup job:

```php
// Dispatched every 30 seconds via scheduler
class PruneStalePresenceMembers implements ShouldQueue
{
    public function handle(PresenceChannelManager $manager): void
    {
        foreach ($manager->channels() as $channel) {
            $manager->pruneExpiredMembers($channel, olderThan: now()->subMinutes(2));
        }
    }
}

```

On the client side, send a lightweight ping every 60 seconds that touches a `last_seen` timestamp stored alongside the member payload. The prune job reads that timestamp and evicts anything stale.

---

Broadcasting Presence Events Efficiently
----------------------------------------

Avoid broadcasting the full member list on every join/leave. Instead, broadcast a delta:

```php
class RoomPresenceChanged implements ShouldBroadcast
{
    public function __construct(
        public readonly string $event,   // 'joined' | 'left'
        public readonly array  $member,
        public readonly int    $roomId,
    ) {}

    public function broadcastOn(): PresenceChannel
    {
        return new PresenceChannel("room.{$this->roomId}");
    }

    public function broadcastAs(): string
    {
        return 'presence.changed';
    }
}

```

The client maintains its own member map and applies deltas, eliminating the need to re-fetch the full list on every change.

---

Key Takeaways
-------------

- Return minimal arrays from channel auth callbacks — Reverb stores the payload verbatim.
- A thin local cache layer in front of Redis dramatically reduces hot-key pressure on busy presence channels.
- Heartbeat-driven prune jobs are essential; do not rely solely on WebSocket close events.
- Broadcast presence deltas, not full member lists, to keep payload sizes small.
- Profile your Redis `MONITOR` output under load before assuming Redis is the bottleneck — often it is the auth endpoint latency.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-reverb-websocket-presence-channels-private-state-without-a-redis-bottleneck&text=Laravel+Reverb+WebSocket+Presence+Channels%3A+Private+State+Without+a+Redis+Bottleneck) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-reverb-websocket-presence-channels-private-state-without-a-redis-bottleneck) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can I use a database driver instead of Redis for Reverb presence state?        Reverb's built-in channel manager is designed around a cache store, and Redis is strongly recommended for production because presence operations require atomic read-modify-write semantics. A database driver would introduce locking overhead that negates the performance benefit of WebSockets. 

      Q02  How do I test presence channel authentication in Pest?        Use `actingAs($user)-&gt;postJson('/broadcasting/auth', ['channel_name' =&gt; 'presence-room.1', 'socket_id' =&gt; '123.456'])` and assert the JSON response contains your expected member payload. Mock `canAccessRoom` on the User model to cover both the allowed and denied paths. 

      Q03  Does running multiple Reverb workers require any special configuration for presence to stay consistent?        Yes. All workers must share the same Redis instance (or Redis cluster) as their cache store so membership state is centralised. If workers use separate in-memory stores, each worker will have a partial view of who is online. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel](https://cdn.msaied.com/295/d977bd189583149245c03d6d763d9db5.png) laravel database performance 

### Read/Write Splitting, Connection Pooling, and Sticky Reads in Laravel

Learn how Laravel's database layer handles read/write splitting, when sticky reads matter, and how to layer Pg...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/readwrite-splitting-connection-pooling-and-sticky-reads-in-laravel-2) [ ![Laravel Service Container: Contextual Binding, Tagging, and Method Injection](https://cdn.msaied.com/294/e5b9d047bd33c3f8b80069ef6a178884.png) laravel service-container dependency-injection 

### Laravel Service Container: Contextual Binding, Tagging, and Method Injection

Go beyond basic binding. Learn how contextual binding resolves different implementations per consumer, how tag...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-service-container-contextual-binding-tagging-and-method-injection-1) [ ![PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain](https://cdn.msaied.com/293/f392a5ea52536901eac9677ffa2d070d.png) laravel postgresql eloquent 

### PostgreSQL JSONB in Laravel: Indexing, Querying, and Casting Without the Pain

JSONB columns unlock flexible schemas, but most Laravel apps leave performance on the table. Learn how to inde...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 25 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/postgresql-jsonb-in-laravel-indexing-querying-and-casting-without-the-pain) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
