We ran a prediction platform on Pusher for 18 months. It worked — until it didn't. Connection drops during traffic spikes, $400/month bills, and the nagging feeling that we were paying for infrastructure we could own.
We migrated to Laravel Octane + Reverb (native WebSockets). The result: 80% cost reduction, zero connection drops under load, and full infrastructure ownership.
Why Pusher Failed Us
Three concrete problems:
- Connection drops under load: During peak prediction cycles (every minute), 15-20% of clients would disconnect and need manual reconnection logic.
- Cost scaling: $400/month for ~50k concurrent connections. Our traffic was growing 20% MoM.
- No control: Debugging connection issues meant filing support tickets. Black box.
Architecture: Octane + Reverb
Octane keeps the Laravel application in memory (Swoole/RoadRunner). Reverb is Laravel's native WebSocket server — built on Swoole, zero dependencies, fully integrated with Laravel's broadcasting.
# Install
composer require laravel/octane laravel/reverb
# Configure Octane for Swoole
php artisan octane:install --server=swoole
# Configure Reverb
php artisan reverb:install
php artisan reverb:start
Broadcasting Events
Standard Laravel broadcasting — just swap the driver:
# .env
BROADCAST_DRIVER=reverb
REVERB_APP_ID=your-app-id
REVERB_APP_KEY=your-app-key
REVERB_APP_SECRET=your-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http
Then your existing events work unchanged:
prediction->market_id)];
}
public function broadcastWith(): array
{
return $this->prediction->toBroadcastArray();
}
}
Horizontal Scaling: Multiple Octane Workers
Octane runs multiple workers. Each worker maintains its own WebSocket connections. Reverb handles the pub/sub across workers via Redis.
# supervisord.conf
[program:octane]
command=php artisan octane:start --server=swoole --workers=8 --max-requests=500
process_name=octane-%(process_num)02d
numprocs=1
autostart=true
autorestart=true
user=www-data
[program:reverb]
command=php artisan reverb:start --host=0.0.0.0 --port=8080
process_name=reverb
numprocs=1
autostart=true
autorestart=true
user=www-data
Results
| Metric | Pusher | Octane + Reverb |
|---|---|---|
| Monthly cost | $400 | $80 (server) |
| Connection drops (peak) | 15-20% | 0% |
| Max concurrent connections | 50k (plan limit) | 200k+ (server limit) |
| Latency (p95) | 180ms | 45ms |
Gotchas
- Memory leaks: Octane keeps app in memory. Use `--max-requests=500` to recycle workers. Profile with
php artisan octane:status. - Stateful services: Singletons persist across requests. Reset DB connections, flush caches in
Octane::flushState(). - SSL termination: Reverb runs on HTTP internally. Terminate TLS at nginx, proxy to Reverb on 8080.
- Heartbeat: Configure
ping_intervalin Reverb config to detect dead connections.
Client-Side: Laravel Echo + Reverb
import Echo from 'laravel-echo'
import Pusher from 'pusher-js'
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,
wssPort: import.meta.env.VITE_REVERB_PORT,
forceTLS: import.meta.env.VITE_REVERB_SCHEME === 'https',
disableStats: true,
})
Conclusion
Moving off Pusher was the best infrastructure decision we made this year. Full control, 80% cost savings, and better reliability. If you're on Laravel and need real-time — Octane + Reverb is the path.