Machines
The core resource — sizes, states, the lifecycle, and the event log.
A machine is a persistent Linux computer with a desktop, a shell and a browser. It keeps its disk across a stop and a start, and loses it only when it is deleted.
Sizes
Sizes are named rather than free-form. The concrete numbers are server-owned, so they can change without a client release and so quota and billing have a fixed set to reason about.
| Size | vCPU | Memory | Disk | Weight |
|---|---|---|---|---|
small |
2 | 4 GB | 10 GB | 0.5 std-hours |
standard |
4 | 8 GB | 25 GB | 1 std-hour |
large |
8 | 16 GB | 50 GB | 2 std-hours |
The weight is what a size costs per hour of runtime, expressed in standard-hours. There is one
compute price and the per-size rates are derived from it, so they cannot drift apart. Read the
live numbers from GET /v1/plans rather than hardcoding them — no price or weight is
hardcoded in any client, which is what lets an older SDK display a newer catalog.
Your plan caps the largest size you may create. See plans and billing.
States
creating → running ⇄ stopping → stopped → starting → running
↘ error ↘ deleting
| State | Meaning |
|---|---|
creating |
accepted; the substrate has not reported it running yet |
running |
booted. not a promise that the desktop answers |
stopping |
shutting down; the disk is kept |
stopped |
not running, disk intact, still billed for storage |
starting |
booting from its existing disk |
error |
a rest state, not a transient one |
deleting |
on its way out; the disk is going |
Transitions are guarded in the database — the update only applies if the row is still in the
state that was read — so two concurrent lifecycle calls cannot both win. An illegal transition
is a conflict naming both states.
Running is not ready
POST /v1/machines returns once the machine is running. A machine has a working shell before
it has a working desktop, so running says nothing about whether the screen routes will answer.
const machine = await client.machines.create({ size: "small" });
const display = await machine.waitForDesktop();
waitForDesktop polls GET /v1/machines/{id}/display until it reports up. Until then that
route answers compute_unavailable, which is a normal boot state to wait through rather than a
failure to report — which is why the SDKs treat it as retryable.
Creating
await client.machines.create({
name: "research", // lowercase, digits and dashes; generated when omitted
size: "small",
template: "tpl_...", // start from a template's captured disk
region: "us-east", // a placement hint
metadata: { owner: "ana" },
start: true, // false is born `stopped`
});
metadata is caller-defined and echoed back unchanged: up to 64-character keys and
1024-character values.
A template pins the image; the size is still this call’s to choose. See persistence.
Stopping, starting, deleting
- Stop keeps the disk. A stopped machine costs storage and no compute.
- Start boots the existing disk. It is a normal boot, not a resume — nothing that was running survives.
- Restart is a stop and a start.
- Delete destroys the disk. Snapshots taken from it survive.
Containers
The machine ships podman with the docker CLI and compose v2. docker run, docker build,
volume mounts and localhost networking all work. Container port publishing does not, and it
fails without an error: the guest kernel lacks the netfilter support that port mapping needs, so
the runtime cannot install a mapping, and instead runs every container in the machine’s own
network namespace.
docker run -p 8080:80is silently dropped. The container comes up withNetworkMode: hostand an emptyPorts; nothing listens on 8080, and the service is on the machine’s localhost at the container’s own port instead.- a compose
ports:mapping on a compose-created network yields a connection that times out rather than one that is refused.
Write compose files for this machine with network_mode: host. That is how a stack is actually
reachable here: a service on the machine’s network namespace is directly reachable on its own
port — which is also the port to hand to published ports when you want
it reachable from outside.
Forking
const copy = await machine.fork({ name: "branch-a" });
Fork copies a disk into a second machine. Without a snapshot id the current disk is captured
first, which needs the machine running; with one, the fork stands on bytes that already exist.
Either way the fork comes back in creating, exactly like a create, so waitForDesktop is what
waits for a usable computer.
The fork owes the original nothing afterwards. Deleting the parent leaves it running.
The event log
for await (const event of machine.events()) console.log(event.type, event.created_at);
Newest first, cursor-paginated. The types are a closed set:
machine.created, machine.started, machine.ready, machine.stopped, machine.failed,
machine.updated, machine.deleted, session.connected, session.control_acquired,
session.control_released, session.disconnected, snapshot.started, snapshot.completed,
snapshot.failed, snapshot.deleted, machine.restore_started, machine.restore_completed,
machine.restore_failed, host.unhealthy.
The same events stream live over the gateway’s events channel.
What a machine never exposes
No substrate identifier, host address, environment id or guest bearer token appears in any response, and there is no route that would return one. That is a property of the API, not of the SDK wrapping it: the vocabulary is machines, screens, keyboards and terminals, all the way down.