WebSocket Hosting in Production: The Complete Guide to Scaling Persistent Connections
How to host WebSocket servers in production — sticky sessions, connection limits, horizontal scaling with Redis pub/sub, and platforms that actually support it.
RaidFrame Team
January 23, 2026 · 7 min read
TL;DR — Most cloud platforms silently break WebSocket connections. Serverless doesn't support them. Edge functions time out after 30 seconds. If you need persistent connections in production, you need a platform that runs long-lived processes — not functions. Here's how to do it right.
Why are WebSockets so hard to host?
WebSockets are fundamentally different from HTTP. An HTTP request hits a server, gets a response, and the connection closes. A WebSocket connection opens and stays open — for minutes, hours, or days. That single difference breaks most modern hosting infrastructure.
Serverless functions have execution time limits. Edge networks terminate idle connections. Load balancers rotate traffic away from the server holding your connection state. Sticky sessions are required but rarely configured by default.
Which platforms DON'T support WebSockets?
| Platform | WebSocket Support | Limitation |
|---|---|---|
| Vercel (Serverless Functions) | No | 10-60s timeout, no persistent connections |
| Cloudflare Pages | No | Static hosting only, no server processes |
| Netlify Functions | No | Lambda-based, 10-26s timeout |
| AWS Lambda | No | Max 15 min, not designed for persistent connections |
| Vercel Edge Functions | Partial | 30s timeout kills most real-time use cases |
Wrong tool if you need a connection alive for more than a minute.
Which platforms actually support WebSockets?
You need persistent processes — a real server, not a function.
| Platform | Connection Limits | Sticky Sessions | Zero-Downtime Deploys | No Published Limits |
|---|---|---|---|---|
| RaidFrame | Configurable per container | Built-in by default | Yes — connections drain gracefully | Yes |
| Railway | No published limits | Manual (Nginx config) | Requires setup | Unclear |
| Fly.io | Soft limit ~10k/VM | Built-in | Yes | No — soft caps apply |
| Render | Limited on free tier | Manual | Basic | No — tier-dependent |
| Self-hosted VPS | OS-level (~65k per IP) | You manage it | DIY | Depends on config |
On RaidFrame, WebSocket apps deploy the same as any other container. No timeouts, no cold starts, no surprises.
How many concurrent connections per server?
A single container can handle 10,000 to 100,000+ concurrent WebSocket connections. The bottleneck is what you do per message, not the connection count.
Benchmarks per container (2 vCPU, 4 GB RAM):
| Use Case | Messages/sec | Concurrent Connections |
|---|---|---|
| Live notifications | ~100 | 50,000-100,000 |
| Chat application | ~5,000 | 20,000-50,000 |
| Real-time dashboard | ~10,000 | 10,000-30,000 |
| Multiplayer game (30 tick) | ~50,000 | 1,000-5,000 |
Each open connection consumes roughly 2-8 KB of memory. The real cost is your application state per connection.
WebSocket server with health checks: Node.js
import { WebSocketServer } from "ws";
import { createServer } from "http";
const server = createServer((req, res) => {
if (req.url === "/health") { res.writeHead(200); res.end("ok"); return; }
res.writeHead(404); res.end();
});
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; });
ws.on("message", (data) => {
for (const client of wss.clients) {
if (client !== ws && client.readyState === 1) client.send(data);
}
});
});
// Heartbeat — detect and terminate dead connections
const heartbeat = setInterval(() => {
for (const ws of wss.clients) {
if (!ws.isAlive) { ws.terminate(); continue; }
ws.isAlive = false;
ws.ping();
}
}, 30_000);
process.on("SIGTERM", () => { clearInterval(heartbeat); wss.close(() => server.close()); });
server.listen(process.env.PORT || 8080);The /health endpoint is critical — without it, your platform can't verify your server is alive and will restart healthy containers.
WebSocket server: Go (gorilla/websocket)
var upgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}
func handleWS(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { return }
defer conn.Close()
for {
msgType, msg, err := conn.ReadMessage()
if err != nil { break }
conn.WriteMessage(msgType, msg)
}
}Go's goroutine-per-connection model handles tens of thousands of connections with minimal overhead.
WebSocket server: Python (websockets)
import asyncio, websockets
connected = set()
async def handler(websocket):
connected.add(websocket)
try:
async for message in websocket:
await asyncio.gather(*[ws.send(message) for ws in connected if ws != websocket], return_exceptions=True)
finally:
connected.discard(websocket)
asyncio.run(websockets.serve(handler, "0.0.0.0", 8080))Try RaidFrame free
Deploy your first app in 60 seconds. No credit card required.
Load balancing WebSocket connections
Standard round-robin breaks WebSockets. The HTTP upgrade hits Server A, the connection lives on Server A, and the load balancer has no idea. You need sticky sessions — the load balancer routes a client to the same backend for the connection's lifetime. On RaidFrame, sticky sessions are enabled by default.
Scaling horizontally with Redis pub/sub
One server only knows its own connections. Client A on Server 1 can't message Client B on Server 2. The fix: Redis pub/sub as a message broker.
import { WebSocketServer } from "ws";
import { createClient } from "redis";
const pub = createClient({ url: process.env.REDIS_URL });
const sub = pub.duplicate();
await pub.connect(); await sub.connect();
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
ws.on("message", (data) => { pub.publish("chat", data.toString()); });
});
await sub.subscribe("chat", (message) => {
for (const client of wss.clients) {
if (client.readyState === 1) client.send(message);
}
});Every server publishes to Redis, every server subscribes. This scales linearly — add servers, handle more connections. On RaidFrame, add Redis with rf add redis and your REDIS_URL is injected automatically.
Client-side reconnection with exponential backoff
Connections will drop. Your client must reconnect gracefully:
function createSocket(url) {
let ws, retries = 0;
function connect() {
ws = new WebSocket(url);
ws.onopen = () => { retries = 0; };
ws.onclose = () => {
if (retries < 10) setTimeout(connect, Math.min(1000 * 2 ** retries++, 30000));
};
}
connect();
return { getSocket: () => ws };
}Without exponential backoff, a thousand clients reconnecting simultaneously after a deploy will overwhelm your server.
Deploying a WebSocket app on RaidFrame
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]rf init
rf deploy --port 8080 --health-check /healthRaidFrame builds the container, routes WebSocket traffic to port 8080, and uses /health for liveness checks. Sticky sessions are automatic. Zero-downtime deploys drain existing connections before terminating old containers.
Common patterns
Chat and messaging — rooms map to Redis pub/sub topics. User presence tracked with TTL-based expiry.
Real-time dashboards — server pushes data to clients. Unidirectional. Scales easily.
Multiplayer games — highest performance demands. See our game server hosting architecture guide for tick rates and state sync.
Live notifications — simplest pattern. Push events, reconnect on drop. 100k+ connections per server.
FAQ
Can I use Socket.IO instead of raw WebSockets?
Yes. Socket.IO adds reconnection, rooms, and namespaces. For horizontal scaling, use @socket.io/redis-adapter.
How do I handle authentication?
Authenticate during the HTTP upgrade request. Pass a JWT as a query parameter or in the Sec-WebSocket-Protocol header. Reject unauthorized connections with a 401 before the handshake completes.
What happens to connections during a deploy?
On RaidFrame, deploys are zero-downtime. New containers pass health checks before accepting traffic. Old containers drain existing connections gracefully (default 30s).
Should I use WebSockets or Server-Sent Events (SSE)?
SSE for server-to-client only (notifications, dashboards) — simpler and works on serverless. WebSockets for bidirectional (chat, games, collaboration).
How much does WebSocket hosting cost?
You pay for compute, not per-connection. A server handling 10,000 connections costs the same as one handling 100 on RaidFrame. Dramatically cheaper than serverless for real-time workloads. See our auto-scaling guide.
Related reading
Ship faster with RaidFrame
Auto-scaling compute, managed databases, global CDN, and zero-config CI/CD. Free tier included.