On this page
Reservation
A host runs more than queue jobs. Stado places a Jeden session on it with
stado workload attach and a browser task with stado workload run. Without a
hold, the host would keep publishing itself as free and could be handed any
number of sessions while the scheduler read it as idle. A reservation is the
hold a placed workload keeps on its host while it runs, so the host publishes
what is really left.
A detached session holds its host a different way. stado workload start
submits the session as a queue job carrying the kind's cpu_cores and
ram_gb as the job's own declared sizing, so the worker admits it only
while that much is really free and subtracts it for as long as the session
runs. There is no reservation document for it: the running job is the hold,
and it ends when the job does, with no caller left to heartbeat anything.
What it is
One JSON document per placed workload, under
state/reservations/<consumer_id>/<reservation_id>.json:
{
"schema_version": 1,
"reservation_id": "e241070e-407b-44b8-98a4-622b2b3c47a0",
"consumer_id": "local-example-host.local",
"target": "example-host",
"kind": "weles-diagnostics",
"product": "weles-worker",
"holder": "operator@workstation hold 2s pid 31618",
"cpu_cores": 1,
"ram_gb": 1.0,
"vram_gb": 0,
"acquired_at": "2026-09-17T06:10:02Z",
"heartbeat_at": "2026-09-17T06:10:02Z",
"ttl_seconds": 180
}
The sizes come from the workload declaration, never from the caller:
stado-rs/data/work/workloads.json carries a reservation per kind — a
Jeden session holds 2 cores and 4 GiB, a browser task 2 cores and 3 GiB, a
diagnostic read 1 core and 1 GiB. A kind that declares none is refused with
workload kind '<KIND>' declares no reservation; add it to stado-rs/data/work/workloads.json.
A reservation is live while heartbeat_at + ttl_seconds is in the future.
The holder heartbeats it every 60 seconds; the TTL is three heartbeats, so
one missed heartbeat is a slow store and three is a holder that is gone.
Who declares it
The path that places the work, immediately before it starts the process:
stado workload attach takes the hold after the host's readiness probe
passes and before jeden rpc starts; stado workload run takes it after
placement and before the runner; stado capacity hold takes it on purpose,
for a fixed time. All three go through one function, so a kind cannot be
placed without its hold. The hold is released when the process ends; a
process that dies without releasing leaves a row that expires by itself.
The holder starts with <account>@<host>, then names the path and its pid. The account is read
as a job's submitter is (see Jobs): USER, else LOGNAME, else
the process's account in the system's user database. When none can be read
the placing path refuses with that reason instead of taking a hold in a
made-up name; a refusal that cannot name its requester is not recorded under
state/fleet/unmet/, and stderr says why.
Before taking the hold, the placing path reads the host's latest capacity publication. A host whose net room is below the kind's declaration is refused with the sentence
<target> has no room for <kind>: needs <c> cores and <r> GiB, host publishes <c2> cores and <r2> GiB net of <n> reservation(s); pick another host with --target or wait for a reservation to end
and a host that publishes accepting_jobs: false is refused with
<target> is not accepting placements (<admission_reason>); pick another host with --target or wait for it to clear. A fresh publication that does not
state available_cpu_cores or accepting_jobs is refused with
<target> publishes no <field> in its capacity row, so whether the hold fits cannot be judged; update its agent or pick another host with --target,
because reading either as none would refuse for a reason the host never gave.
A free RAM or reservation count the publication leaves out is printed as
not reported in the no-room sentence. Every refusal is recorded under
state/fleet/unmet/ so stado fleet needs
can count them. An unpinned Jeden session moves on to the next candidate
host after a refusal, exactly as it does after a failed readiness probe.
A host with no fresh publication is taken on trust: its agent is silent, and refusing every placement on a quiet host would make the primitive a liability the day a store hiccups. The hold is still written, so the next publication subtracts it.
Who observes it
The host's own agent, on every poll. Both writers of a capacity document —
the agent tick and the heartbeat republisher — pass through one publish
function, which reads the host's live reservations and subtracts them:
available_cpu_cores, free_ram_gb and free_vram_gb are published net of
every live hold, the measured figures stay in diag.measured_available_cpu_cores,
diag.measured_free_ram_gb and diag.measured_free_vram_gb, and the
document gains running_workloads, reserved and reservations:
"running_workloads": 1,
"reserved": {"cpu_cores": 2, "ram_gb": 4.0, "vram_gb": 0},
"reservations": [{"reservation_id": "…", "kind": "jeden-session", "product": "jeden",
"holder": "operator@workstation jeden-session wisent-compute pid 4711",
"cpu_cores": 2, "ram_gb": 4.0, "vram_gb": 0, "acquired_at": "…"}]
When the net leaves less than one core or one GiB, the host publishes
accepting_jobs: false with diag.admission_reason: reservations_exhausted.
The scheduler, stado host gates and every other reader of the publication
see the same number; nothing reads the reservations a second way.
The agent also retires every expired row older than an hour, in the same
listing it reads the live ones from. A row that just
expired stays for the hour so stado capacity reservations can still show
what ended.
Where it lives
state/reservations/<consumer_id>/, in the canonical
object store. It is under state/ because that is the root
the object gateway authorizes for fleet state; a prefix no namespace policy
declares is refused with 401 unauthorized or non-immutable release write,
on every write, while reads keep working.
Commands
stado capacity list [--json] # every host, net of its reservations
stado capacity reservations [TARGET] [--json] # the holds, live or just ended
stado capacity hold --kind KIND --target TARGET --seconds N [--json]
list prints one row per publication: target, whether it accepts work, running
jobs, running workloads, CPU and RAM and VRAM free/total, what is reserved, the
admission reason and the publication's age. A field the publication does not
carry (one written before workloads or reservations were published) prints as
not reported, and is null with --json, instead of a zero or an empty
list. reservations prints the holds;
with no target it prints every host's, and <target> holds no reservations or
no host holds a reservation when there are none. hold takes a kind's
declared hold on a host, heartbeats it for N seconds and releases it,
printing the receipt with --json; it is refused with the sentences above
when the host has no room.
stado host gates TARGET prints a reserved: line and one line per hold when
the host holds any, and carries running_workloads, reserved and
reservations under capacity in its JSON.
Not to be confused with
- The lease — time-bounded ownership of one mutation subject, serializing writers. A reservation serializes nothing; it subtracts.
- The job claim — a queue job is counted by the agent that runs
it, in
running_jobs; a reservation is for work the queue does not run. - A grant — a credential capability. A reservation confers no authority; a workload with no grant is refused before it could hold anything.
Source: this website