Learning objectives
After reading this article you will be able to:
- Run a Node.js WebSocket server with a heartbeat and a clean shutdown
- Configure a reverse proxy to accept Upgrade and not cut idle connections
- Plan client reconnection and scaling beyond a single instance
On this page
- What does a WebSocket server need from hosting?
- How do you write the server?
- How do you check that it works?
- How do you configure the reverse proxy?
- How should the client react when the connection drops?
- How do you scale beyond one instance?
- What errors show up most often?
- How to do this on Square Cloud
- FAQs
A regular HTTP server answers a request and forgets the client. A WebSocket server keeps hundreds or thousands of connections open at once, each for minutes or hours. That changes what matters in hosting: the process cannot sleep, the proxy cannot cut idle connections, and memory grows with the number of connected clients.
This guide shows how to run a server with the ws library, put it behind a proxy, and think about scaling. If the protocol is new to you, start with what a WebSocket is.
What does a WebSocket server need from hosting?#
Four things:
- An always-on process. Serverless platforms that stop the process after each request do not fit, because the connection dies with it. See serverless vs containers.
- A proxy that understands Upgrade. The handshake is an HTTP/1.1
GETwithUpgrade: websocketandConnection: Upgrade. The server answers101 Switching Protocols, and from then on the connection is no longer HTTP. - TLS. A page served over HTTPS can only open secure WebSockets. In production the client opens
wss://. - Memory proportional to connections. Each connection holds buffers and whatever state you attach to it. Load-test before choosing a server size.
How do you write the server?#
Install the dependency (the code uses ES modules, so set "type": "module" in package.json):
npm install wsThe server below listens on a port from the environment, broadcasts messages, detects dead connections with ping/pong, and shuts down cleanly on SIGTERM:
import { createServer } from "node:http";
import { WebSocketServer, WebSocket } from "ws";
const port = Number(process.env.PORT ?? 3000);
const server = createServer((req, res) => {
// Plain HTTP route for health checks
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok");
});
const wss = new WebSocketServer({
server,
path: "/ws",
maxPayload: 1024 * 1024, // 1 MiB; ws defaults to 100 MiB
});
function heartbeat() {
this.isAlive = true;
}
wss.on("connection", (ws) => {
ws.isAlive = true;
ws.on("pong", heartbeat);
ws.on("error", console.error);
ws.on("message", (data, isBinary) => {
for (const client of wss.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
}
});
});
// Ping every 30 s: keeps the proxy happy and removes dead connections
const interval = setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) {
ws.terminate();
continue;
}
ws.isAlive = false;
ws.ping();
}
}, 30_000);
wss.on("close", () => clearInterval(interval));
process.on("SIGTERM", () => {
// 1001 = "going away": clients know they should reconnect
for (const ws of wss.clients) ws.close(1001, "server restarting");
wss.close();
server.close(() => process.exit(0));
});
server.listen(port, () => console.log(`Listening on port ${port}`));Two details matter. maxPayload caps each message; the ws default is 100 MiB, which is far too much for a chat. And the heartbeat follows the library's own README: without it, a connection whose client vanished (phone out of signal, cable pulled) still looks open on the server.
How do you check that it works?#
Test locally with wscat:
npx wscat -c ws://localhost:3000/wsOpen two terminals, type a message in one and watch it arrive in the other. Then repeat against the public URL with wss://. If the connection fails with Unexpected server response: 400 or 200, the proxy is probably not forwarding Upgrade (next section).
How do you configure the reverse proxy?#
A reverse proxy receives public traffic and forwards it to your process. For WebSockets it must pass the Upgrade headers along. Example based on the nginx documentation:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 120s;
}
}On nginx versions before 1.29.7, also add proxy_http_version 1.1; inside the location block. The critical piece is the timeout: by default nginx closes the connection if the server sends no data for 60 seconds. Two defenses work together: raise proxy_read_timeout and keep the server ping interval below it. With a 30 s ping and a 120 s timeout, one lost ping drops nobody. Cloud load balancers have similar limits, so read the timeout of the layer you use.
How should the client react when the connection drops?#
Long connections drop: deploys, restarts, a phone switching networks. The client must reconnect on its own, with growing delays so it does not flood the server with simultaneous retries:
function connect(url, onMessage) {
let attempt = 0;
function open() {
const ws = new WebSocket(url); // built into browsers and current Node.js versions
ws.addEventListener("open", () => {
attempt = 0;
});
ws.addEventListener("message", (event) => onMessage(event.data));
ws.addEventListener("close", () => {
// 1 s, 2 s, 4 s... up to 30 s, with random jitter
const base = Math.min(1000 * 2 ** attempt++, 30_000);
setTimeout(open, base / 2 + Math.random() * (base / 2));
});
}
open();
}
connect("wss://example.com/ws", (data) => console.log(data));Jitter matters: if a thousand clients lose their connection in the same restart, without it they all retry in the same millisecond. After reconnecting, the client should also ask the server for the messages it missed, because WebSockets never resend anything on their own.
How do you scale beyond one instance?#
One instance handles a lot, but eventually you want two, for capacity or redundancy. The catch: each connection lives in a single process. If user A is on instance 1 and user B on instance 2, a broadcast on instance 1 never reaches B.
The usual fix is a shared bus. Each instance subscribes to a Redis publish/subscribe channel; when it receives a message from a client, it publishes to the channel, and every instance (itself included) delivers it to its local clients. Redis publish/subscribe delivers each message at most once: a subscriber that is disconnected at that moment loses it. That way any instance can serve any client, and the load balancer can spread connections however it likes.
Two rules help. First, keep the server free of local session state: store whatever must survive a crash in a database or Redis. Second, treat the connection list as disposable, because every connection is recreated on a deploy.
What errors show up most often?#
- Connections always drop at the same interval (60 s, for example): a proxy timeout. Send pings more often.
400 Bad Requeston the handshake: the proxy strippedUpgradeorConnection, orSec-WebSocket-Keynever arrived.- Browser blocks
ws://on an HTTPS page: usewss://. - Memory keeps growing: dead connections never removed (no heartbeat) or references kept in global maps after
close. - Works locally, fails in production: check the port being listened on, the path (
/ws), and whether the proxy accepts Upgrade on that path. - Origin not checked: browsers always send
Origin. Validate it on the server (answer403) so other sites cannot open connections on a user's behalf. Non-browser clients can fake that header, so authenticate with a token too.
How to do this on Square Cloud#
On Square Cloud, your application runs in an isolated container that stays on all the time, with no sleep mode, which covers the first requirement of a WebSocket server. With automatic restart (on by default for dashboard deploys), it comes back by itself when the process exits with an error. The platform network supports WebSockets and HTTP/3, with DDoS protection, and the proxy neither cuts long connections nor buffers them. Every web application gets a *.squareweb.app subdomain with HTTPS, so clients connect with wss:// without you setting up a proxy or a certificate.
You do the deploy: send your code by ZIP, CLI, GitHub or the VS Code extension and pick the runtime (Node.js, Python, Go and others). The squarecloud.app config file is optional, because the dashboard generates it at deploy time with runtime, memory and main file. Environment variables live in the dashboard and apply on the next restart, and real-time logs show connections and errors. The example already listens on the port in the PORT variable, which the platform sets; keep the server accepting connections on 0.0.0.0.
The plan's RAM is a pool shared across your applications, with a minimum of 512 MB per web application, so measure usage with real clients to size it. There is no autoscaling: to scale vertically, you change the application's RAM yourself.
For more than one instance, load balancing spreads a custom domain across several applications with round-robin and passive failover. With no sticky sessions, each reconnect may land on another instance. Redis hosting (from the Standard plan) accepts PUBLISH/SUBSCRIBE and can serve as the bus between them. Read more in API hosting.
API hosting
APIs in always-on containers, with no cold starts
FAQs
Do I need a separate port for WebSockets?
No. A WebSocket starts as an HTTP request and is upgraded, so it can share the port of your HTTP server. Ports other than 80 and 443 may cause problems with firewalls and proxies.
Why does my connection drop after 60 seconds without messages?
Usually a proxy in the path. nginx, for example, closes the connection if the server sends no data for 60 seconds (proxy_read_timeout). Sending a server ping every 30 seconds fixes it.
Do WebSockets work behind a load balancer?
Yes, but each connection is pinned to one instance. Messages between clients on different instances need a shared bus, such as Redis publish/subscribe.
Should I use ws:// or wss:// in production?
Use wss://, which is WebSocket over TLS. An HTTPS page cannot open ws:// (the browser blocks it as mixed content), and unencrypted traffic can be read along the way.
More in APIs
See all 15 articles- How to host an APIHosting an API means running its HTTP server around the clock at a public address with HTTPS. Step-by-step with Node.js and Python, testing and common errors.
- How to host a Node.js appTo host a Node.js app, prepare package.json, read the port and secrets from the environment, pick an LTS version and run it with node. Step-by-step guide.
- How to host a FastAPI, Flask or Django appTo host a FastAPI, Flask or Django app, run it with a production server (Uvicorn or Gunicorn) on 0.0.0.0. Commands, Django settings and common errors included.
- How to host a Python app or scriptTo host a Python app or script, pin the version, isolate dependencies in a venv, read secrets from the environment and handle SIGTERM. Step-by-step guide.