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 removes it from the rentable set; 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.
Fetch a snapshot without a socket
The same address answers a plain GET. It returns one snapshot frame and closes. Use it when you want the rentable set once and do not want to hold a connection — a script, a scheduled job, or an agent that reads the API spec.
curl -fsS https://lium.io/api/executors/ws | jq '.executors | length'
The response is the snapshot the server published last. The server re-reads the full rentable set after each change in the database, and at least one time every 30 seconds. The timer starts again after each read, so the data is thus not older than about 30 seconds.
A 503 means that the server did not complete its first read. Retry.
The limit is 60 requests each minute for each IP address. A GET gives you the set when you ask for it; the WebSocket pushes each change as the server finds it. Also, seq orders the frames of one WebSocket connection only. Do not compare the seq of two GET responses.
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.