WebSockets keep a connection open between browser and server for real-time features: chat, live dashboards, notifications, multiplayer games. In production, run them over TLS (wss://), usually with Nginx in front to handle certificates and route traffic.
Architecture
Browser ──wss://──▶ Nginx :443 (TLS) ──ws://──▶ app on 127.0.0.1:8080
Nginx terminates TLS and upgrades the connection; your app only listens on localhost.
1. A minimal WebSocket server (Node.js)
mkdir -p /opt/ws && cd /opt/ws
npm init -y && npm install ws
server.js:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ host: '127.0.0.1', port: 8080 });
wss.on('connection', (ws, req) => {
const ip = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress;
ws.isAlive = true;
ws.on('pong', () => (ws.isAlive = true));
ws.on('message', (data) => {
// broadcast to everyone
for (const client of wss.clients) if (client.readyState === 1) client.send(data.toString());
});
});
// Drop dead connections every 30 s
setInterval(() => {
for (const ws of wss.clients) {
if (!ws.isAlive) { ws.terminate(); continue; }
ws.isAlive = false;
ws.ping();
}
}, 30_000);
Add "type": "module" to package.json. Python (websockets), Go (gorilla/websocket, nhooyr/websocket) and others follow the same pattern.
2. Run it as a service
/etc/systemd/system/ws.service:
[Unit]
Description=WebSocket server
After=network.target
[Service]
User=wsapp
WorkingDirectory=/opt/ws
ExecStart=/usr/bin/node server.js
Restart=always
LimitNOFILE=65535
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
adduser --system --group wsapp && chown -R wsapp: /opt/ws
systemctl enable --now ws
LimitNOFILE matters: every open connection uses a file descriptor.
3. Nginx with TLS
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name ws.example.com;
listen 443 ssl;
http2 on;
# ssl_certificate lines added by certbot
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s; # idle connections stay open
proxy_send_timeout 3600s;
}
}
certbot --nginx -d ws.example.com
nginx -t && systemctl reload nginx
Test from your computer:
npx wscat -c wss://ws.example.com
4. Security
-
Authenticate on connect. Pass a short-lived token (e.g. in the query string or a cookie) and verify it in the
connectionhandler; close unauthenticated sockets immediately. -
Check the Origin header for browser clients to prevent cross-site WebSocket hijacking.
-
Validate every message — size, format and rate. Set
maxPayloadinwsto reject huge frames. -
Rate-limit connections per IP in Nginx:
limit_conn_zone $binary_remote_addr zone=wsconn:10m; # in location: limit_conn wsconn 20; -
Keep the app bound to
127.0.0.1and firewall everything except 80/443.
5. Scaling
- Connections per server: a small VPS can hold tens of thousands of idle WebSocket connections; CPU usage depends on message volume. Raise
worker_connectionsin Nginx and file limits in systemd. - Multiple instances: run several app processes and use Nginx
upstreamwithip_hash(or a shared pub/sub like Redis) so broadcasts reach clients on every instance. - Latency: host close to your users — USA, Hong Kong or Tokyo. For global audiences, regional servers sharing state via Redis or a message queue reduce round trips.
Behind Cloudflare
Cloudflare proxies WebSockets on all plans. Keep the idle timeout in mind (connections without traffic may be closed after a period of inactivity) and send pings regularly. See VPS + Cloudflare.
Summary
Run the WebSocket app on localhost, put Nginx in front for TLS and upgrades, raise timeouts and file limits, authenticate every connection and validate every message. Related: Nginx reverse proxy for Docker and Node.js on a VPS.