WebSockets on Voroa: send and receive data in real time
Run WebSockets on Voroa web services: framework setup, connecting over wss, keepalive pings, reconnecting after deploys, saving session state, and limits.
The WebSocket protocol enables real-time, two-way data streaming between a client and a server. It is commonly used for app features like text chat, live financial dashboards and AI voice assistants:

Voroa web services accept inbound WebSocket connections from the public internet. Every other service type except static sites can also open outbound WebSocket connections over the public internet.
Web service setup
In your web service code, you usually extend your existing HTTP server framework with WebSocket
support. In Node.js, for example, it is common to use the ws module with the Express framework.
Below are basic examples for popular frameworks; see your framework’s documentation for details. In
every case your server handles HTTP and WebSocket traffic on the same port, read from the PORT
environment variable that Voroa sets for you.
Express (Node.js)
This example uses Express with the ws module:
// app.js
const express = require('express')
const { createServer } = require('http')
const WebSocket = require('ws')
const app = express()
const server = createServer(app)
const port = process.env.PORT || 3000
// Serves WebSocket connections at /ws (any path is fine)
const wss = new WebSocket.Server({ server, path: '/ws' })
// HTTP routes
app.get('/', (req, res) => {
res.send('Hello over HTTP!')
})
// WebSocket connections
wss.on('connection', (ws) => {
console.log('WebSocket client connected')
ws.on('message', (message) => {
console.log('Received:', message.toString())
ws.send('Hello over WebSocket!')
})
})
server.listen(port, () => {
console.log(`Server listening on port ${port}`)
})
FastAPI (Python)
FastAPI supports WebSockets out of the box:
# main.py
from fastapi import FastAPI, WebSocket
app = FastAPI()
@app.get("/")
def index():
return {"message": "Hello over HTTP!"}
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
message = await websocket.receive_text()
print("Received:", message)
await websocket.send_text("Hello over WebSocket!")
Start command: uvicorn main:app --host 0.0.0.0 --port $PORT
Django (Python)
Django handles WebSockets through Channels, which requires an ASGI server such as Daphne or Uvicorn:
# myapp/consumers.py
from channels.generic.websocket import WebsocketConsumer
class EchoConsumer(WebsocketConsumer):
def connect(self):
self.accept()
def receive(self, text_data=None, bytes_data=None):
print("Received:", text_data)
self.send(text_data="Hello over WebSocket!")
Route it at ws/ in your ASGI routing, and use a start command such as
daphne -b 0.0.0.0 -p $PORT myproject.asgi:application.
Rails (Ruby)
Rails uses Action Cable, which serves WebSocket connections at /cable by default:
# app/channels/echo_channel.rb
class EchoChannel < ApplicationCable::Channel
def subscribed
stream_from "echo"
end
def receive(data)
ActionCable.server.broadcast("echo", { message: "Hello over WebSocket!" })
end
end
Connecting from clients
After you deploy WebSocket support to your web service, you can start opening connections from client code.
To test quickly, install the websocat command-line tool and connect
straight from your terminal:
$ brew install websocat
$ websocat wss://example-app.onvoroa.com/ws
test test
Hello over WebSocket!
Always use
wssfor WebSocket connections over the public internet. If you usews, most WebSocket clients fail, because Voroa answers their handshake request with a redirect to HTTPS. Usewsonly for testing a server on your own machine.
Here is a simple Node.js client that connects to a Voroa-hosted WebSocket server:
// client.js
const WebSocket = require('ws')
const ws = new WebSocket('wss://example-app.onvoroa.com/ws')
ws.onopen = () => {
ws.send('Hello from the client!')
}
ws.onmessage = (event) => {
console.log('Received:', event.data)
}
Whatever your language or framework, all you need is your web service’s public URL (custom domains work too), including the path of your WebSocket server.
Maintaining connections
Voroa does not enforce a maximum duration for WebSocket connections. A connection can still be interrupted by a deploy or restart, a network issue, or platform maintenance.
An idle connection is closed after about 100 seconds. A connection that carries no data at all for about 100 seconds can be closed on its way through the edge network. Send a ping at least every 30 seconds and this never happens.
To keep connections alive and detect stale ones, your web service and its clients should periodically
send each other keepalive messages. The WebSocket protocol
defines special ping and pong control frames for exactly this. When one side sends a ping, the
other side responds with a pong:

Many WebSocket libraries answer pong automatically, so you usually only need to implement the ping
logic on each side.
Server-side pings
On the server, periodic pings detect stale connections as early as possible, which frees up resources and keeps your service responsive.
The example below extends the Express example with a basic heartbeat using ping.
The same ideas apply to other languages and frameworks.
// app.js
const express = require('express')
const { createServer } = require('http')
const WebSocket = require('ws')
const app = express()
const server = createServer(app)
const port = process.env.PORT || 3000
const wss = new WebSocket.Server({ server, path: '/ws' })
// Called for a connection whenever the client responds with a pong
function heartbeat() {
this.isAlive = true
}
wss.on('connection', function connection(ws) {
ws.isAlive = true
ws.on('pong', heartbeat)
ws.on('message', (message) => {
console.log('Received:', message.toString())
ws.send('Hello over WebSocket!')
})
})
// Every 30 seconds, drop connections that did not answer the last ping, and ping the rest
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate()
ws.isAlive = false
ws.ping()
})
}, 30000)
wss.on('close', function close() {
clearInterval(interval)
})
server.listen(port, () => {
console.log(`Server listening on port ${port}`)
})
Adapted from the ws README.
Client-side reconnects
Your clients should reconnect to your service after an interruption. Handle both clean disconnects (your service closing the connection because it is being replaced) and unexpected errors (the connection going stale because of a network issue).
Reconnect with exponential backoff, so a service that is struggling is not flooded with reconnects.
Clients are not guaranteed to reconnect to the same instance. If your service runs more than one instance, each new WebSocket connection is assigned to one of them, whatever it was connected to before.
The longer example below shows client-side reconnects with exponential backoff in Node.js. The same ideas apply to other languages and frameworks.
// client.js
const WebSocket = require('ws')
const wsUrl = 'wss://example-app.onvoroa.com/ws'
let ws = null
let attempt = 0
const maxDelay = 30000 // never wait more than 30 seconds between attempts
function connect() {
ws = new WebSocket(wsUrl)
ws.onopen = () => {
console.log('Connected')
attempt = 0 // reset the backoff after a successful connection
ws.send('Hello from the client!')
}
ws.onmessage = (event) => {
console.log('Received:', event.data)
}
ws.onclose = () => {
// 1s, 2s, 4s, 8s ... up to maxDelay, plus jitter so many clients do not retry in lockstep
const delay = Math.min(maxDelay, 1000 * 2 ** attempt) + Math.floor(Math.random() * 1000)
attempt += 1
console.log(`Disconnected. Reconnecting in ${delay} ms`)
setTimeout(connect, delay)
}
ws.onerror = (err) => {
console.error('WebSocket error:', err.message)
ws.close() // onclose schedules the reconnect
}
}
connect()
Handling deploys and restarts
Voroa replaces your web service’s running instance when you deploy a new version, when you restart the service, and during platform maintenance.
When you deploy, Voroa starts the new version first and moves traffic to it once it is ready. The previous version is then stopped, and its open WebSocket connections close at that moment. There is no extra shutdown window to drain them, so do not rely on a shutdown signal to save state: save anything a client needs as you go (see below), and let clients reconnect. With the reconnect logic above, they are back on the new version within seconds.
Saving session state
If a connection is interrupted and your service has been keeping state for that client, store that state in a Voroa Key Value instance or other shared storage:

This way, whichever instance the client reconnects to can fetch the saved state and resume the session.
If you use this pattern, set a TTL on saved session state so it expires automatically once it is no longer needed.
FAQ
Can I receive WebSocket connections on a different port from other HTTP requests?
No. All public internet traffic to your web service is routed to a single port, the one your service
listens on (PORT). This includes WebSocket connections, which start as HTTP requests and are then
upgraded. Serve WebSockets on a path of the same server instead, such as /ws.
How long can a WebSocket connection stay open?
Until your service is deployed or restarted. Voroa does not impose a fixed limit, as long as the connection carries some data (or pings) at least every 100 seconds. See Maintaining connections.
Do WebSocket messages count as outbound bandwidth usage?
Some of them do. Outbound WebSocket messages from your services over the public internet count as outbound bandwidth. Inbound messages do not.
Is there a limit on the number of open WebSocket connections a service can have?
No. However, a large number of connections can strain your instance’s CPU and memory and slow it down. To handle more connections, move to a larger instance type or run more instances:
- A larger instance type has more CPU and RAM, so each instance can handle more connections.
- Scaling your service horizontally spreads connections across several instances, reducing the load on each. Each new connection is assigned to one of your instances.