Executor availability WebSocket
Use this public WebSocket to maintain a current list of executors that are rentable right now. It replaces repeated polling of GET /executors for availability changes.
Connect to:
wss://lium.io/api/executors/ws
The endpoint requires no API key. The server sends JSON text frames only; clients do not send messages.
Send a User-Agent header​
The CDN in front of lium.io rejects a request that has no User-Agent header. It answers 403 Forbidden before the request gets to the API. Set any non-empty User-Agent, and the handshake completes with 101 Switching Protocols.
To identify a 403 from the CDN, look at the Server header of the response. The CDN sends Server: CloudFront. The API always sends Server: uvicorn.
Browsers, curl, Python websockets and Go gorilla/websocket set the header for you. These clients do not set it, so you must add it:
| Client | Add the header |
|---|---|
| websocat | websocat -t -H='User-Agent: my-app/1.0' -B 2000000 wss://lium.io/api/executors/ws |
Node ws | new WebSocket(url, { headers: { 'User-Agent': 'my-app/1.0' } }) |
Rust tungstenite | Give connect a request from http::Request::builder().header("User-Agent", "my-app/1.0") |
websocat also needs the -B option, because the snapshot frame is larger than its default 64 KB buffer.
Keep a local executor map​
The first frame on every connection is always a complete snapshot. Replace local state with it. Then apply each upsert or delete by executor_id.
const executors = new Map<string, ExecutorAvailability>()
const socket = new WebSocket('wss://lium.io/api/executors/ws')
socket.onmessage = ({ data }) => {
const frame = JSON.parse(data)
switch (frame.type) {
case 'snapshot':
executors.clear()
for (const executor of frame.executors) executors.set(executor.id, executor)
break
case 'upsert':
executors.set(frame.executor_id, frame.executor)
break
case 'delete':
executors.delete(frame.executor_id)
break
case 'heartbeat':
break
}
}
Frames​
Every frame has these envelope fields:
| Field | Meaning |
|---|---|
type | snapshot, upsert, delete, or heartbeat |
schema_version | Protocol version. The current version is 1. |
seq | Monotonic sequence number within this WebSocket connection. |
ts | Server publish time as Unix seconds. |
Snapshot​
{
"type": "snapshot",
"schema_version": 1,
"seq": 42,
"ts": 1787572800.5,
"executors": [
{
"id": "ad83de56-8a04-4ab1-ac5a-4ab007b4e2b3",
"machine_name": "NVIDIA H200",
"price_per_gpu": 2.5,
"gpu_count": 8,
"available_gpu_count": 8,
"uptime_in_minutes": 4815,
"specs": { "gpu": { "count": 8, "details": [] } }
}
]
}
Upsert and delete​
An upsert adds an executor to the rentable set or replaces its full availability projection. Its executor object is the same projection the snapshot carries; the examples on this page show a few of its fields, not all of them. A delete means it is no longer rentable; the server does not expose the reason.
{
"type": "upsert",
"schema_version": 1,
"seq": 43,
"ts": 1787572801.0,
"executor_id": "ad83de56-8a04-4ab1-ac5a-4ab007b4e2b3",
"executor": { "id": "ad83de56-8a04-4ab1-ac5a-4ab007b4e2b3", "available_gpu_count": 4 }
}
{
"type": "delete",
"schema_version": 1,
"seq": 44,
"ts": 1787572802.0,
"executor_id": "ad83de56-8a04-4ab1-ac5a-4ab007b4e2b3"
}
heartbeat has only the envelope and confirms that the connection is still live.
Reconnects and compatibility​
There is no resume token. On every reconnect, discard any partial state when the next snapshot arrives and start applying subsequent frames from that snapshot. A later snapshot on an existing connection also replaces the full local state.
The server closes with WebSocket code 1013 when it is not ready, the connection limit is reached, or a slow client has fallen behind. Retry with exponential backoff.
Treat schema_version as a contract: reconnecting cannot make a client compatible with a higher version. Add support for the new version before accepting it.
The executor object is an availability projection. It intentionally excludes internal details and most high-churn telemetry; only use fields present in a received frame. uptime_in_minutes is included for marketplace filtering and sorting, but its minute-by-minute change does not trigger an upsert by itself.