Back to tutorials
Tutorial

WebSocket Setup Guide Tutorial (2026): Run Real-Time Apps Behind Nginx on a VPS Without 101 Errors

WebSocket setup guide tutorial for Nginx on a VPS: fix 101/400 errors, tune timeouts, and verify with real tests in 2026.

By Anurag Singh
Updated on Jun 15, 2026
Category: Tutorial
Share article
WebSocket Setup Guide Tutorial (2026): Run Real-Time Apps Behind Nginx on a VPS Without 101 Errors

WebSockets tend to fail in boring, repeatable ways. You never get 101 Switching Protocols. Connections die around the 60‑second mark. Users see “Disconnected” at random. This WebSocket setup guide tutorial shows a production-style Nginx setup on a VPS, plus test commands that prove the upgrade handshake works and stays up.

The examples assume an Ubuntu 24.04/26.04-class server. They translate cleanly to Debian 12/13.

The goal is simple: run a real-time app (chat, notifications, dashboards) behind Nginx. It should survive proxies, idle timeouts, and TLS.

What you’ll deploy (and why this is different from a generic reverse proxy)

Most reverse-proxy guides stop once HTTP requests reach your backend. WebSockets keep a long-lived TCP connection per client. That makes details you can ignore for normal HTTP matter a lot:

  • Upgrade headers must pass through Nginx unchanged.
  • Timeouts must allow “idle but healthy” connections.
  • HTTP/2 vs HTTP/1.1 matters: browsers generally use HTTP/1.1 for classic WebSockets, even if your site supports HTTP/2.
  • Proxy buffering can add latency or break streaming-style flows.
  • Load balancing may need stickiness (or shared session state) depending on your framework.

If you want an isolated environment with predictable performance, a HostMyCode VPS is a clean base.

If you’d rather not handle Nginx hardening and service tuning yourself, managed VPS hosting can make production rollouts less stressful.

Prerequisites checklist (5 minutes)

  • A VPS with Ubuntu and root/sudo access
  • A domain pointed to the VPS (A/AAAA record)
  • Nginx 1.24+ installed (Ubuntu packages are fine)
  • A backend WebSocket service listening locally (examples below)
  • Ports open: 80 and 443 to the world; backend port open only to localhost

If your DNS is still propagating, or you want a safer cutover, see Server Migration Tutorial (2026). The same low TTL, validation, and rollback habits also help real-time deployments.

Step 1: Install Nginx and create a clean vhost

On Ubuntu:

sudo apt update
sudo apt install -y nginx

Create a dedicated server block. Replace example.com with your domain:

sudo nano /etc/nginx/sites-available/example.com

Start with a minimal HTTP listener. For now, it only handles ACME validation and redirects to HTTPS:

server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;

  location /.well-known/acme-challenge/ { root /var/www/html; }

  location / {
    return 301 https://$host$request_uri;
  }
}

Enable it and validate syntax:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Step 2: Bring up a local WebSocket backend (quick test service)

You can point Nginx at your real app (Node, Python, Go). For a fast, known-good target, spin up a tiny Node WebSocket server first.

sudo apt install -y nodejs npm
mkdir -p ~/ws-demo && cd ~/ws-demo
npm init -y
npm install ws

Create server.js:

nano ~/ws-demo/server.js
const http = require('http');
const WebSocket = require('ws');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('OK\n');
});

const wss = new WebSocket.Server({ server, path: '/ws' });

wss.on('connection', (ws, req) => {
  ws.send('connected');
  ws.on('message', (msg) => ws.send('echo: ' + msg));
});

server.listen(3000, '127.0.0.1', () => {
  console.log('WS demo on http://127.0.0.1:3000 (path /ws)');
});

Run it (keep it in a screen/tmux session for now):

node ~/ws-demo/server.js

Confirm it’s only listening on localhost:

ss -lntp | grep 3000

Step 3: Configure Nginx for WebSocket upgrade correctly

This is the core of the WebSocket setup guide tutorial. Most “it connects but doesn’t upgrade” issues come from missing Upgrade/Connection headers. Another common cause is proxying upstream as HTTP/1.0.

The other big failure mode is timeouts. Values that work for normal HTTP can be brutal for long-lived sockets.

Create a small include for the WebSocket headers so your vhost stays readable:

sudo nano /etc/nginx/snippets/websocket-headers.conf
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# WebSockets and streaming behave better without proxy buffering
proxy_buffering off;

Now add an upstream block and an HTTPS server block. Edit your vhost:

sudo nano /etc/nginx/sites-available/example.com

Add this above the server {} blocks:

upstream ws_backend {
  server 127.0.0.1:3000;
  keepalive 32;
}

Then add an HTTPS server block (the certificate comes in the next step):

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name example.com www.example.com;

  # Cert paths will be filled by Certbot
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  # Good defaults; you can tune later
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;

  # WebSocket endpoint
  location /ws {
    include /etc/nginx/snippets/websocket-headers.conf;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;

    proxy_pass http://ws_backend;
  }

  # Regular HTTP traffic (optional)
  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_pass http://ws_backend;
  }
}

Test the config. Nginx will complain about missing cert files until you install them. That is expected.

If you prefer a clean nginx -t right now, temporarily comment the SSL certificate lines.

Step 4: Add TLS with Let’s Encrypt (and avoid WebSocket + SSL pitfalls)

Install Certbot for Nginx:

sudo apt install -y certbot python3-certbot-nginx

Issue the certificate:

sudo certbot --nginx -d example.com -d www.example.com

Certbot can update your server block for you. After it finishes, validate and reload:

sudo nginx -t
sudo systemctl reload nginx

If you want a deeper TLS walkthrough (including challenge failures and renewal checks), use SSL Certificate Setup Guide (Tutorial) for Ubuntu VPS.

Step 5: Verify the WebSocket handshake from the command line

You should still test in a browser. But CLI checks fail fast and tell the truth.

Option A: Test with wscat

Install wscat:

sudo npm i -g wscat

Connect:

wscat -c wss://example.com/ws

You should get a “connected” message from the demo server. Send a message and expect an echo:

> hello
< echo: hello

Option B: Confirm a 101 response with curl (handshake view)

curl isn’t a full WebSocket client. It is still great for confirming the server is willing to upgrade:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Host: example.com" \
  -H "Origin: https://example.com" \
  -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  -H "Sec-WebSocket-Version: 13" \
  https://example.com/ws

You’re looking for HTTP/1.1 101 Switching Protocols. If you see 400 or 426, jump to the troubleshooting section. Then work through it systematically.

Step 6: Tune timeouts so real users don’t drop every minute

WebSockets usually get killed by “helpful” timeout defaults in two places:

  • Nginx proxy timeouts: proxy_read_timeout is the one that bites most often.
  • Upstream app timeouts: many frameworks close idle sockets unless ping/pong traffic keeps them active.

A practical baseline for many apps:

  • proxy_read_timeout 3600s; (or 600s in stricter environments)
  • proxy_send_timeout 3600s;

If you’re behind extra layers (a CDN, corporate proxy, or another load balancer), expect to add application-level keepalive pings every 20–30 seconds. That’s normal.

Middleboxes drop quiet connections.

Step 7: Lock down the firewall (only expose what you mean to)

A WebSocket-enabled site still only needs 80/443 open publicly. Your backend port (3000 in this demo) should stay private.

On Ubuntu with UFW:

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Do not open port 3000. In this demo it listens on 127.0.0.1, so it’s not reachable from the internet anyway.

For a more hosting-oriented baseline (including safer SSH rules), follow UFW Firewall Setup Tutorial (2026).

Step 8: Make your WebSocket app survive reboots (systemd service)

A Node process running in a terminal is fine for a demo. In production, use a systemd unit.

A unit restarts the process cleanly. It also brings the app back after reboots.

Create a system user and a service directory:

sudo useradd -r -s /usr/sbin/nologin wsapp || true
sudo mkdir -p /opt/ws-demo
sudo cp -r ~/ws-demo/* /opt/ws-demo/
sudo chown -R wsapp:wsapp /opt/ws-demo

Create a systemd unit:

sudo nano /etc/systemd/system/ws-demo.service
[Unit]
Description=WebSocket Demo Service
After=network.target

[Service]
Type=simple
User=wsapp
WorkingDirectory=/opt/ws-demo
ExecStart=/usr/bin/node /opt/ws-demo/server.js
Restart=on-failure
RestartSec=2

# Basic hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now ws-demo
sudo systemctl status ws-demo --no-pager

Tail logs while you test connections:

journalctl -u ws-demo -f

Step 9: Add a few Nginx settings that prevent subtle WebSocket breakage

Open /etc/nginx/nginx.conf (or a file under /etc/nginx/conf.d/). Make sure these look sane in the http {} block:

# If you use variables in proxy_pass in other vhosts, this can matter.
# For this tutorial, proxy_pass is static, so it’s optional.
# resolver 1.1.1.1 1.0.0.1 valid=300s;

# Avoid compression issues on upgraded connections
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

# Keepalive improves reuse for normal HTTP requests
keepalive_timeout 65;

One setting people often misread is client_max_body_size. It does not cap WebSocket message size the way many assume.

Message limits usually live in your app/framework, not Nginx. Don’t “fix” WebSockets by inflating upload limits unless you know what you’re solving.

Step 10: Troubleshooting: fix the 6 errors you’ll actually see

Use this like a small runbook. Check one thing at a time. Keep the commands close.

1) WebSocket fails with 400 Bad Request

  • Cause: Upgrade headers missing or overwritten.
  • Fix: Confirm proxy_http_version 1.1 and both Upgrade and Connection headers are set in the WebSocket location.
  • Check: sudo nginx -T | sed -n '/location \/ws/,/}/p'

2) You never see HTTP 101 Switching Protocols

  • Cause: The request never hits your WebSocket location (wrong path), or the upstream isn’t speaking WebSocket on that path.
  • Fix: Verify the exact path your app expects (/ws in this demo). Match it exactly.
  • Check: curl -i https://example.com/ws and inspect Nginx access logs.

3) Connection drops at ~60 seconds or 120 seconds

  • Cause: A timeout somewhere (Nginx, CDN, upstream framework) kills idle sockets.
  • Fix: Raise proxy_read_timeout. Add ping/pong at the application layer every 20–30 seconds.
  • Check: sudo tail -f /var/log/nginx/error.log for upstream timeout messages.

4) 502 Bad Gateway during peak traffic

  • Cause: The upstream restarts, runs out of file descriptors, or hits connection limits.
  • Fix: Check worker_connections, OS limits, and the app’s max connections. Scale up, or split real-time traffic to a dedicated instance.
  • Check: sudo journalctl -u ws-demo --since "10 min ago" and ulimit -n for the service user.

5) Mixed content or “blocked insecure WebSocket” in browser console

  • Cause: Your site loads over HTTPS but the client code connects with ws://.
  • Fix: Use wss:// (or derive from window.location.protocol).

6) Works on direct IP, fails on domain

  • Cause: DNS points somewhere else, or the wrong vhost is catching the request.
  • Fix: Confirm A/AAAA records, and that server_name matches. Use a low TTL while making changes.
  • Check: dig +short example.com and sudo nginx -T | grep -R "server_name example.com" -n

Step 11: Operational checklist for production WebSockets

  • Capacity: estimate concurrent connections and RAM overhead (each socket costs memory in your app and kernel structures).
  • Limits: set sane worker_connections and OS fd limits; avoid defaults on busy nodes.
  • Monitoring: watch Nginx 4xx/5xx and upstream timeouts; alert on sudden connection churn.
  • Logging: keep error logs useful; avoid debug logs on busy nodes.
  • Security: rate-limit abusive endpoints (especially unauthenticated connection attempts), and keep dependencies patched.

If your node fills up on disk while you debug connection churn, fix it fast. This guide walks through it: Disk Space Troubleshooting Tutorial (2026).

Summary: a stable WebSocket deployment on a hosting VPS

You now have Nginx terminating TLS, routing requests, and proxying WebSockets to a backend bound to localhost.

The important pieces are in place: correct Upgrade headers, buffering disabled for the WebSocket path, and timeouts long enough for real users.

Keep the verification commands nearby. They save time during DNS changes, migrations, and certificate renewals.

For production, run this on a server with predictable CPU and network headroom. A HostMyCode VPS is a solid fit for real-time workloads, and managed VPS hosting makes sense if you want help with updates, monitoring, and recovery when something breaks at 2 a.m.

If you’re rolling out WebSockets for chat, notifications, or a real-time dashboard, start with a VPS that stays stable under long-lived connections. HostMyCode offers HostMyCode VPS plans sized for steady throughput. If you’d rather offload patches, monitoring, and routine server maintenance, choose managed VPS hosting.

FAQ

Do WebSockets work over HTTP/2 on Nginx?

Most browsers still use classic WebSockets over HTTP/1.1 with an Upgrade handshake. Nginx can serve your site over HTTP/2 while proxying WebSockets on a separate HTTP/1.1 upgraded connection.

What’s the right Nginx timeout for WebSockets?

proxy_read_timeout is the key setting. Start at 10–60 minutes for real-time apps, then add application pings to keep connections active across middleboxes.

Should I enable proxy_buffering for WebSockets?

No. For WebSockets and streaming-style responses, buffering often adds latency or causes message delivery issues. Keep proxy_buffering off for the WebSocket location.

How do I confirm my client is actually using WebSockets?

Use browser DevTools Network tab (WS filter) to see frames, or connect with wscat and verify message round-trips. Also check Nginx access logs for a 101 response.

Can I run WebSockets on shared hosting?

Typically not reliably. Shared hosting often restricts long-lived connections and custom daemons. A VPS is the normal choice for WebSockets because you control Nginx, ports, and the backend process.