Running Laravel Reverb on Your Own Server: Real-Time Events Without Pusher

Laravel Reverb is the official WebSocket server you run yourself instead of Pusher, with no per-message billing and event data kept in Saudi Arabia. Put it behind Nginx with TLS as a systemd service, and a 2 vCPU server holds thousands of connections.
Laravel Reverb is the first-party WebSocket server from the Laravel team. You run it on your own server, it accepts browser connections and pushes events in real time using the Pusher protocol, so Laravel Echo works with it without touching your frontend code. The short version: install it with php artisan install:broadcasting, set the REVERB variables in .env, start it with php artisan reverb:start on internal port 8080, and put Nginx in front of it to terminate TLS and pass the Upgrade and Connection headers so the HTTP request becomes a WebSocket. Then wrap it in a systemd service that restarts it automatically, and raise the open file limit to 10,000 or more, since every connection holds one open file. Companies make this move for two reasons. Cost comes first, because hosted services bill by concurrent connections and message volume, so the invoice grows with usage. Sovereignty comes second, because every event you broadcast (an order status, a staff notification, a support message) travels through servers outside Saudi Arabia. With Reverb on a Riyadh server everything stays inside your infrastructure, and events reach users in about 8ms from Riyadh instead of making a round trip to another continent.
Why replace Pusher with self-hosted Reverb
Pusher and similar services are great for getting started, but their pricing is built on two ceilings: concurrent connections and daily messages. An internal dashboard with 300 staff connected all day, each receiving 200 events, adds up to 60,000 messages per day, and that number climbs fast once you add status updates or typing indicators in chat. With Reverb the cost becomes server resources you already own, and nobody is counting messages.
The second point matters more for companies working with government entities or sensitive data. The event payload itself passes through the external service, so if you broadcast a national ID, a transaction status or a customer name, you are moving personal data across borders. Running Reverb on a Saudi cloud server solves this at the root, because the connection starts in the browser and ends on your server inside the Kingdom.
| Criterion | External hosted service | Reverb on your server |
|---|---|---|
| Cost | Grows with connections and messages | Fixed within server resources |
| Data location | Usually outside Saudi Arabia | Riyadh |
| Latency | 60 to 150ms depending on region | About 8ms from Riyadh |
| Maintenance | On the provider | On you (one service) |
Installation and environment variables
On Laravel 11 and 12 a single command installs Reverb, adds routes/channels.php and wires up Echo on the frontend:
php artisan install:broadcasting
# prompts for Reverb and installs laravel-echo and pusher-js
npm run build
Next comes .env. The key idea is that there are two addresses: an internal one Reverb listens on, and a public one the browser connects to through Nginx:
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=482113
REVERB_APP_KEY=k7f2x9q1
REVERB_APP_SECRET=s3cr3t-change-me
# public address the browser sees
REVERB_HOST=ws.example.sa
REVERB_PORT=443
REVERB_SCHEME=https
# internal address Reverb listens on
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
Binding REVERB_SERVER_HOST to 127.0.0.1 keeps port 8080 closed to the internet, so all traffic flows through Nginx alone. That shrinks your attack surface and keeps TLS in one place. Remember to rebuild the frontend after changing any VITE_REVERB_* variable, since those values are injected at build time.
Nginx with TLS and upgrade headers
The most common mistake we see here is forgetting the upgrade headers. The request reaches Reverb as plain HTTP, gets rejected, and the browser shows a connection failure after 3 retries. This configuration works as is:
server {
listen 443 ssl http2;
server_name ws.example.sa;
ssl_certificate /etc/letsencrypt/live/ws.example.sa/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ws.example.sa/privkey.pem;
location / {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 120s;
proxy_pass http://127.0.0.1:8080;
}
}
The proxy_read_timeout value matters because Nginx drops idle connections after 60 seconds by default, while Echo sends a ping roughly every 30 seconds, so raising it to 120 seconds prevents random disconnects. Also raise worker_connections in nginx.conf to at least 10,000, since each WebSocket uses two Nginx connections, one to the browser and one to Reverb.
Running it as a systemd service
Starting reverb:start from an SSH window means it dies when you close the window. The fix is a systemd unit that restarts it on any crash and lifts the open file limit:
[Unit]
Description=Laravel Reverb
After=network.target redis-server.service
[Service]
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php artisan reverb:start --no-interaction
Restart=always
RestartSec=3
LimitNOFILE=20000
[Install]
WantedBy=multi-user.target
Save it as /etc/systemd/system/reverb.service, then run systemctl daemon-reload and systemctl enable --now reverb. If you already use supervisor for queue workers you can add Reverb there with minfds=20000; what matters is having exactly one supervisor responsible for restarts. After every deploy run php artisan reverb:restart so Reverb picks up the new code, because it is a long-lived process that keeps code in memory just like queue workers.
One more detail makes a difference under heavy load. Reverb runs on the ReactPHP event loop, and the default loop uses stream_select, which is capped at 1,024 connections. If you expect more than that, install the ext-uv extension from PECL and Reverb will use it automatically with no code changes.
How many connections a 2 vCPU / 4 GB server can hold
An idle Reverb connection is cheap, roughly 20 to 50 KB of memory depending on how many channels it subscribes to. On a Growth plan server (2 vCPU and 4 GB RAM) that also runs the app, the database and Redis, Reverb usually has about 1.5 GB to work with, which in theory covers tens of thousands of idle connections. The real limit is CPU during broadcasts, not memory:
- Broadcasting one event to a channel with 5,000 subscribers means 5,000 network writes, and Reverb is a single process on a single core.
- In our rough tests, one core comfortably pushes between 15,000 and 25,000 small messages (under 1 KB) per second.
- A safe working figure for a Growth server is 3,000 to 5,000 concurrent connections with moderate broadcast traffic, leaving headroom for the app and the database.
Beyond that you have two options: add resources at 500 SAR per year for each extra vCPU, or run several Reverb instances as described below. The Growth plan itself is 6,900 SAR per year including VAT, about 19 SAR per day, and every plan is listed on the pricing page. Always measure with a tool like k6 or artillery before launch instead of relying on estimates.
Scaling with Redis pub/sub across instances
A single Reverb instance knows nothing about connections held by another. If you run two instances behind a load balancer and the app broadcasts an event that lands on the first one, subscribers on the second never see it. The fix is built into Reverb:
REVERB_SCALING_ENABLED=true
REDIS_HOST=10.0.0.5
REDIS_PORT=6379
With scaling enabled, each instance publishes every message it receives to Redis and all instances listen on the same channel, so the event reaches every subscriber wherever their connection lives. Point all instances at the same Redis server and let Nginx spread connections through an upstream block using least_conn, since these connections live for a long time. On a single 4-core server you can run 3 instances on ports 8080, 8081 and 8082 and leave one core for the app, using every core instead of just one.
Monitoring and what to watch
Reverb integrates with Laravel Pulse out of the box, showing connection counts and message rates on one dashboard. Watch three numbers: concurrent connections, messages per second, and CPU usage of the Reverb process. If CPU stays above 70% it is time to add an instance. Keep an eye on the systemd journal with journalctl -u reverb -f for any Too many open files errors, which mean your LimitNOFILE is now below what you need.
Steps to take today
- Install Reverb with
install:broadcastingand set both the internal and public addresses in.env. - Add a subdomain such as
ws.with a TLS certificate and the Nginx block above, including the upgrade headers. - Turn it into a systemd service with
LimitNOFILE=20000and addreverb:restartto your deploy script. - Install
ext-uvif you expect more than 1,000 connections. - Load test with k6 to find your server's real ceiling, then enable
REVERB_SCALING_ENABLEDwhen you need multiple instances.
If your applications need a Saudi server that runs Reverb alongside Laravel and the database in one place, a CLOUD server activates within seconds of payment, so you can start on the Growth plan and add cores when your monitoring numbers call for it rather than by guesswork.


