{"id":3613,"date":"2025-12-28T18:36:02","date_gmt":"2025-12-28T15:36:02","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/how-websockets-and-real-time-apps-really-work\/"},"modified":"2026-11-04T20:10:45","modified_gmt":"2026-11-04T17:10:45","slug":"websocket-hosting-laravel-reverb-socketio","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/websocket-hosting-laravel-reverb-socketio\/","title":{"rendered":"WebSocket Hosting: Laravel Reverb and Socket.IO"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>Chat, live dashboards and in-app notifications need timely updates, but they do not all require two-way communication. <strong>WebSocket hosting<\/strong> becomes relevant when the application needs persistent bidirectional messaging. With Laravel Echo, Reverb or Socket.IO, the practical question is whether the chosen shared plan supports the workload or whether a <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a> or managed realtime service is a better fit.<\/p>\n<p>This article explains how WebSockets work, which limitations you may hit on typical shared hosting, how a VPS changes the picture, and concrete architectures for Node.js, Laravel Echo and Socket.io. You will see realistic patterns: from \u201cI only have cPanel\u201d to \u201cI\u2019m ready to run reverse proxies, queues and Redis on a VPS.\u201d By the end, you should have a basis for choosing a platform, planning capacity tests, and identifying the deployment requirements for your real\u2011time stack.<\/p>\n<p>The examples use Laravel 13 \/ PHP 8.3+, Reverb 1.12 and Echo 2.6, followed by a separate Node.js 22 \/ Socket.IO 4 application. The Laravel example assumes an existing application with working session login, a database, Composer and a Vite build environment. Run application commands in that application\u2019s directory and review dependencies and pending migrations in staging before deployment.<\/p>\n<p>To choose the right hosting, it helps to understand what makes WebSockets different from normal HTTP.<\/p>\n<div id=\"toc_container\" role=\"navigation\" aria-label=\"Table of Contents\" data-nosnippet class=\"toc_transparent no_bullets toc_numbered toc_title_center\"><p class=\"toc_title\">\u0130\u00e7indekiler<\/p><ul class=\"toc_list\"><ul><li><a href=\"#HTTP_vs_WebSocket_in_simple_terms\"><span class=\"toc_number toc_depth_2\">0.1.<\/span> HTTP vs WebSocket in simple terms<\/a><\/li><li><a href=\"#Where_Nodejs_Laravel_Echo_and_Socketio_fit\"><span class=\"toc_number toc_depth_2\">0.2.<\/span> Where Node.js, Laravel Echo and Socket.io fit<\/a><\/li><\/ul><\/li><li><a href=\"#Shared_Hosting_vs_VPS_for_WebSocket_and_RealTime_Workloads\"><span class=\"toc_number toc_depth_1\">1.<\/span> Shared Hosting vs VPS for WebSocket and Real\u2011Time Workloads<\/a><ul><li><a href=\"#Constraints_of_typical_shared_hosting\"><span class=\"toc_number toc_depth_2\">1.1.<\/span> Constraints of typical shared hosting<\/a><\/li><li><a href=\"#What_shared_hosting_is_still_good_for\"><span class=\"toc_number toc_depth_2\">1.2.<\/span> What shared hosting is still good for<\/a><\/li><li><a href=\"#What_changes_when_you_move_to_a_VPS\"><span class=\"toc_number toc_depth_2\">1.3.<\/span> What changes when you move to a VPS<\/a><\/li><\/ul><\/li><li><a href=\"#Architecture_Patterns_for_Nodejs_Laravel_Echo_and_Socketio\"><span class=\"toc_number toc_depth_1\">2.<\/span> Architecture Patterns for Node.js, Laravel Echo and Socket.io<\/a><ul><li><a href=\"#Pattern_1_Everything_on_one_VPS\"><span class=\"toc_number toc_depth_2\">2.1.<\/span> Pattern 1: Everything on one VPS<\/a><\/li><li><a href=\"#Pattern_2_Shared_hosting_for_Laravel_VPS_for_realtime\"><span class=\"toc_number toc_depth_2\">2.2.<\/span> Pattern 2: Shared hosting for Laravel, VPS for real\u2011time<\/a><\/li><li><a href=\"#Pattern_3_Full_Nodejs_stack_with_Laravel_only_as_API\"><span class=\"toc_number toc_depth_2\">2.3.<\/span> Pattern 3: Full Node.js stack with Laravel only as API<\/a><\/li><\/ul><\/li><li><a href=\"#Running_WebSockets_on_Shared_Hosting_What_Is_Actually_Possible\"><span class=\"toc_number toc_depth_1\">3.<\/span> Running WebSockets on Shared Hosting: What Is Actually Possible?<\/a><ul><li><a href=\"#Option_1_Simulate_realtime_without_WebSockets\"><span class=\"toc_number toc_depth_2\">3.1.<\/span> Option 1: Simulate \u201creal\u2011time\u201d without WebSockets<\/a><\/li><li><a href=\"#Option_2_Use_an_external_realtime_service\"><span class=\"toc_number toc_depth_2\">3.2.<\/span> Option 2: Use an external real\u2011time service<\/a><\/li><li><a href=\"#Option_3_Move_only_the_WebSocket_piece_to_a_VPS\"><span class=\"toc_number toc_depth_2\">3.3.<\/span> Option 3: Move only the WebSocket piece to a VPS<\/a><\/li><\/ul><\/li><li><a href=\"#Setting_Up_WebSockets_on_a_VPS_A_Practical_Blueprint\"><span class=\"toc_number toc_depth_1\">4.<\/span> Setting Up WebSockets on a VPS: A Practical Blueprint<\/a><ul><li><a href=\"#Step_1_Basic_server_hardening_and_web_stack\"><span class=\"toc_number toc_depth_2\">4.1.<\/span> Step 1: Basic server hardening and web stack<\/a><\/li><li><a href=\"#Step_2_Install_Reverb_and_Configure_Its_Addresses\"><span class=\"toc_number toc_depth_2\">4.2.<\/span> Step 2: Install Reverb and Configure Its Addresses<\/a><\/li><li><a href=\"#Step_3_Create_a_Queued_Event_and_Authorized_Publisher\"><span class=\"toc_number toc_depth_2\">4.3.<\/span> Step 3: Create a Queued Event and Authorized Publisher<\/a><\/li><li><a href=\"#Step_4_Nginx_reverse_proxy_for_WebSockets\"><span class=\"toc_number toc_depth_2\">4.4.<\/span> Step 4: Nginx reverse proxy for WebSockets<\/a><\/li><li><a href=\"#Step_5_Subscribe_with_Echo_and_Run_the_Workers\"><span class=\"toc_number toc_depth_2\">4.5.<\/span> Step 5: Subscribe with Echo and Run the Workers<\/a><\/li><li><a href=\"#Separate_Example_Authenticated_SocketIO_Rooms\"><span class=\"toc_number toc_depth_2\">4.6.<\/span> Separate Example: Authenticated Socket.IO Rooms<\/a><\/li><\/ul><\/li><li><a href=\"#Performance_Scaling_and_Resource_Planning_for_RealTime_Apps\"><span class=\"toc_number toc_depth_1\">5.<\/span> Performance, Scaling and Resource Planning for Real\u2011Time Apps<\/a><ul><li><a href=\"#Key_resource_dimensions\"><span class=\"toc_number toc_depth_2\">5.1.<\/span> Key resource dimensions<\/a><\/li><li><a href=\"#Connection_counts_and_limits\"><span class=\"toc_number toc_depth_2\">5.2.<\/span> Connection counts and limits<\/a><\/li><li><a href=\"#Measure_before_guessing\"><span class=\"toc_number toc_depth_2\">5.3.<\/span> Measure before guessing<\/a><\/li><\/ul><\/li><li><a href=\"#Security_and_Reliability_Checklist_for_WebSocket_Hosting\"><span class=\"toc_number toc_depth_1\">6.<\/span> Security and Reliability Checklist for WebSocket Hosting<\/a><ul><li><a href=\"#1_Protect_Browser_Traffic_with_TLS\"><span class=\"toc_number toc_depth_2\">6.1.<\/span> 1. Protect Browser Traffic with TLS<\/a><\/li><li><a href=\"#2_Strict_authentication_and_authorization\"><span class=\"toc_number toc_depth_2\">6.2.<\/span> 2. Strict authentication and authorization<\/a><\/li><li><a href=\"#3_Rate_limiting_and_abuse_protection\"><span class=\"toc_number toc_depth_2\">6.3.<\/span> 3. Rate limiting and abuse protection<\/a><\/li><li><a href=\"#4_Robust_process_supervision_and_logging\"><span class=\"toc_number toc_depth_2\">6.4.<\/span> 4. Robust process supervision and logging<\/a><\/li><li><a href=\"#5_Plan_for_graceful_deploys\"><span class=\"toc_number toc_depth_2\">6.5.<\/span> 5. Plan for graceful deploys<\/a><\/li><\/ul><\/li><li><a href=\"#Choose_Hosting_Through_Protocol_Checks_and_Measurement\"><span class=\"toc_number toc_depth_1\">7.<\/span> Choose Hosting Through Protocol Checks and Measurement<\/a><\/li><li><a href=\"#Primary_Technical_References\"><span class=\"toc_number toc_depth_1\">8.<\/span> Primary Technical References<\/a><\/li><\/ul><\/div>\n<h3><span id=\"HTTP_vs_WebSocket_in_simple_terms\">HTTP vs WebSocket in simple terms<\/span><\/h3>\n<p>Traditional HTTP is <strong>request\/response<\/strong> based. The browser requests a page or API response and the server replies; the underlying connection can remain open for further requests. For live features such as chat, notifications and counters, HTTP-based options include:<\/p>\n<ul>\n<li><strong>Short polling:<\/strong> The browser hits an endpoint every few seconds.<\/li>\n<li><strong>Long polling:<\/strong> The server keeps the request open until there is new data or a configured timeout.<\/li>\n<li><strong>Server\u2011Sent Events (SSE):<\/strong> A one\u2011way stream from server to browser.<\/li>\n<\/ul>\n<p>WebSocket is different. In the usual HTTP\/1.1 setup, an initial HTTP handshake <strong>upgrades<\/strong> the connection to a persistent, full\u2011duplex WebSocket channel over TCP. Browser and server can push data to each other at any time without reopening connections.<\/p>\n<p>For real\u2011time apps, that means:<\/p>\n<ul>\n<li>Less per-message HTTP request overhead than polling.<\/li>\n<li>No polling-interval delay for server-pushed events once the socket is open.<\/li>\n<li>Ability to push events to many online users, subject to server and network capacity.<\/li>\n<\/ul>\n<h3><span id=\"Where_Nodejs_Laravel_Echo_and_Socketio_fit\">Where Node.js, Laravel Echo and Socket.io fit<\/span><\/h3>\n<p>The supported stacks in this guide have distinct roles:<\/p>\n<ul>\n<li><strong>Laravel:<\/strong> application logic, HTTP endpoints, authentication, channel authorization and queued broadcast jobs.<\/li>\n<li><strong>Reverb:<\/strong> a PHP WebSocket server using the Pusher protocol; it receives Laravel broadcasts through a supported publishing API.<\/li>\n<li><strong>Laravel Echo with pusher-js:<\/strong> the browser subscriber for the Reverb example.<\/li>\n<li><strong>Socket.IO with socket.io-client:<\/strong> a separate Node.js realtime stack with its own event and room protocol. It is not a raw WebSocket client and does not connect to Reverb by swapping package names.<\/li>\n<\/ul>\n<p>You can also build everything purely in Node.js (Express + Socket.io or NestJS + WebSockets), rather than combining Laravel and Node.js.<\/p>\n<h2><span id=\"Shared_Hosting_vs_VPS_for_WebSocket_and_RealTime_Workloads\">Shared Hosting vs VPS for WebSocket and Real\u2011Time Workloads<\/span><\/h2>\n<p>If your application already runs on shared hosting, check what the plan actually permits: persistent processes, WebSocket proxying, queue workers, supported runtimes and resource limits. The panel name alone does not establish those capabilities.<\/p>\n<h3><span id=\"Constraints_of_typical_shared_hosting\">Constraints of typical shared hosting<\/span><\/h3>\n<p>On classic shared hosting with cPanel or DirectAdmin, the provider usually gives you:<\/p>\n<ul>\n<li>Supported PHP versions and a managed web\/database stack.<\/li>\n<li>Defined CPU, memory, I\/O and process limits, sometimes enforced through CloudLinux or similar tooling.<\/li>\n<li>Provider-controlled ports, firewall and reverse-proxy configuration.<\/li>\n<li>Potential Node.js or persistent-process support that must be checked against the specific WebSocket requirements.<\/li>\n<\/ul>\n<p>WebSocket connections are served by a long-running process. A plan may support that process through its application platform, restrict it, or terminate it under limits. Verify:<\/p>\n<ul>\n<li>Whether the process remains running and restarts after failure or host maintenance.<\/li>\n<li>How its local endpoint is exposed through the provider\u2019s proxy.<\/li>\n<li>Whether upgrade requests, streaming, heartbeat timing and concurrent connections fit the plan.<\/li>\n<\/ul>\n<p>There are some shared hosting platforms with integrated Node.js support, but even then you often have strict limits on memory, CPU and concurrent connections. Whether those limits become a bottleneck depends on concurrent connections, message frequency, payload size and application work; Node.js support alone does not establish suitability.<\/p>\n<h3><span id=\"What_shared_hosting_is_still_good_for\">What shared hosting is still good for<\/span><\/h3>\n<p>Shared hosting can still be suitable for these uses, within the plan\u2019s runtime and resource limits:<\/p>\n<ul>\n<li>Your <strong>main PHP\/Laravel site<\/strong> and classic HTTP APIs.<\/li>\n<li><strong>Simple real\u2011time\u2011like features<\/strong> implemented via AJAX polling where a few seconds of delay is acceptable. SSE is another option if the host permits long-lived streaming responses without buffering or restrictive timeouts; concurrent streams must fit its resource limits.<\/li>\n<li>Using an <strong>external real\u2011time platform<\/strong> (a hosted WebSocket\/broadcasting SaaS) via HTTP APIs from your PHP app.<\/li>\n<\/ul>\n<p>If you are interested in a deeper PHP framework comparison on shared environments, we wrote a detailed guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/shared-hosting-vs-vps-for-laravel-and-php-frameworks-queues-schedulers-and-cache\/\">choosing between shared hosting and VPS for Laravel and other PHP frameworks<\/a>. Many of the same trade\u2011offs apply directly to real\u2011time workloads.<\/p>\n<h3><span id=\"What_changes_when_you_move_to_a_VPS\">What changes when you move to a VPS<\/span><\/h3>\n<p>For a self-managed VPS deployment, look for the following capabilities in the chosen plan:<\/p>\n<ul>\n<li>Appropriate administrative access to install supported runtimes, Reverb, optional Redis and queue workers.<\/li>\n<li>Control of guest firewall rules within provider policy; public HTTPS at 443, with application ports such as 8080 and 3000 kept private.<\/li>\n<li>A process manager such as systemd or Supervisor for PHP processes, or systemd\/PM2 for Node.js.<\/li>\n<li>A configurable reverse proxy and TLS certificates. Allocated vCPU counts do not imply dedicated physical cores; resource guarantees remain plan-specific.<\/li>\n<\/ul>\n<p>At that point, you are building a small \u201creal\u201d backend architecture, not just uploading PHP files. If you want a step\u2011by\u2011step look at what that feels like in production, we documented it in our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/how-i-host-node-js-in-production-without-drama-pm2-systemd-nginx-ssl-and-zero-downtime-deploys\/\">hosting Node.js in production with PM2, Nginx, SSL and zero\u2011downtime deploys<\/a>.<\/p>\n<p>For an initial capacity test of a small chat or notification system, you might start with a VPS offering 1\u20132 vCPU and 2\u20134 GB RAM. Treat this as a trial configuration, not a capacity guarantee: concurrent connections, event fan-out and any colocated database or workers determine the resources needed. In our article on <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-sizing-woocommerce-laravel-nodejs\/\">choosing VPS specs for WooCommerce, Laravel and Node.js<\/a>, we go deeper into sizing CPU, RAM and storage for application servers; the same logic applies to WebSocket workloads.<\/p>\n<h2><span id=\"Architecture_Patterns_for_Nodejs_Laravel_Echo_and_Socketio\">Architecture Patterns for Node.js, Laravel Echo and Socket.io<\/span><\/h2>\n<p>The following architecture options range from a single-server deployment to a split stack.<\/p>\n<h3><span id=\"Pattern_1_Everything_on_one_VPS\">Pattern 1: Everything on one VPS<\/span><\/h3>\n<p>A single-VPS layout for a small to medium project could use:<\/p>\n<ul>\n<li><strong>Nginx:<\/strong> public TLS entry point.<\/li>\n<li><strong>PHP-FPM and Laravel:<\/strong> application routes, session login and private-channel authorization.<\/li>\n<li><strong>Database queue and worker:<\/strong> asynchronous broadcasts in the example below.<\/li>\n<li><strong>Reverb:<\/strong> loopback WebSocket\/publishing service on port 8080.<\/li>\n<li><strong>Optional Redis:<\/strong> explicitly configured queue\/cache or Reverb scaling, not a universal broadcast bridge.<\/li>\n<\/ul>\n<p>The old <code>laravel-echo-server<\/code> repository is archived and its 1.6.3 package depends on Socket.IO 2.x. BeyondCode Laravel WebSockets also states that it is no longer maintained and recommends Reverb. These are not the installation path for the Laravel 13 example; an existing legacy deployment needs its own compatibility and migration assessment.<\/p>\n<p>Nginx routes the Laravel site separately from the Reverb browser connection:<\/p>\n<ul>\n<li><code>https:\/\/app.example.com<\/code> \u2192 Laravel \/ PHP-FPM, including <code>\/broadcasting\/auth<\/code>.<\/li>\n<li><code>wss:\/\/ws.example.com\/app\/...<\/code> \u2192 Reverb on <code>127.0.0.1:8080<\/code>.<\/li>\n<li>Laravel queue worker \u2192 Reverb publishing API at <code>http:\/\/127.0.0.1:8080\/apps\/...<\/code>.<\/li>\n<\/ul>\n<p>A single VPS reduces the number of hosts to manage, but the services still compete for resources and share a failure domain. Decide whether it is suitable through capacity and recovery tests.<\/p>\n<h3><span id=\"Pattern_2_Shared_hosting_for_Laravel_VPS_for_realtime\">Pattern 2: Shared hosting for Laravel, VPS for real\u2011time<\/span><\/h3>\n<p>If you already have a stable Laravel application on shared hosting and want to add WebSockets without migrating everything, a practical compromise is:<\/p>\n<ul>\n<li>Keep Laravel and its database on the existing plan if their requirements remain supported.<\/li>\n<li>Place Reverb, or a separately designed Node.js realtime application, on an appropriately sized VPS.<\/li>\n<li>Provide the queue processing and a compatible publishing integration. A generic WebSocket URL or shared Redis address is not sufficient.<\/li>\n<li>Point <code>ws.example.com<\/code> to the VPS and provide TLS. Laravel must reach Reverb\u2019s publishing API over HTTPS or a protected private network; localhost addresses do not cross machines.<\/li>\n<\/ul>\n<p>The long-lived connection process can move to the VPS while Laravel continues serving HTTP. Channel authorization still needs the application\u2019s session or token system. Configure cookie, origin and network access deliberately; CORS does not supply identity or channel permissions.<\/p>\n<h3><span id=\"Pattern_3_Full_Nodejs_stack_with_Laravel_only_as_API\">Pattern 3: Full Node.js stack with Laravel only as API<\/span><\/h3>\n<p>A project can instead use Node.js for its realtime features while keeping Laravel as a separate API or administration application:<\/p>\n<ul>\n<li>Serve the frontend at <code>app.example.com<\/code> and a compatible Socket.IO service at <code>ws.example.com<\/code>.<\/li>\n<li>Use Laravel as a separate API if needed.<\/li>\n<li>Define a supported identity\/token contract and server-side room permissions. Merely selecting JWT or OAuth does not implement cross-application authentication or an event bridge.<\/li>\n<\/ul>\n<p>From the hosting side, this looks more like a classic Node.js SaaS architecture and benefits strongly from the practices we detail in <a href=\"https:\/\/www.dchost.com\/blog\/en\/where-should-you-host-node-js-apps-cpanel-shared-hosting-or-vps\/\">our comparison of where to host Node.js apps: cPanel, shared hosting or VPS<\/a>.<\/p>\n<h2><span id=\"Running_WebSockets_on_Shared_Hosting_What_Is_Actually_Possible\">Running WebSockets on Shared Hosting: What Is Actually Possible?<\/span><\/h2>\n<p>On a shared plan, compare three practical choices against the features your application actually needs.<\/p>\n<h3><span id=\"Option_1_Simulate_realtime_without_WebSockets\">Option 1: Simulate \u201creal\u2011time\u201d without WebSockets<\/span><\/h3>\n<p>If your use case is not ultra\u2011latency\u2011sensitive, you can often get away with:<\/p>\n<ul>\n<li><strong>Short polling:<\/strong> choose the polling interval from acceptable delay and request volume.<\/li>\n<li><strong>Long polling:<\/strong> requests remain open until data or an application timeout; this still occupies server resources.<\/li>\n<li><strong>SSE:<\/strong> one-way streaming from server to browser, requiring a host\/proxy that permits streaming without disruptive buffering or timeouts.<\/li>\n<\/ul>\n<p>These options may avoid a separate WebSocket daemon, but long polling and SSE still maintain requests. Check worker\/concurrency limits and query volume rather than assuming that ordinary PHP support makes them suitable.<\/p>\n<h3><span id=\"Option_2_Use_an_external_realtime_service\">Option 2: Use an external real\u2011time service<\/span><\/h3>\n<p>Another pattern is to use a <strong>third\u2011party real\u2011time platform<\/strong> (a hosted WebSocket\/broadcasting SaaS) that provides:<\/p>\n<ul>\n<li>A WebSocket endpoint your frontend can connect to.<\/li>\n<li>HTTP APIs your PHP\/Laravel app can call to publish events.<\/li>\n<li>SDKs for presence channels, private channels, etc.<\/li>\n<\/ul>\n<p>The service handles persistent client connections while your application retains business logic, permissions and publishing. Queued Laravel broadcasts still require a worker. Compare event\/connection quotas, region latency, outage behavior and total operating cost; a managed service may remain appropriate long term.<\/p>\n<h3><span id=\"Option_3_Move_only_the_WebSocket_piece_to_a_VPS\">Option 3: Move only the WebSocket piece to a VPS<\/span><\/h3>\n<p>You can keep Laravel on the existing host and move Reverb to a VPS, provided the publishing API and private-channel authorization paths are accessible and protected. The one-host example below uses loopback and must be adapted when services run on different machines.<\/p>\n<h2><span id=\"Setting_Up_WebSockets_on_a_VPS_A_Practical_Blueprint\">Setting Up WebSockets on a VPS: A Practical Blueprint<\/span><\/h2>\n<p>The following blueprint is a single-VPS Laravel\/Reverb example, plus an independent Socket.IO demo. It assumes:<\/p>\n<ul>\n<li>A supported Linux environment, PHP 8.3+ and an existing Laravel 13 application with working session login.<\/li>\n<li>Nginx, valid TLS certificates and <code>app.example.com<\/code> for the Laravel site.<\/li>\n<li>Database queue migrations, a queue worker and a Composer\/Vite build environment.<\/li>\n<li>Reverb 1.12.0, Echo 2.6.3 and Pusher JS 8.6.0 as the example version set; supported Node.js 22 for build tooling and the later Socket.IO demo.<\/li>\n<\/ul>\n<h3><span id=\"Step_1_Basic_server_hardening_and_web_stack\">Step 1: Basic server hardening and web stack<\/span><\/h3>\n<p>Before touching WebSockets, prepare the VPS properly:<\/p>\n<ol>\n<li>Prepare supported packages and a deliberate security-update process.<\/li>\n<li>Allow required web traffic and preserve necessary management access. Keep the application\u2019s 8080\/3000 ports on loopback in this one-host layout.<\/li>\n<li>Configure the Laravel web root, PHP-FPM, database, application user and TLS before testing broadcasts.<\/li>\n<\/ol>\n<p>If you want a structured checklist for initial VPS hardening, our guide on <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-ssh-hardening\/\">VPS security hardening (sshd_config, Fail2ban, disabling direct root)<\/a> is a good reference. Investing in this early saves a lot of trouble later.<\/p>\n<h3><span id=\"Step_2_Install_Reverb_and_Configure_Its_Addresses\">Step 2: Install Reverb and Configure Its Addresses<\/span><\/h3>\n<p>Run the installer inside the existing Laravel project. Retain its generated <code>REVERB_APP_ID<\/code>, <code>REVERB_APP_KEY<\/code> and <code>REVERB_APP_SECRET<\/code>; the secret stays server-side. Review existing migration changes before running migrate and confirm the database queue\u2019s <code>jobs<\/code> table exists.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">php artisan install:broadcasting --reverb\ncomposer require laravel\/reverb:1.12.0\nnpm install --save-dev laravel-echo&#64;2.6.3 pusher-js&#64;8.6.0\nphp artisan migrate\nphp artisan channel:list\n<\/code><\/pre>\n<p>Use the following values in the application environment. The installer supplies the application credentials; these additional values separate listener, publisher and browser addresses:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">BROADCAST_CONNECTION=reverb\nQUEUE_CONNECTION=database\nDB_QUEUE_RETRY_AFTER=90\n\nREVERB_SERVER_HOST=127.0.0.1\nREVERB_SERVER_PORT=8080\nREVERB_HOST=127.0.0.1\nREVERB_PORT=8080\nREVERB_SCHEME=http\n\nVITE_REVERB_APP_KEY=&quot;${REVERB_APP_KEY}&quot;\nVITE_REVERB_HOST=ws.example.com\nVITE_REVERB_PORT=443\nVITE_REVERB_SCHEME=https\n<\/code><\/pre>\n<p><code>REVERB_SERVER_HOST\/PORT<\/code> controls the listener. <code>REVERB_HOST\/PORT\/SCHEME<\/code> is Laravel\u2019s publishing destination. Both use loopback here. The <code>VITE_REVERB_*<\/code> values are the public browser TLS endpoint and take effect after a new frontend build.<\/p>\n<p>In <code>config\/reverb.php<\/code>, restrict the application\u2019s <code>allowed_origins<\/code> to <code>['app.example.com']<\/code>. Confirm that <code>routes\/channels.php<\/code> and <code>\/broadcasting\/auth<\/code> were registered by installation. If needed, add <code>channels: __DIR__.'\/..\/routes\/channels.php'<\/code> to <code>withRouting<\/code> in <code>bootstrap\/app.php<\/code>. Origin checking and channel authorization solve different problems.<\/p>\n<p>Use separate managed processes for Reverb and the queue worker; a running HTTP site does not process queued broadcasts by itself. Process lifecycle details appear after the frontend example.<\/p>\n<h3><span id=\"Step_3_Create_a_Queued_Event_and_Authorized_Publisher\">Step 3: Create a Queued Event and Authorized Publisher<\/span><\/h3>\n<p>The event below targets the signed-in user\u2019s private channel. <code>broadcastWith<\/code> sends only the intended message field rather than every public property.<\/p>\n<pre class=\"language-php line-numbers\"><code class=\"language-php\">&lt;?php\n\/\/ app\/Events\/NoticeCreated.php\nnamespace App\\Events;\n\nuse Illuminate\\Broadcasting\\PrivateChannel;\nuse Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;\n\nclass NoticeCreated implements ShouldBroadcast\n{\n    public function __construct(\n        public string $userId,\n        public string $message,\n    ) {}\n\n    public function broadcastOn(): array\n    {\n        return [new PrivateChannel(&#039;users.&#039;.$this-&gt;userId)];\n    }\n\n    public function broadcastAs(): string\n    {\n        return &#039;notice.created&#039;;\n    }\n\n    public function broadcastWith(): array\n    {\n        return [&#039;message&#039; =&gt; $this-&gt;message];\n    }\n}\n<\/code><\/pre>\n<p>Register the per-user authorization callback and retain any other application channels:<\/p>\n<pre class=\"language-php line-numbers\"><code class=\"language-php\">&lt;?php\n\/\/ routes\/channels.php: retain any other application channels.\nuse App\\Models\\User;\nuse Illuminate\\Support\\Facades\\Broadcast;\n\nBroadcast::channel(&#039;users.{userId}&#039;, function (User $user, string $userId) {\n    return (string) $user-&gt;getAuthIdentifier() === $userId;\n});\n<\/code><\/pre>\n<p>Add the following to <code>routes\/web.php<\/code> without duplicating existing imports. The recipient comes from the authenticated session, not a client-supplied user ID. The web middleware provides CSRF handling; auth and throttle apply to the publisher.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ Add to routes\/web.php, below its existing PHP opening tag.\nuse App\\Events\\NoticeCreated;\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::view(&#039;\/realtime-demo&#039;, &#039;realtime-demo&#039;)-&gt;middleware(&#039;auth&#039;);\n\nRoute::post(&#039;\/realtime-demo\/notice&#039;, function (Request $request) {\n    $data = $request-&gt;validate([&#039;message&#039; =&gt; [&#039;required&#039;, &#039;string&#039;, &#039;max:200&#039;]]);\n    event(new NoticeCreated(\n        (string) $request-&gt;user()-&gt;getAuthIdentifier(),\n        $data[&#039;message&#039;],\n    ));\n    return response()-&gt;json([&#039;queued&#039; =&gt; true], 202);\n})-&gt;middleware([&#039;auth&#039;, &#039;throttle:20,1&#039;]);\n<\/code><\/pre>\n<p>HTTP 202 means queued, not delivered. The database worker must run before the event reaches Reverb. This is an ephemeral notification example, not durable message history.<\/p>\n<h3><span id=\"Step_4_Nginx_reverse_proxy_for_WebSockets\">Step 4: Nginx reverse proxy for WebSockets<\/span><\/h3>\n<p>For an HTTPS application, expose the browser WebSocket endpoint over <code>wss:\/\/<\/code> with this routing pattern:<\/p>\n<ul>\n<li>Terminate browser TLS at Nginx on port 443.<\/li>\n<li>Proxy Reverb browser subscriptions under <code>\/app\/<\/code> to loopback port 8080.<\/li>\n<li>Proxy the optional, separate Socket.IO application under <code>\/socket.io\/<\/code> to loopback port 3000.<\/li>\n<\/ul>\n<p>With a valid certificate already installed for <code>ws.example.com<\/code>, use this configuration. The map belongs in the HTTP context; enable only the location blocks your deployment uses.<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\"># map belongs in the http context, outside server blocks.\nmap $http_upgrade $connection_upgrade {\n    default upgrade;\n    &#039;&#039; close;\n}\n\nserver {\n    listen 443 ssl;\n    server_name ws.example.com;\n    ssl_certificate     \/etc\/letsencrypt\/live\/ws.example.com\/fullchain.pem;\n    ssl_certificate_key \/etc\/letsencrypt\/live\/ws.example.com\/privkey.pem;\n\n    # Reverb browser subscriptions; Laravel publishes over loopback :8080.\n    location \/app\/ {\n        proxy_pass http:\/\/127.0.0.1:8080;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_set_header Host $host;\n        proxy_read_timeout 120s;\n        proxy_send_timeout 120s;\n    }\n\n    # Separate, optional Socket.IO example.\n    location \/socket.io\/ {\n        proxy_pass http:\/\/127.0.0.1:3000;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_set_header Host $host;\n        proxy_read_timeout 120s;\n        proxy_send_timeout 120s;\n    }\n\n    location \/ { return 404; }\n}\n<\/code><\/pre>\n<p>No URI suffix on <code>proxy_pass<\/code> means the request path is preserved. <code>proxy_read_timeout<\/code> measures the gap between upstream reads, not total connection lifetime. The 120-second value exceeds Socket.IO\u2019s default 25-second ping interval plus 20-second ping timeout; recheck it when heartbeat settings change. Validate the actual configuration with <code>nginx -t<\/code>. Reverb\u2019s <code>\/apps<\/code> publishing API is reached over loopback here and is not publicly proxied; a split-host layout needs an accessible HTTPS publishing path or protected private-network connection.<\/p>\n<p>Cloudflare supports WebSockets, but its WAF checks the opening HTTP handshake rather than subsequent WebSocket messages. Keep message authorization and rate limits in the application, protect direct origin access, and expect reconnections after network or edge changes. The <a href=\"https:\/\/www.dchost.com\/blog\/en\/how-i-keep-websockets-and-grpc-happy-behind-cloudflare-the-calm-guide-to-nginx-timeouts-keep-alive-and-zero-downtime\/\">Cloudflare, WebSocket and gRPC guide<\/a> covers the connection layer.<\/p>\n<h3><span id=\"Step_5_Subscribe_with_Echo_and_Run_the_Workers\">Step 5: Subscribe with Echo and Run the Workers<\/span><\/h3>\n<p>Create the following Blade view. Add <code>resources\/js\/realtime-demo.js<\/code> to the Laravel plugin\u2019s input list in <code>vite.config.js<\/code>, keeping existing entries. This separate page starts only the Echo instance shown here.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;!-- resources\/views\/realtime-demo.blade.php --&gt;\n&lt;!doctype html&gt;\n&lt;html lang=&quot;en&quot;&gt;\n&lt;head&gt;\n    &lt;meta charset=&quot;utf-8&quot;&gt;\n    &lt;meta name=&quot;csrf-token&quot; content=&quot;{{ csrf_token() }}&quot;&gt;\n    &lt;meta name=&quot;user-id&quot; content=&quot;{{ auth()-&gt;id() }}&quot;&gt;\n    &lt;title&gt;Realtime demo&lt;\/title&gt;\n    @vite(&#039;resources\/js\/realtime-demo.js&#039;)\n&lt;\/head&gt;\n&lt;body&gt;\n    &lt;form id=&quot;notice-form&quot;&gt;\n        &lt;label&gt;Message &lt;input name=&quot;message&quot; maxlength=&quot;200&quot; required&gt;&lt;\/label&gt;\n        &lt;button id=&quot;send&quot; disabled&gt;Send&lt;\/button&gt;\n    &lt;\/form&gt;\n    &lt;p id=&quot;status&quot; role=&quot;status&quot;&gt;Connecting&lt;\/p&gt;\n    &lt;p id=&quot;notice&quot;&gt;&lt;\/p&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/code><\/pre>\n<p>The JavaScript uses <code>pusher-js<\/code> with the Reverb broadcaster. The Send button becomes available after private-channel subscription. The custom broadcast name requires the leading dot in <code>.notice.created<\/code>.<\/p>\n<p>Create <code>resources\/js\/realtime-demo.js<\/code>:<\/p>\n<pre class=\"language-python line-numbers\"><code class=\"language-python\">\/\/ resources\/js\/realtime-demo.js\nimport Echo from &#039;laravel-echo&#039;;\nimport Pusher from &#039;pusher-js&#039;;\n\nwindow.Pusher = Pusher;\nconst csrf = document.querySelector(&#039;meta[name=&quot;csrf-token&quot;]&#039;).content;\nconst userId = document.querySelector(&#039;meta[name=&quot;user-id&quot;]&#039;).content;\nconst status = document.querySelector(&#039;#status&#039;);\nconst send = document.querySelector(&#039;#send&#039;);\nlet deliverySeen = false;\nconst echo = new Echo({\n    broadcaster: &#039;reverb&#039;,\n    key: import.meta.env.VITE_REVERB_APP_KEY,\n    wsHost: import.meta.env.VITE_REVERB_HOST,\n    wsPort: Number(import.meta.env.VITE_REVERB_PORT),\n    wssPort: Number(import.meta.env.VITE_REVERB_PORT),\n    forceTLS: import.meta.env.VITE_REVERB_SCHEME === &#039;https&#039;,\n    enabledTransports: [&#039;ws&#039;, &#039;wss&#039;],\n    authEndpoint: &#039;\/broadcasting\/auth&#039;,\n    auth: { headers: { &#039;X-CSRF-TOKEN&#039;: csrf } },\n});\n\necho.private(`users.${userId}`)\n    .subscribed(() =&gt; { send.disabled = false; status.textContent = &#039;Subscribed&#039;; })\n    .error(() =&gt; { send.disabled = true; status.textContent = &#039;Subscription failed&#039;; })\n    .listen(&#039;.notice.created&#039;, event =&gt; {\n        deliverySeen = true;\n        document.querySelector(&#039;#notice&#039;).textContent = event.message;\n        status.textContent = &#039;Event received&#039;;\n    });\n\necho.connector.pusher.connection.bind(&#039;state_change&#039;, ({ current }) =&gt; {\n    if (current !== &#039;connected&#039;) send.disabled = true;\n});\n\ndocument.querySelector(&#039;#notice-form&#039;).addEventListener(&#039;submit&#039;, async event =&gt; {\n    event.preventDefault();\n    deliverySeen = false;\n    status.textContent = &#039;Submitting&#039;;\n    try {\n        const response = await fetch(&#039;\/realtime-demo\/notice&#039;, {\n            method: &#039;POST&#039;,\n            credentials: &#039;same-origin&#039;,\n            headers: {\n                &#039;Content-Type&#039;: &#039;application\/json&#039;,\n                &#039;Accept&#039;: &#039;application\/json&#039;,\n                &#039;X-CSRF-TOKEN&#039;: csrf,\n            },\n            body: JSON.stringify({ message: event.target.elements.message.value }),\n        });\n        if (!response.ok) throw new Error(&#039;Request failed&#039;);\n        if (!deliverySeen) status.textContent = &#039;Queued; waiting for delivery&#039;;\n    } catch {\n        status.textContent = &#039;Request failed; check session or validation&#039;;\n    }\n});\n<\/code><\/pre>\n<p>Build the frontend and initially run Reverb and the database worker in separate terminals:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">php artisan config:cache\nnpm run build\nphp artisan reverb:start\n# In a separate process:\nphp artisan queue:work database --queue=default --sleep=1 --tries=3 --timeout=60\n<\/code><\/pre>\n<p>The 60-second worker timeout is below the 90-second database reservation interval; CLI PHP needs PCNTL for timeout handling. Give external I\/O a shorter budget, and recalculate both limits for longer jobs. Three tries includes the first attempt. In production, configure separate Supervisor or systemd processes under the application user, with correct PHP paths, startup, restart and log retention. A 75-second manager stop allowance is an example for this 60-second worker budget. Rebuild cached configuration and restart long-lived processes after relevant configuration changes; Vite endpoint changes also need a new build.<\/p>\n<h3><span id=\"Separate_Example_Authenticated_SocketIO_Rooms\">Separate Example: Authenticated Socket.IO Rooms<\/span><\/h3>\n<p>This demo uses Node.js 22.23.4, Socket.IO 4.8.4 and jose 6.2.15. It does not integrate with Laravel\/Reverb. In a new test directory, install dependencies, create the three files below, then run the final three commands in the same trusted terminal. Alice may access only <code>room:red<\/code>; Bob only <code>room:blue<\/code>. The generated key stays server-side and tokens expire after five minutes.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">npm init -y\nnpm install --save-exact socket.io&#64;4.8.4 socket.io-client&#64;4.8.4 jose&#64;6.2.15\nexport DEMO_JWT_KEY_HEX=&quot;$(openssl rand -hex 32)&quot;\nnode token.mjs alice\nnode server.mjs\n<\/code><\/pre>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">\/\/ key.mjs: server-side only.\nconst hex = process.env.DEMO_JWT_KEY_HEX ?? &#039;&#039;;\nif (!\/^[a-f0-9]{64}$\/i.test(hex)) throw new Error(&#039;Set a 32-byte demo key&#039;);\nexport const key = Buffer.from(hex, &#039;hex&#039;);\n<\/code><\/pre>\n<pre class=\"language-python line-numbers\"><code class=\"language-python\">\/\/ token.mjs: run only in the trusted demo server terminal.\nimport { SignJWT } from &#039;jose&#039;;\nimport { key } from &#039;.\/key.mjs&#039;;\nconst user = process.argv[2];\nif (![&#039;alice&#039;, &#039;bob&#039;].includes(user)) throw new Error(&#039;Use alice or bob&#039;);\nconsole.log(await new SignJWT({})\n    .setProtectedHeader({ alg: &#039;HS256&#039; })\n    .setIssuer(&#039;realtime-demo&#039;).setAudience(&#039;realtime-demo&#039;)\n    .setSubject(user).setIssuedAt().setExpirationTime(&#039;5m&#039;)\n    .sign(key));\n<\/code><\/pre>\n<p>The server verifies signature, issuer, audience, algorithm and token age; it checks room permission again for publishing. The origin filter limits browser origins but does not replace identity or authorization. CORS alone does not protect WebSocket access.<\/p>\n<pre class=\"language-python line-numbers\"><code class=\"language-python\">\/\/ server.mjs\nimport { createServer } from &#039;node:http&#039;;\nimport { Server } from &#039;socket.io&#039;;\nimport { jwtVerify } from &#039;jose&#039;;\nimport { key } from &#039;.\/key.mjs&#039;;\n\nconst origin = &#039;https:\/\/app.example.com&#039;;\nconst acl = new Map([\n    [&#039;alice&#039;, new Set([&#039;room:red&#039;])],\n    [&#039;bob&#039;, new Set([&#039;room:blue&#039;])],\n]);\nconst http = createServer((request, response) =&gt; {\n    response.writeHead(request.url === &#039;\/health&#039; ? 200 : 404);\n    response.end(request.url === &#039;\/health&#039; ? &#039;ok&#039; : &#039;not found&#039;);\n});\nconst io = new Server(http, {\n    cors: { origin, methods: [&#039;GET&#039;, &#039;POST&#039;] },\n    allowRequest: (request, done) =&gt; done(null, request.headers.origin === origin),\n    maxHttpBufferSize: 16_384,\n});\n\nio.use(async (socket, next) =&gt; {\n    try {\n        const { payload } = await jwtVerify(socket.handshake.auth.token, key, {\n            algorithms: [&#039;HS256&#039;], issuer: &#039;realtime-demo&#039;, audience: &#039;realtime-demo&#039;,\n            requiredClaims: [&#039;sub&#039;, &#039;iat&#039;, &#039;exp&#039;], maxTokenAge: &#039;5m&#039;,\n        });\n        if (!acl.has(payload.sub) || payload.exp - payload.iat &gt; 300) {\n            throw new Error(&#039;Invalid identity or lifetime&#039;);\n        }\n        socket.data.user = payload.sub;\n        socket.data.expires = payload.exp * 1000;\n        next();\n    } catch {\n        next(new Error(&#039;Unauthorized&#039;));\n    }\n});\n\nio.on(&#039;connection&#039;, socket =&gt; {\n    const timer = setTimeout(() =&gt; socket.disconnect(true),\n        Math.max(0, socket.data.expires - Date.now()));\n    socket.on(&#039;disconnect&#039;, () =&gt; clearTimeout(timer));\n    let windowStart = Date.now(), count = 0;\n    socket.use((packet, next) =&gt; {\n        if (Date.now() &gt;= socket.data.expires) return socket.disconnect(true);\n        if (Date.now() - windowStart &gt;= 10_000) { windowStart = Date.now(); count = 0; }\n        if (++count &gt; 20) return socket.disconnect(true);\n        next();\n    });\n    socket.on(&#039;join&#039;, async (room, callback) =&gt; {\n        const reply = typeof callback === &#039;function&#039; ? callback : () =&gt; {};\n        if (!acl.get(socket.data.user)?.has(room)) return reply({ ok: false });\n        await socket.join(room);\n        reply({ ok: true });\n    });\n    socket.on(&#039;publish&#039;, (data, callback) =&gt; {\n        const reply = typeof callback === &#039;function&#039; ? callback : () =&gt; {};\n        if (!data || !acl.get(socket.data.user)?.has(data.room)\n            || !socket.rooms.has(data.room) || typeof data.text !== &#039;string&#039;\n            || !data.text.trim() || data.text.length &gt; 200) return reply({ ok: false });\n        io.to(data.room).emit(&#039;notice&#039;, { text: data.text, sender: socket.data.user });\n        reply({ ok: true });\n    });\n});\n\nhttp.listen(3000, &#039;127.0.0.1&#039;);\nfor (const signal of [&#039;SIGINT&#039;, &#039;SIGTERM&#039;]) {\n    process.once(signal, () =&gt; io.close(() =&gt; process.exit(0)));\n}\n<\/code><\/pre>\n<p>Bundle the client below into an HTTPS page at <code>app.example.com<\/code>. Paste the short-lived Alice token from the trusted terminal into the demo prompt; do not put it in a URL. It rejoins and sends Hello after each connection, so this sample is not suitable for non-idempotent business actions.<\/p>\n<pre class=\"language-python line-numbers\"><code class=\"language-python\">\/\/ Bundle into an HTTPS page at https:\/\/app.example.com.\nimport { io } from &#039;socket.io-client&#039;;\n\nconst token = window.prompt(&#039;Paste the short-lived alice demo token&#039;);\nconst socket = io(&#039;https:\/\/ws.example.com&#039;, { auth: { token }, autoConnect: false });\nsocket.on(&#039;notice&#039;, event =&gt; { console.log(&#039;Notice:&#039;, event.text); });\nsocket.on(&#039;connect_error&#039;, () =&gt; { console.log(&#039;Connection or authorization failed&#039;); });\nsocket.on(&#039;disconnect&#039;, reason =&gt; {\n    if (reason === &#039;io server disconnect&#039;) console.log(&#039;Obtain a fresh token before reconnecting&#039;);\n});\nsocket.on(&#039;connect&#039;, () =&gt; {\n    socket.timeout(5000).emit(&#039;join&#039;, &#039;room:red&#039;, (error, joined) =&gt; {\n        if (error || !joined?.ok) return console.log(&#039;Join failed&#039;);\n        socket.timeout(5000).emit(&#039;publish&#039;, { room: &#039;room:red&#039;, text: &#039;Hello&#039; },\n            (error, result) =&gt; console.log(error || !result?.ok ? &#039;Publish failed&#039; : &#039;Accepted&#039;));\n    });\n});\nsocket.connect();\n<\/code><\/pre>\n<p>The 20-event \/ 10-second limit is per connection and resets on reconnect. Production needs shared user\/IP limits, current permissions and a revocation\/disconnection path. Replace the static demo ACL and token command with your real identity provider. On token expiry the server disconnects the client; acquire a fresh token before reconnecting. An acknowledgement means accepted by this application, not persisted or delivered to every recipient.<\/p>\n<p>For a systemd 252 host, a Node service declaration can be:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">[Unit]\nDescription=Socket.IO demo\nAfter=network.target\nStartLimitIntervalSec=60\nStartLimitBurst=5\n\n[Service]\nType=simple\nUser=realtime\nWorkingDirectory=\/srv\/realtime\nEnvironmentFile=\/etc\/realtime-demo.env\nExecStart=\/opt\/node22\/bin\/node \/srv\/realtime\/server.mjs\nRestart=always\nRestartSec=5\nTimeoutStopSec=30\nStandardOutput=journal\nStandardError=journal\n\n[Install]\nWantedBy=multi-user.target\n<\/code><\/pre>\n<p>First prepare the <code>realtime<\/code> user, <code>\/srv\/realtime<\/code> files and actual Node 22 binary path. The root-readable <code>\/etc\/realtime-demo.env<\/code> file contains <code>DEMO_JWT_KEY_HEX=&lt;64-hex-character-key&gt;<\/code>, matching the token issuer\u2019s key; it is not a shell script. Validate the unit with <code>systemd-analyze verify<\/code>, then apply daemon-reload and enable &#8211;now through an authorized administrator account. Configure journal retention and alert if restart limits stop the service. Use one manager for a given process.<\/p>\n<h2><span id=\"Performance_Scaling_and_Resource_Planning_for_RealTime_Apps\">Performance, Scaling and Resource Planning for Real\u2011Time Apps<\/span><\/h2>\n<p>WebSockets change the resource profile of your server. Instead of lots of short\u2011lived HTTP requests, you now have <strong>many concurrent open connections<\/strong>. That affects how you size and tune your VPS.<\/p>\n<h3><span id=\"Key_resource_dimensions\">Key resource dimensions<\/span><\/h3>\n<ul>\n<li><strong>CPU:<\/strong> parsing, encoding, application work and message fan-out. A core count alone does not establish throughput.<\/li>\n<li><strong>Memory:<\/strong> application\/connection state and kernel buffers, plus PHP-FPM, database, worker and optional Redis memory.<\/li>\n<li><strong>Network:<\/strong> message size, delivery fan-out, latency and jitter. Storage performance can matter for database and persistent queue work; it does not directly remove network delay. The <a href=\"https:\/\/www.dchost.com\/blog\/en\/nvme-vps-iops-latency-storage-tests\/\">NVMe VPS guide<\/a> explains storage trade-offs.<\/li>\n<\/ul>\n<h3><span id=\"Connection_counts_and_limits\">Connection counts and limits<\/span><\/h3>\n<p>Do not infer a connection limit from RAM alone. For a hypothetical calculation, reserving 1.2 GB for connections and assuming 30 KB per connection gives 40,000 using decimal units. Those invented inputs illustrate division, not a benchmark; they omit kernel overhead, CPU, traffic bursts and other services.<\/p>\n<ul>\n<li>Per-process file descriptors, reverse-proxy connection limits and kernel memory.<\/li>\n<li>TCP accept queues and connection churn; backlog settings are not established-connection capacity figures.<\/li>\n<li>Event-loop delay, queue age and fan-out storms, such as emitting a room-wide update on every keystroke.<\/li>\n<\/ul>\n<p>As your app grows, you can:<\/p>\n<ul>\n<li>Use Reverb\u2019s supported Redis scaling or the appropriate Socket.IO adapter for that stack. Sharing Redis does not bridge the two protocols.<\/li>\n<li>With Socket.IO HTTP long-polling enabled, multiple nodes need session affinity as well as an adapter. The Redis adapter does not remove affinity requirements or provide durable replay.<\/li>\n<li>Separate database and connection services when measurements justify it; secure Redis with network restrictions, credentials\/ACLs and TLS where needed.<\/li>\n<li>Apply event debouncing, payload limits and shared rate limits. A Redis prefix or logical database does not isolate memory, failure or eviction domains.<\/li>\n<\/ul>\n<h3><span id=\"Measure_before_guessing\">Measure before guessing<\/span><\/h3>\n<p>Use a compatible client for the actual protocol: Echo\/Pusher for Reverb, and <code>socket.io-client<\/code> or a verified Socket.IO-capable test engine for Socket.IO. A generic k6\/Locust raw WebSocket handshake does not test Engine.IO negotiation, room authorization or event handling. Exercise allowed and denied subscriptions, invalid\/expired credentials, message delivery, reconnect waves and worker delay, then apply realistic payload sizes and fan-out. Small functional tests do not establish capacity. The <a href=\"https:\/\/www.dchost.com\/blog\/en\/load-test-hosting-with-k6-jmeter-and-locust\/\">hosting load-testing guide<\/a> provides the measurement approach.<\/p>\n<h2><span id=\"Security_and_Reliability_Checklist_for_WebSocket_Hosting\">Security and Reliability Checklist for WebSocket Hosting<\/span><\/h2>\n<p>Protect the handshake, channel permissions, messages and operational lifecycle separately:<\/p>\n<h3><span id=\"1_Protect_Browser_Traffic_with_TLS\">1. Protect Browser Traffic with TLS<\/span><\/h3>\n<p>Serve browser WebSockets over <code>wss:\/\/<\/code> for HTTPS applications. This protects traffic in transit. In the examples, Nginx terminates TLS and the backend hop stays on the same host\u2019s loopback interface; cross-host hops need their own protected transport and access policy.<\/p>\n<h3><span id=\"2_Strict_authentication_and_authorization\">2. Strict authentication and authorization<\/span><\/h3>\n<ul>\n<li>Authorize Laravel private\/presence channels against current user\/room access; an origin allowlist is not authorization.<\/li>\n<li>Validate Socket.IO credentials at connection and permissions for each protected event. Connection middleware runs once per connection, not before every message.<\/li>\n<li>Handle expiry and revocation for active connections. The Socket.IO demo disconnects at expiry; immediate account\/session revocation still needs an application-level mechanism. Laravel authorization at subscription likewise does not continually re-evaluate existing subscriptions.<\/li>\n<\/ul>\n<h3><span id=\"3_Rate_limiting_and_abuse_protection\">3. Rate limiting and abuse protection<\/span><\/h3>\n<p>WebSockets can be abused by:<\/p>\n<ul>\n<li>Opening many concurrent connections from the same IP.<\/li>\n<li>Flooding the server with messages.<\/li>\n<li>Attempting to brute\u2011force channel names or auth tokens.<\/li>\n<\/ul>\n<p>Defences include:<\/p>\n<ul>\n<li>Apply connection limits with a deliberate trusted-proxy\/client-IP configuration.<\/li>\n<li>Use application-level per-user event limits and payload bounds; a per-socket demo limit is not a distributed abuse control.<\/li>\n<li>Use WAF rules for the HTTP handshake while retaining message validation and authorization in the application.<\/li>\n<\/ul>\n<p>For broader WAF and bot\u2011blocking strategies around your real\u2011time app, our article on <a href=\"https:\/\/www.dchost.com\/blog\/en\/the-layered-shield-i-trust-waf-and-bot-protection-with-cloudflare-modsecurity-and-fail2ban\/\">layered WAF and bot protection with Cloudflare, ModSecurity and Fail2ban<\/a> gives a practical, hosting\u2011side view.<\/p>\n<h3><span id=\"4_Robust_process_supervision_and_logging\">4. Robust process supervision and logging<\/span><\/h3>\n<ul>\n<li>Manage Reverb, queue workers and Node.js with appropriate startup\/restart policies and one manager per process.<\/li>\n<li>Centralize errors, reconnect counts and correlation IDs without recording tokens or private message bodies.<\/li>\n<li>Monitor HTTP liveness plus an authorized subscription-and-delivery check; a successful <code>\/health<\/code> response alone does not prove realtime delivery.<\/li>\n<\/ul>\n<p>We have written several guides on <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-resource-monitoring\/\">VPS resource monitoring<\/a> and <a href=\"https:\/\/www.dchost.com\/blog\/en\/vps-monitoring-prometheus-grafana\/\">setting up Prometheus + Grafana + exporters for alerting<\/a>; these same tools work well for tracking connection counts, CPU spikes and sustained memory growth in real\u2011time stacks.<\/p>\n<h3><span id=\"5_Plan_for_graceful_deploys\">5. Plan for graceful deploys<\/span><\/h3>\n<p>Restarting a connection process disconnects its active clients. Plan for:<\/p>\n<ul>\n<li>Drain or deliberately close old connections, then let clients reconnect with fresh authorization. Reverb restart does not preserve existing connections.<\/li>\n<li>Use bounded retry\/backoff and resubscription; a rolling release or systemd reload alone cannot guarantee connection continuity.<\/li>\n<li>Keep message versions compatible during overlap. Socket.IO defaults to at-most-once delivery; missed server events are not automatically replayed. Durable history, offsets and idempotency are needed when loss or duplicates matter.<\/li>\n<li>Before DNS migration, lower TTL and wait out the old TTL. TTL neither allocates a traffic percentage nor moves open connections; weighted DNS or a load balancer is a separate mechanism, and DNS caching prevents exact user-percentage control.<\/li>\n<\/ul>\n<p>The <a href=\"https:\/\/www.dchost.com\/blog\/en\/zero-downtime-ci-cd-to-a-vps-the-friendly-rsync-symlink-systemd-playbook-i-keep-reusing\/\">VPS deployment guide<\/a> covers release mechanics. Realtime applications also need recovery tests for live connection state and durable data: protect code, lock files, configuration and databases; define Redis queue\/session persistence and eviction policies instead of treating all Redis data as disposable cache.<\/p>\n<h2><span id=\"Choose_Hosting_Through_Protocol_Checks_and_Measurement\">Choose Hosting Through Protocol Checks and Measurement<\/span><\/h2>\n<p>Laravel Reverb\/Echo and Socket.IO offer different supported realtime paths. Choose the protocol first, then verify the complete publisher, queue, authorization and subscriber flow. Hosting must support all the required processes and network paths.<\/p>\n<p>A shared plan may work when its runtime, proxy and resource limits fit; a managed realtime service can also remain a long-term choice. A VPS gives more process and network control with added operating responsibility. Multiple VPS instances or a <a href=\"https:\/\/www.dchost.com\/dedicated-server\">dedicated server<\/a> may suit measured resource or availability needs, but one larger server does not create redundancy.<\/p>\n<p>Compare plans using concurrent connections, message frequency and size, fan-out, latency targets and colocated services. Include update ownership, log retention, backup recovery and reconnect behavior in the comparison. A tested application flow and measured operating margin are more useful than an unsupported connection-count promise.<\/p>\n<h2><span id=\"Primary_Technical_References\">Primary Technical References<\/span><\/h2>\n<ul>\n<li><a href=\"https:\/\/laravel.com\/framework\/docs\/13.x\/broadcasting\" rel=\"nofollow noopener\" target=\"_blank\">Laravel 13 \u2014 Broadcasting<\/a><\/li>\n<li><a href=\"https:\/\/laravel.com\/framework\/docs\/13.x\/reverb\" rel=\"nofollow noopener\" target=\"_blank\">Laravel 13 \u2014 Reverb<\/a><\/li>\n<li><a href=\"https:\/\/laravel.com\/framework\/docs\/13.x\/queues\" rel=\"nofollow noopener\" target=\"_blank\">Laravel 13 \u2014 Queues<\/a><\/li>\n<li><a href=\"https:\/\/socket.io\/docs\/v4\/\" rel=\"nofollow noopener\" target=\"_blank\">Socket.IO 4 \u2014 Introduction<\/a><\/li>\n<li><a href=\"https:\/\/socket.io\/docs\/v4\/middlewares\/\" rel=\"nofollow noopener\" target=\"_blank\">Socket.IO \u2014 Middleware<\/a><\/li>\n<li><a href=\"https:\/\/socket.io\/docs\/v4\/using-multiple-nodes\/\" rel=\"nofollow noopener\" target=\"_blank\">Socket.IO \u2014 Multiple Nodes<\/a><\/li>\n<li><a href=\"https:\/\/socket.io\/docs\/v4\/delivery-guarantees\/\" rel=\"nofollow noopener\" target=\"_blank\">Socket.IO \u2014 Delivery Guarantees<\/a><\/li>\n<li><a href=\"https:\/\/nginx.org\/en\/docs\/http\/websocket.html\" rel=\"nofollow noopener\" target=\"_blank\">Nginx \u2014 WebSocket Proxying<\/a><\/li>\n<li><a href=\"https:\/\/developers.cloudflare.com\/network\/websockets\/\" rel=\"nofollow noopener\" target=\"_blank\">Cloudflare \u2014 WebSockets<\/a><\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Compare shared hosting and VPS requirements for realtime applications. Build a Laravel Reverb\/Echo flow and a separate Socket.IO demo, with authorized channels, queued publishing, proxying and recovery planning.<\/p>\n","protected":false},"author":1,"featured_media":12711,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[184],"tags":[],"class_list":["post-3613","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-technology"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3613","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=3613"}],"version-history":[{"count":8,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3613\/revisions"}],"predecessor-version":[{"id":12713,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/3613\/revisions\/12713"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/12711"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=3613"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=3613"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=3613"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}