---
sidebar_position: 9
title: Executor availability WebSocket
description: Receive a snapshot and live updates for executors rentable on Lium.
---

> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lium.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 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:

```text
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`.

```ts
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

```json
{
  "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.

```json
{
  "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 }
}
```

```json
{
  "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 [OpenAPI spec](/developers/openapi).

```bash
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.
