Skip to content

WebSockets

WebSockets work out of the box on StackBlaze. Long-lived connections are fully supported with no proxy timeout issues.

WebSocket connections are supported on all StackBlaze web services without any configuration. The Ingress controller passes Upgrade: websocket headers through to your service, and long-lived connections are maintained with a 3600-second proxy read timeout, far longer than any typical idle connection.

Server-Sent Events (SSE) are also fully supported with the same timeout settings.

All major WebSocket libraries work on StackBlaze without modification:

server.ts
import { WebSocketServer } from 'ws'
import { createServer } from 'http'
const server = createServer(app)
const wss = new WebSocketServer({ server })
wss.on('connection', (ws, req) => {
console.log('Client connected')
ws.on('message', (data) => {
// Echo back to sender
ws.send(data.toString())
})
ws.on('close', () => {
console.log('Client disconnected')
})
// Send a ping every 30s to keep the connection alive
const interval = setInterval(() => ws.ping(), 30_000)
ws.on('close', () => clearInterval(interval))
})
server.listen(process.env.PORT || 8080)
server.ts
import { createServer } from 'http'
import { Server } from 'socket.io'
const httpServer = createServer(app)
const io = new Server(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*',
methods: ['GET', 'POST'],
},
// Socket.IO will use WebSockets by default, falling back to polling
transports: ['websocket', 'polling'],
})
io.on('connection', (socket) => {
socket.on('join-room', (roomId) => {
socket.join(roomId)
})
socket.on('message', (roomId, data) => {
socket.to(roomId).emit('message', data)
})
})
httpServer.listen(process.env.PORT || 8080)
server.ts
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // Disable nginx buffering for SSE
})
const send = (data: object) => {
res.write(`data: ${JSON.stringify(data)}\n\n`)
}
send({ type: 'connected' })
const interval = setInterval(() => {
send({ type: 'heartbeat', timestamp: Date.now() })
}, 15_000)
req.on('close', () => {
clearInterval(interval)
})
})

A WebSocket connection is pinned to a single pod for its lifetime. When you scale your service to multiple replicas, each new connection is routed to a pod by the Kubernetes Service load balancer (round-robin), but existing connections stay on their current pod.

This means if you need to broadcast an event to all connected clients (e.g. a chat message), you must use a shared pub/sub mechanism so all pods can forward the event to their locally-connected clients.

WebSocket + Redis pub/sub A socket stays on one replica · each replica fans out to its local clients Client A pinned WS Client B pinned WS Client C pinned WS Replica 1 local clients A, B socket stays here Replica 2 local client C · fans out publish subscribe Redis pub/sub channel every replica hears the event New sockets round-robin across replicas io.emit and room broadcast work only after Redis pub/sub is wired

Broadcasting with Redis pub/sub (Socket.IO adapter)

Section titled “Broadcasting with Redis pub/sub (Socket.IO adapter)”
server.ts
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'
const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
await Promise.all([pubClient.connect(), subClient.connect()])
io.adapter(createAdapter(pubClient, subClient))
// Now io.emit() and socket.to(room).emit() work across all pods
io.emit('announcement', { message: 'Server update in 5 minutes' })
broadcast.ts
import { createClient } from 'redis'
import { WebSocketServer } from 'ws'
const publisher = createClient({ url: process.env.REDIS_URL })
const subscriber = publisher.duplicate()
await Promise.all([publisher.connect(), subscriber.connect()])
const wss = new WebSocketServer({ port: 8080 })
const clients = new Set<WebSocket>()
wss.on('connection', (ws) => {
clients.add(ws)
ws.on('close', () => clients.delete(ws))
})
// Subscribe to broadcast channel, runs on all pods
await subscriber.subscribe('broadcast', (message) => {
for (const client of clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(message)
}
}
})
// To broadcast from any pod:
export function broadcast(data: object) {
publisher.publish('broadcast', JSON.stringify(data))
}

StackBlaze sets a proxy read timeout of 3600 seconds (1 hour) on the Ingress controller. Connections idle for more than 1 hour will be closed by the proxy. To keep long-lived connections open, implement a heartbeat/ping mechanism in your application:

Client-side keep-alive
// Send a ping every 30 seconds from the client
const ws = new WebSocket('wss://my-service.stackblaze.app/ws')
const ping = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30_000)
ws.addEventListener('close', () => clearInterval(ping))