[{"data":1,"prerenderedAt":17},["ShallowReactive",2],{"$fQoKG3AICg1oOHq4FxxzekRSIK6L8AZSXwFMzvjDa00A":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"draft":13,"readingMinutes":14,"html":15,"related":16},"laravel-reverb","Laravel Reverb in Production: The Limits to Raise First","Laravel Reverb runs WebSockets inside your Laravel app. What install:broadcasting sets up, and the five server limits that stop it near 1,000 users.","2026-10-31",[9,10,11,12],"laravel","reverb","websockets","php",false,4,"\u003Cp>Laravel Reverb is Laravel&#39;s own WebSocket server. You install it with \u003Ccode>php artisan install:broadcasting\u003C/code>, start it with \u003Ccode>php artisan reverb:start\u003C/code>, and your existing broadcast events reach the browser through Laravel Echo without paying Pusher or Ably. It works out of the box on a laptop. On a real server it hits a wall around 1,000 open connections, and the wall isn&#39;t Reverb.\u003C/p>\n\u003Cp>Everything below is checked against the \u003Ca href=\"https://laravel.com/docs/reverb\">Reverb docs for Laravel 13\u003C/a> on October 31, 2026.\u003C/p>\n\u003Ch2>What the install actually does\u003C/h2>\n\u003Cp>\u003Ccode>install:broadcasting\u003C/code> runs \u003Ccode>reverb:install\u003C/code> behind the scenes, writes \u003Ccode>config/reverb.php\u003C/code>, and adds three credentials to \u003Ccode>.env\u003C/code>:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">REVERB_APP_ID=...\nREVERB_APP_KEY=...\nREVERB_APP_SECRET=...\n\u003C/code>\u003C/pre>\n\u003Cp>It speaks the Pusher protocol, which is why the client side is just Echo with the \u003Ccode>reverb\u003C/code> broadcaster, and why moving off Pusher is mostly a config change rather than a rewrite. Your \u003Ccode>ShouldBroadcast\u003C/code> events, private channels and presence channels stay as they are.\u003C/p>\n\u003Cp>The bit that confuses everyone on the first deploy is the two pairs of host and port variables. \u003Ccode>REVERB_SERVER_HOST\u003C/code> and \u003Ccode>REVERB_SERVER_PORT\u003C/code> are where the Reverb process listens (default \u003Ccode>0.0.0.0:8080\u003C/code>). \u003Ccode>REVERB_HOST\u003C/code> and \u003Ccode>REVERB_PORT\u003C/code> are where Laravel and the browser \u003Cem>send\u003C/em> things. In production those are different:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-ini\">REVERB_SERVER_HOST=0.0.0.0\nREVERB_SERVER_PORT=8080\n\nREVERB_HOST=ws.example.com\nREVERB_PORT=443\nREVERB_SCHEME=https\n\u003C/code>\u003C/pre>\n\u003Cp>If you see Echo trying to connect to \u003Ccode>ws://localhost:8080\u003C/code> from a live site, it&#39;s the second pair. The Vite build bakes \u003Ccode>VITE_REVERB_*\u003C/code> values in at build time, so fix \u003Ccode>.env\u003C/code> and then rebuild the frontend, or you&#39;ll be staring at the same wrong URL wondering why nothing changed.\u003C/p>\n\u003Ch2>The five limits, in the order you&#39;ll hit them\u003C/h2>\n\u003Cp>A WebSocket server keeps every connection open. On Linux each one is an open file, and several layers each have their own cap. Reverb&#39;s production section lists them; here they are in the order I&#39;d raise them.\u003C/p>\n\u003Cimg src=\"/blog/laravel-reverb/reverb-connection-limits.webp\" alt=\"A pipe that narrows at five points between users and Reverb: stream_select at 1,024, the user open-file limit, Supervisor minfds, Nginx worker_connections and the local port range of 28,231\" width=\"1600\" height=\"900\" loading=\"lazy\">\u003Cp>\u003Cstrong>1. The event loop.\u003C/strong> Reverb runs on a ReactPHP event loop. The default one uses \u003Ccode>stream_select\u003C/code>, which &quot;is typically limited to 1,024 open files.&quot; So past roughly 1,000 concurrent connections you need the \u003Ccode>uv\u003C/code> extension (\u003Ccode>pecl install uv\u003C/code>), and Reverb switches to it by itself when it&#39;s loaded. This is the one that bites people, because everything looks fine in staging with 40 users.\u003C/p>\n\u003Cp>\u003Cstrong>2. Open files for the user.\u003C/strong> \u003Ccode>ulimit -n\u003C/code> shows the limit for the user running Reverb. The docs raise it in \u003Ccode>/etc/security/limits.conf\u003C/code>, for example to 10,000 soft and hard for that user. If Reverb runs under systemd instead, set \u003Ccode>LimitNOFILE=\u003C/code> in the unit file, because systemd services don&#39;t read \u003Ccode>limits.conf\u003C/code>.\u003C/p>\n\u003Cp>\u003Cstrong>3. Supervisor.\u003C/strong> If Supervisor keeps Reverb alive, its own \u003Ccode>minfds\u003C/code> setting caps what its child processes can open. The docs set \u003Ccode>minfds=10000\u003C/code> under \u003Ccode>[supervisord]\u003C/code>. Miss this and you&#39;ve raised the OS limit for nothing.\u003C/p>\n\u003Cp>\u003Cstrong>4. Nginx.\u003C/strong> Reverb shouldn&#39;t face the internet directly. Put Nginx in front, proxy with the \u003Ccode>Upgrade\u003C/code> and \u003Ccode>Connection &quot;Upgrade&quot;\u003C/code> headers, and make sure both \u003Ccode>/app\u003C/code> (WebSocket connections) and \u003Ccode>/apps\u003C/code> (the HTTP API Laravel uses to publish) reach it. Then raise \u003Ccode>worker_rlimit_nofile\u003C/code> and \u003Ccode>worker_connections\u003C/code> in \u003Ccode>nginx.conf\u003C/code>, since Nginx holds a connection open for every socket it proxies.\u003C/p>\n\u003Cp>\u003Cstrong>5. Ports.\u003C/strong> \u003Ccode>cat /proc/sys/net/ipv4/ip_local_port_range\u003C/code> usually prints \u003Ccode>32768 60999\u003C/code>, which is 28,231 connections per server. You can widen it in \u003Ccode>sysctl.conf\u003C/code>, but by then you should be adding servers instead.\u003C/p>\n\u003Ch2>Scaling past one server\u003C/h2>\n\u003Cp>Set \u003Ccode>REVERB_SCALING_ENABLED=true\u003C/code>, point every Reverb server at one shared Redis, and put them behind a load balancer. Reverb uses Redis pub/sub to pass each message to the other servers, so a user connected to server A still gets the event that was published on server B. Use a Redis your app already trusts; if the WebSocket servers can&#39;t reach it, messages quietly stop crossing over.\u003C/p>\n\u003Ch2>The parts I&#39;d add before going live\u003C/h2>\n\u003Cp>\u003Ccode>reverb:start\u003C/code> is a long-running process, so new code doesn&#39;t reach it until you run \u003Ccode>php artisan reverb:restart\u003C/code> in your deploy script. Forget that and your broadcast payloads come from last week&#39;s code. Put it next to \u003Ccode>queue:restart\u003C/code> so the two never drift apart.\u003C/p>\n\u003Cp>Set \u003Ccode>allowed_origins\u003C/code> in \u003Ccode>config/reverb.php\u003C/code> to your real domains instead of \u003Ccode>*\u003C/code>. Turn on the Pulse recorders (\u003Ccode>ReverbConnections\u003C/code>, \u003Ccode>ReverbMessages\u003C/code>) if you already run Pulse; the graph of connections over a day tells you more about capacity than any load test. And keep \u003Ccode>--debug\u003C/code> for your laptop, it prints every frame.\u003C/p>\n\u003Cp>Is it worth it over Pusher? For most Laravel apps, I think yes. It&#39;s one more process to run, but it&#39;s PHP you can read, it scales sideways with Redis you probably already have, and the bill doesn&#39;t grow with your connection count. If you&#39;d rather someone else ran it, Laravel Cloud offers managed Reverb clusters.\u003C/p>\n\u003Cp>If you&#39;re adding real-time features to a Laravel app and want the server side done properly, that&#39;s part of the \u003Ca href=\"/services/laravel-development\">Laravel development\u003C/a> work I do. The \u003Ca href=\"https://calendly.com/director-norsky/meeting\">first call is free\u003C/a>.\u003C/p>\n",[],1794181975288]