webpatser / resonate
Fiber-based WebSocket server: a drop-in replacement for Laravel Reverb, built on fledge-fiber
Requires
- php: ^8.5
- illuminate/cache: ^13.0
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/support: ^13.0
- revolt/event-loop: ^1.0
- webpatser/fledge-fiber: ^13.4
Requires (Dev)
- laravel/pulse: ^1.0
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^4.0
- pusher/pusher-php-server: ^7.2
README
Fiber-based drop-in replacement for Laravel Reverb, built on webpatser/fledge-fiber and PHP 8.5+.
Why
Reverb is already async, but it pulls in its own ReactPHP / Ratchet / clue-redis stack. Resonate consolidates the runtime onto Fledge: Revolt + webpatser/fledge-fiber, the same async stack that powers webpatser/torque, webpatser/laravel-fiber, and webpatser/laravel-resp3-cache. The wins are practical:
- PHP 8.5 only. No polyfills, no
version_compare, native URI parser, nativearray_all/array_any. - One async runtime per app. Fledge's HTTP server gives HTTP/2 and shares the loop with the rest of your async work, with no second event loop competing for the request.
- Fiber ergonomics. Channel auth, application providers, and pub/sub callbacks read like synchronous code but yield on I/O. Custom auth backends can hit a database or HTTP API without blocking the tick.
The wire protocol, REST API, and config schema are byte-compatible with Laravel Reverb.
Install (fresh app)
composer require webpatser/resonate php artisan resonate:install php artisan resonate:start
Install (swap from laravel/reverb)
composer remove laravel/reverb composer require webpatser/resonate
That's it. Nothing else changes:
- Same
config/reverb.php. Resonate reads the existing file. - New artisan commands:
resonate:start,resonate:restart,resonate:reload,resonate:install. Update supervisor / systemd / Docker entrypoints accordingly. - Same
laravel:reverb:restartcache key. Running servers restart on the same signal. - Same Pusher wire protocol (byte-exact JSON framing) and the same Pusher-compatible REST API.
- Supervisor / systemd / Docker configs stay as-is.
- Front-end Echo and
pusher-jsconfigs stay as-is.
Zero-downtime reload
resonate:restart is the legacy hard restart: it sets the laravel:reverb:restart cache key, the running server picks it up within 5 seconds, calls stop(), and your supervisor respawns it. WebSocket connections drop; the listener is gone for the 0-5 second window between exit and respawn. Fine for development, rough for production deploys.
resonate:reload is the production path. The listener is bound with SO_REUSEPORT so the new process can hold the port while the old one drains.
# Default: spawn a replacement, wait for /up, then drain the old PID. php artisan resonate:reload # Drain only (for systemd ExecReload=, Kubernetes preStop, Supervisor). php artisan resonate:reload --drain
Tune the drain window with REVERB_DRAIN_TIMEOUT (default 30 seconds). Existing WebSocket clients stay connected to the old process until they disconnect naturally or the timeout fires.
Horizontal scaling
Set REVERB_SCALING_ENABLED=true along with your REDIS_* variables. Multiple Resonate instances coordinate via Redis pub/sub on fledge-fiber's async Redis client; message, terminate, and metrics events propagate across nodes.
Resonate uses a pure JSON envelope for cross-node messages, with no serialize() on the wire. This means a cluster cannot run mixed Resonate and laravel/reverb nodes; migration is all-at-once.
Requirements
- PHP
^8.5 - Laravel
^13.0
Optional integrations:
laravel/pulse: Resonate registers thereverb.connectionsandreverb.messagesLivewire dashboard components automatically.laravel/telescope: entry storage for inspecting connections, channels, and messages.
Acknowledgements
Resonate is a clean-room port of laravel/reverb (MIT, © Taylor Otwell, Joe Dixon). Several files (notably the Pusher protocol layer and the Pulse dashboard cards) are direct ports of Reverb's MIT-licensed code. See LICENSE.md for the full attribution.
License
MIT. See LICENSE.md.