EngineeringWebSocketsreal-timescaling

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.

R

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?

PlatformWebSocket SupportLimitation
Vercel (Serverless Functions)No10-60s timeout, no persistent connections
Cloudflare PagesNoStatic hosting only, no server processes
Netlify FunctionsNoLambda-based, 10-26s timeout
AWS LambdaNoMax 15 min, not designed for persistent connections
Vercel Edge FunctionsPartial30s 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.

PlatformConnection LimitsSticky SessionsZero-Downtime DeploysNo Published Limits
RaidFrameConfigurable per containerBuilt-in by defaultYes — connections drain gracefullyYes
RailwayNo published limitsManual (Nginx config)Requires setupUnclear
Fly.ioSoft limit ~10k/VMBuilt-inYesNo — soft caps apply
RenderLimited on free tierManualBasicNo — tier-dependent
Self-hosted VPSOS-level (~65k per IP)You manage itDIYDepends 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 CaseMessages/secConcurrent Connections
Live notifications~10050,000-100,000
Chat application~5,00020,000-50,000
Real-time dashboard~10,00010,000-30,000
Multiplayer game (30 tick)~50,0001,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.

Start free

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 /health

RaidFrame 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.

WebSocketsreal-timescalingdeploymentNode.jsGoPython

Ship faster with RaidFrame

Auto-scaling compute, managed databases, global CDN, and zero-config CI/CD. Free tier included.