Deployment
Deploying with Docker
uWebSockets.js is a native C++ addon, so your Docker image needs to match the platform it was compiled for. Build inside the container to be safe.
FROM node:22-trixie-slim AS build
# git is required - uWebSockets.js is installed from GitHub, not npm
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage - no git needed
FROM node:22-trixie-slim
WORKDIR /app
COPY --from=build /app/build build/
COPY --from=build /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
CMD ["node", "build"] With TLS:
CMD ["sh", "-c", "SSL_CERT=/certs/cert.pem SSL_KEY=/certs/key.pem node build"] With environment variables:
docker run -p 3000:3000 \
-e PORT=3000 \
-e ORIGIN=https://example.com \
my-app Important: Use Debian Trixie or Ubuntu 24.04+ based images (glibc >= 2.38). Bookworm-based images (
node:*-slim,node:*-bookworm) ship glibc 2.36 which is too old for uWebSockets.js. Don’t use Alpine either - uWebSockets.js binaries are compiled against glibc, not musl.
Clustering
The adapter supports multi-core scaling with two modes, auto-selected based on platform.
Set the CLUSTER_WORKERS environment variable to enable it:
# Use all available CPU cores
CLUSTER_WORKERS=auto node build
# Fixed number of workers
CLUSTER_WORKERS=4 node build
# Combined with other options
CLUSTER_WORKERS=auto PORT=8080 ORIGIN=https://example.com node build If a worker crashes, it is automatically restarted with exponential backoff. On SIGTERM/SIGINT, the primary tells all workers to drain in-flight requests and shut down gracefully.
Clustering modes
reuseport (Linux default) - each worker binds to the same port via SO_REUSEPORT. The kernel distributes incoming connections across all listening workers. There is no single-threaded acceptor bottleneck and no single point of failure - one worker crashing does not affect the others.
acceptor (macOS/Windows default) - a primary thread creates an acceptor app that receives all connections and distributes them to worker threads via uWS child app descriptors. Works on all platforms.
The mode is auto-detected. Override it explicitly if needed:
# Force acceptor mode on Linux (e.g. for debugging)
CLUSTER_MODE=acceptor CLUSTER_WORKERS=auto node build Setting CLUSTER_MODE=reuseport on non-Linux platforms is an error (SO_REUSEPORT is not reliable outside Linux).
WebSocket + clustering
platform.publish() is automatically relayed across all workers via the primary thread, so subscribers on any worker receive the message. This is built in - no external pub/sub needed.
If you add your own cross-process messaging (Redis, Postgres LISTEN/NOTIFY, etc.), pass { relay: false } to prevent duplicate delivery - your external source already fans out to every worker, so the built-in relay would double it.
Per-worker limitations (acceptable for most apps):
platform.connections- returns the count for the local worker onlyplatform.subscribers(topic)- returns the count for the local worker onlyplatform.sendTo(filter, ...)- only reaches connections on the local worker
Docker / multi-process deployments (Linux)
On Linux, SO_REUSEPORT is set on every app.listen() call - including single-process mode. This means multiple independent node build processes can bind to the same port without any adapter-level clustering. The kernel distributes connections across them.
If you already have external pub/sub (Redis, Postgres LISTEN/NOTIFY) handling cross-process messaging, you do not need CLUSTER_WORKERS at all. Just run multiple replicas and let your infrastructure handle the rest:
# docker-compose.yml
services:
app:
build: .
command: node build
network_mode: host
environment:
- PORT=443
- SSL_CERT=/certs/cert.pem
- SSL_KEY=/certs/key.pem
deploy:
replicas: 4 Each replica is a plain single-process node build. No coordinator thread, no built-in relay. Docker handles restarts, Redis handles cross-process messaging, the kernel handles port sharing.
With network_mode: host, containers share the host network stack directly - no port mapping needed, and services like Postgres and Redis are reachable via 127.0.0.1. This avoids Docker bridge DNS and gives the best network performance.
When to use what:
CLUSTER_WORKERS- single-machine deployments without Docker/k8s/systemd managing processes for you- Docker replicas - production deployments where your infrastructure already handles process management and you have external pub/sub for cross-process messaging
OS tuning for production
uWebSockets.js can handle hundreds of thousands of connections per process, but Linux defaults are conservative. For any deployment expecting more than a few hundred concurrent WebSocket connections, apply these settings on the host machine.
Kernel parameters
Add to /etc/sysctl.conf and run sysctl -p:
net.ipv4.tcp_max_syn_backlog = 4096 # pending TCP connection queue
net.ipv4.tcp_tw_reuse = 1 # reuse TIME_WAIT sockets faster
net.core.somaxconn = 4096 # listen() backlog limit
fs.file-max = 1024000 # system-wide file descriptor limit
net.netfilter.nf_conntrack_max = 262144 # connection tracking table size (default 65536 fills up fast under load, drops ALL new TCP including SSH)
net.ipv4.tcp_fastopen = 3 # TCP Fast Open for both client and server (saves 1 RTT on reconnecting clients)
net.ipv4.tcp_defer_accept = 5 # don't wake the app until data arrives (ignores port scanners and half-open probes) TCP Fast Open (tcp_fastopen = 3) lets a returning client send data in the SYN packet, eliminating one round-trip for the first request after a short idle. Browsers and HTTP clients that support TFO will use it automatically. The value 3 enables it for both incoming (server) and outgoing (client) connections.
TCP Defer Accept (tcp_defer_accept = 5) keeps the kernel from delivering the accepted socket to the application until data arrives. Port scanners, SYN probes, and clients that open a TCP connection but send nothing are handled at the kernel level rather than consuming event loop time. The value is the timeout in seconds before a data-less connection is dropped.
File descriptor limits
Add to /etc/security/limits.conf (takes effect on next login):
* soft nofile 1024000
* hard nofile 1024000
root soft nofile 1024000
root hard nofile 1024000 The wildcard * does not apply to the root user on most Linux distributions. If the app runs as root (common in Docker), the explicit root lines are required.
Docker ulimits
If running in Docker, the container also needs raised limits. Add to your docker-compose.yml:
services:
app:
ulimits:
nofile:
soft: 65536
hard: 65536 Without these changes, each process is limited to 1024 file descriptors (the default). Each WebSocket connection uses one file descriptor, so the default caps you at roughly 1000 concurrent connections per process. The server CPU can be well under 50% and you will still hit this ceiling - the bottleneck is the OS, not uWS or your application code.
For a deeper walkthrough, see Millions of active WebSockets with Node.js from the uWebSockets.js authors.
Stress testing: run it from the server
If you run a stress test from your local machine against a remote server, every WebSocket connection goes through your home router’s NAT table. Home routers typically have 1024 to 4096 NAT entries. Once the table fills up, the router drops ALL new outbound connections - not just your test, but SSH, your phone on WiFi, everything on your network.
Symptoms of NAT table exhaustion:
- Connection ceiling stuck around 1200-1900 regardless of server tuning
- SSH to the server times out during the test
- Other devices on the same WiFi lose internet access
- Server CPU is barely loaded (the server is fine, your router is not)
- Switching your phone from WiFi to mobile data works immediately
The fix: run the stress test from the server itself (localhost to localhost) or from a machine on the same network as the server. This bypasses NAT entirely and lets you hit the actual server limits.
Connection management (uWS defaults)
uWebSockets.js manages connection lifecycle at the C++ level. These are its built-in behaviors:
HTTP keepalive: uWS closes idle HTTP connections after 10 seconds of inactivity. This is compiled into the C++ layer and is not configurable from JavaScript. Behind a reverse proxy (nginx, Caddy, Cloudflare), the proxy manages keepalive for external clients; uWS handles only the proxy-to-app leg.
Slow-loris protection: uWS requires at least 16 KB/second of throughput from each HTTP client. Connections that send data slower than this (a common DoS technique) are dropped by the C++ layer before they reach your application code.
WebSocket ping/pong: Set idleTimeout in the adapter’s websocket option (in seconds) to have uWS send automatic WebSocket ping frames and close connections that don’t respond. The default is 120 seconds. The client store handles pong automatically.
// svelte.config.js
adapter({
websocket: {
idleTimeout: 120, // close WS connections silent for 120s
maxPayloadLength: 16 * 1024 * 1024 // max incoming WS message size
}
}) Was this page helpful?