VM billing and burn rate
How VM billing works: one bundled hourly rate per VM, locked when it starts, CPU and GPU sizing, and how long your balance lasts before auto-stop.
One bundled hourly rate
Every VM bills at a single hourly rate:
- GPU VMs: the per-GPU catalog price times the number of GPUs. A 3-GPU VM at $1.37/GPU-hr bills $4.11/hr.
- CPU VMs: a flat rate for the machine size.
CPU, RAM, and disk are included in that rate. Disk is not billed separately, and there is no per-GB storage charge.
CPU machine sizes
A CPU VM boots with exactly the vCPU and RAM its size carries:
resourceSize | vCPU | RAM |
|---|---|---|
small | 2 | 4 GB |
medium | 4 | 8 GB |
large | 8 | 16 GB |
xl | 16 | 32 GB |
GET /v1/pricing returns the same figures alongside each size's rate
(vcpu, ramMb), so read them from there rather than hardcoding this table.
resourceSize applies to CPU-only VMs. Omitting it on a CPU VM gives you
small; an unrecognised value is rejected with INVALID_RESOURCE_SIZE, and
sending it alongside a GPU count is rejected with
RESOURCE_SIZE_NOT_APPLICABLE.
GPU VM sizing
A GPU VM is sized from the host capacity its GPUs come with, not from a machine tier. Two fields size it, and omitting either asks the platform to pick:
| Field | Omitted (auto) | Floor | Ceiling |
|---|---|---|---|
guestMemMb | The per-GPU share of the tightest node that can still give a full one | 65% of the VRAM the VM is placed against | One node's per-GPU share times gpuCount, and its free RAM right now |
diskSizeGb | 100 GB per GPU | The platform minimum, a flat 50 GB | One node's per-GPU disk share times gpuCount, and its free space, and VM_DISK_MAX_GB |
A node's share is the RAM, disk and cores it sells (what it has, less what the host keeps for itself) divided evenly across its own GPUs, times the GPUs you asked for, so the node stays able to run one VM per GPU after yours lands. Auto takes the tightest node that can still give a full share, which keeps the RAM-rich nodes available to requests that need more than a small node's share.
The share bounds you as well as auto. An explicit guestMemMb or
diskSizeGb is capped at the node's per-GPU share times gpuCount, and then
answered against what is free there: a one-GPU VM on a five-GPU box gets a fifth
of the host, however idle the box is, because taking more would leave its other
four cards unsellable. A request above every node's share is refused outright; a
request inside a share that nothing has free right now is refused as transient.
A VM keeps the guest RAM it was placed with across stops and restarts, with one
exception. When the node its disk lives on offers less than that (its per-GPU
share has dropped, for example because the host now keeps more RAM for itself),
starting the VM after a stop, or re-dispatching it after its job is lost, starts
it at that node's current share, as long as the share still clears the floor, and requestedGuestMemMb is updated to match. A
fork of a snapshot is admitted the same way. The VM is never moved to another
node to keep its size, because its disk stays where it is.
The 100 GB per GPU figure is a default, not a floor: omit diskSizeGb and
you get it, name a smaller disk and you get that, down to the platform minimum
(minDiskGb on GET /v1/gpu-availability) and whatever the chosen node has
room for.
In the dashboard the boot disk is the Storage control beside the operating system. It lists the tiers the fleet actually runs: the platform minimum plus each node's own share for the GPU count you picked, so every choice is a real machine rather than an arbitrary number, and it is pre-selected at 100 GB per GPU. A tier no node can serve at this moment stays listed and is disabled. Guest RAM has no control: the dashboard sizes it at the tightest node's share for your GPU count, steps it down to a tier the fleet can serve right now, and shows that figure in the summary before you deploy.
The dashboard always sends both fields explicitly, pre-selected or not, so the figure it shows is the figure the VM boots with. Omitting them is still how a CLI or SDK caller asks the platform to size a VM; the difference is that the server then resolves them against whichever node it picks, whose per-GPU share may differ from any one figure a form could have displayed beforehand.
GET /v1/gpu-availability publishes what to ask for. Each class carries
floorPerGpu and maxPerGpu (the per-GPU range) and offers, one anonymous
entry per node with perGpu (its per-GPU share, which is both the auto value and
your ceiling), free (what is left on it right now), and the free GPU tiling. A
shape is placeable when one node tiles the GPU count and covers the request
on both axes, so evaluate a (count, RAM, disk) choice against each offer rather
than against fleet totals: a RAM-rich node and a disk-rich node do not add up to
a placement. Disk scales linearly, so an N-GPU ceiling is N x perGpu.diskGb;
RAM does not, because the hypervisor overhead is not linear in the count, so
derive it from perGpu.ramBudgetMb instead of multiplying perGpu.ramMb.
Two details the offers carry that are easy to misread. unitSizes is the tiling
any buyer may take and spotUnitSizes is capacity released from a reservation,
which only a tier=spot create reaches; a count is placeable at your tier only
if it sums out of the lists you can use. And a diskGb of 0 means the node has
not reported a live disk reading, so disk is not constraining placement on it: it
does not mean the node can hold nothing.
A request no node's share covers is rejected with VM_SHAPE_NOT_PLACEABLE (400)
and the message carries the largest share on offer; one inside a share that no
node has free right now is INSUFFICIENT_HOST_CAPACITY (503), which clears on
turnover. GET /v1/vms/{id} reports guestMemMb and guestVcpu, what
the VM is actually running with, alongside the requestedGuestMemMb it was
admitted at.
Rate locking
The rate is locked when the VM starts running. A catalog price change never re-rates a running VM; the locked rate is exactly what you are charged at settlement. A stopped VM has no locked rate, so quotes for it use the current catalog price.
Live burn: GET /v1/vms/{id}/burn
Returns the VM's rate, what the current session has cost so far, and your organization's overall burn:
curl https://api.openrelay.inc/v1/vms/{id}/burn \
-H "Authorization: Bearer or_your_api_key"{
"vmId": "4f6b9a1e-9f1c-4c58-a6a3-1f2f0f6f7a10",
"pricePerHourCents": 411,
"rateSource": "locked",
"rateBasis": "gpu",
"gpuCount": 3,
"perGpuCents": 137,
"diskBilled": false,
"accruedSessionCents": 1028,
"sessionStartedAt": "2026-07-18T09:12:00Z",
"orgAccruedCents": 2120,
"orgAvailableCents": 10430,
"orgBurnPerHourCents": 414,
"projectedRunwayHours": 25.07,
"autoStopFloorCents": 50
}rateSourceislockedwhile a usage session is open (the rate settlement will charge) andcatalogotherwise (the rate that would apply on the next start).accruedSessionCentsis the cost of the current session so far, computed with the same math as your monthly bill.orgBurnPerHourCentsis the sum of the locked rates of everything currently running in your organization.projectedRunwayHoursis how long your available balance lasts at the current burn, measured down to the auto-stop floor ($0.50 of available balance). When available balance drops below that floor and auto-recharge cannot top it up, running workloads are stopped automatically. It isnullwhen nothing is accruing.
From the CLI: orl vms burn get <vm-id>.
The same numbers appear in the dashboard on each VM's detail page (hourly rate, session cost) and on the Usage page (org burn rate and runway).