DEV Community

Madi souheib
Madi souheib

Posted on

Why I built a Reverb alternative in Rust ?

I've been running real-time features in Laravel apps for a while, and self-hosted broadcasting always came with a tax. You either pay Pusher/Ably per connection, or you self-host — and self-hosting meant Reverb (single-threaded PHP, needs ext-ev + Redis once you pass ~1k connections) or Soketi (a Node runtime sitting next to your PHP stack).

Two things kept biting me: memory per connection, and what happens when one slow client backs up the event loop for everyone else.

So I built Ripple — a Pusher-compatible WebSocket server written in Rust — to see if I could fix both without asking anyone to change a single line of client code.

The design constraint: change nothing on the client

The most important decision was compatibility. Ripple speaks the Pusher Channels protocol, so your existing setup works unchanged:

  • Laravel Echo
  • pusher-js
  • pusher-php-server
  • mobile SDKs

You point wsHost / wsPort at Ripple and you're done. No Redis, no Node, no PHP extensions. It ships as a single static binary — the Docker image is a ~5 MB FROM scratch.

composer require ripple/ripple-laravel
php artisan ripple:install
php artisan ripple:start
Enter fullscreen mode Exit fullscreen mode

Why Rust

Reverb is a single-threaded PHP event loop. That's a hard ceiling: one core, and it needs ext-ev/ext-uv plus Redis to scale past a thousand-ish connections.

Ripple runs across all cores natively, and the memory footprint per connection is small enough that a modest box holds tens of thousands of idle connections.

The numbers

Measured head-to-head against Reverb on an AWS c6i.xlarge, both servers pinned to 2 cores. Full methodology and caveats live in the repo's bench/RESULTS.md:

Metric Ripple Reverb (tuned)
60,000 idle connections 770 MiB, 100% established not attempted
Memory @ 40k connections 512 MiB (~12.8 KB/conn) 834 MiB (~20 KB/conn)
50k deliveries/s — p50 / p99 14.7 / 32 ms 24.6 / 254 ms
Fan-out 1 event → 10k subs p50 94 ms p50 122 ms

A caveat I want to be honest about: these numbers are specific to that hardware — treat them as relative, not absolute. And I haven't run Soketi on this harness yet, so there are no Soketi numbers here. I don't want to publish benchmarks I didn't measure.

The part I actually care about: slow consumers

Benchmarks of raw throughput are the easy part. The failure mode that hurts in production is subtler: one client on a bad mobile connection can't drain messages fast enough, its buffer grows, and — depending on the server — that pressure degrades everyone.

Under a flood, Reverb buffered unbounded and p99 latency for all clients climbed into the hundreds of seconds. Ripple bounds each connection's buffer, keeps fan-out non-blocking, and disconnects the laggard instead of letting it drag the rest down. Memory stays flat.

That, more than any throughput number, is why I kept building.

Session resume (the one extension I'm proud of)

Standard Pusher clients drop messages across a reconnect — a mobile blip or a deploy and those events are gone.

Ripple has an opt-in extension: with history_size = N on an app, every broadcast frame carries a per-channel sequence number (standard clients ignore it, so full compatibility is preserved), and the server keeps the last N events. After a reconnect, the client sends its last seen sequence and the missed events replay in order. It ships with a ~40-line Echo/pusher-js companion and it's authorization-safe — resume requires a prior signed subscribe.

What else is in the box

Presence channels, client events, webhooks, Prometheus metrics at /metrics, a Horizon-style Laravel dashboard at /ripple, native TLS via rustls, and graceful shutdown that drains in-flight messages and sends a proper close frame so clients auto-reconnect cleanly on deploy.

It's early

Ripple is v0 and MIT licensed. It's not battle-tested at massive scale yet, and I'd genuinely value scrutiny — on the protocol edge cases, and especially on the benchmark methodology.

Repo: https://github.com/madisoheib/Ripple

If you run real-time on Laravel, I'd love to hear where your setup hurts.

Top comments (0)