On this page
Beacon
How does the fleet know a host is alive without asking it? Each host publishes a small health document on a timer; every liveness verdict in Stado is a read of that document's contents and age, never an ssh probe.
What it is
A beacon is one JSON document per host: the host's own slug, a reported_at
timestamp, a disk line, and a units{} map with one entry per declared unit,
each carrying its state (stado host beacon collect). The unit list is not
fixed anywhere else — "the labels this host is judged on are the ones the
registry declares for it", matched by target name or declared hostname with
the .local suffix dropped. For each label the collector asks launchd in
both the GUI and system domains, or systemd, and a live process outranks
history: the previous run's exit code is read only when nothing is running
now. A unit loaded with no process whose last run exited nonzero is failed,
and its detail is the supervisor's own account of it, <unit> runs no process: its last run exited <code> and its supervisor has started it <runs> time(s) — a release that cannot serve, restarted without end — which stado service list and Stado Desktop's Managed services show beside the state.
On Linux, before publishing, the host reconciles each non-active unit with
systemd's own systemctl show state, so a timer-driven oneshot that ran or is
running reads scheduled and a unit systemd reports active reads active.
When that read cannot run or fails, the collector's state stands and the unit
carries lifecycle_unread with the command's cause, so a state left
unreconciled is told apart from one systemd confirmed. A trigger (the timer
of a oneshot) whose state cannot be read is listed in triggers_unread with
each read's cause, rather than skipped as if the unit had no such trigger.
A failed unit is also a crash loop Oko is told of once. The collector keeps
each failing unit in ~/.stado/failing-units.json under its label and, for a
loop no defect was recorded for yet, runs oko defects record --product stado
with the service, the host, when the loop was first read and the verdict.
What Oko answered joins the unit's detail as Oko defect: recorded: …, or
as why it did not record (Oko defect: oko defects record could not run: …,
Oko defect: oko defects record ended …), in which case the next pass tries
again; stado service list and Stado Desktop show it beside the state. A unit
read not failing ends its loop, so a later failure is a new defect; a unit
the pass could not read keeps its loop as it was. crash_loops_unrecorded is
null, or why the loop file could not be read or written, in which case no
unit's loop was raised. A unit the registry no longer declares on the host
drops its loop from the file on the next pass.
A beacon's roles block names the roles held on the host and the Stado role
processes nothing accounts for. Only the host process's own health pass ends
those, and its beacon lists the ones it ended under ended_strays. stado host beacon collect, with or without --publish, lists them under
unended_strays and ends none: run under another HOME, as a test or a
second account does, it finds no role lock for the machine's own resident and
would otherwise take that resident for a stray.
A host's beacons are published by the --health-interval-seconds role of
its stado serve process, at the period that option declares.
One process per role, and the roles block
Every long-running role of stado serve (the host process, with or without
--worker, --api, a resolver) takes a lock at start,
~/.stado/roles/<role>.lock, holding its pid, program and start time, for as
long as the process lives. A second start of the same role on the host is
refused before any listener binds: role serve already runs on this host: pid <pid>, program <path>, since <time>; one process holds a role, and a second start of the same role is never admitted beside it. A process whose program
is not the installed Stado (~/.stado/bin/stado) is refused the same way,
naming both programs: a checkout's build or a copy of the binary never serves
a role beside the installed one.
Each health pass then reads the host's process table for Stado role processes
nothing accounts for — a copy or a checkout's build running a role, a process
whose program no longer exists on disk, stado agent from a release that had
that role, a second holder of a role — ends each one (SIGTERM, then SIGKILL
while the kernel still reports it), and publishes what it found in the
beacon's roles block: held, one entry per role with its pid, program and
since, and ended_strays, one entry per process ended on this pass with
its pid, command, how long it had run, the reason (not_installed_program,
program_gone, retired_role, duplicate_role) and a detail sentence.
stado service list --json carries the same block on every unit row of the
host, so a stray is read from the beacon it was ended on, never from a
process list taken over ssh. The process table is read with ps; a host
whose ps cannot be run publishes the beacon without the block and says why
on the host process's stderr.
The next publication a beacon promises
A beacon the host process publishes (stado serve --health-interval-seconds)
carries next_by: the time by which that process will publish again, its own
period plus how long its last collection took. stado registry doctor reports
stale-beacon once that time has passed, and also for a beacon that states no
such time — one handed in by a one-shot stado host beacon publish or
beacon collect --publish, which promises nothing. A beacon written before
next_by existed is held to the stale_after_seconds it stated. No reader
applies a window of its own.
A worker's capacity broadcast (capacity/<consumer>.json) carries the same
field. The worker promises its next broadcast within its --poll-seconds
plus how long its last write took; while its tick works, its heartbeat
republishes the last measured snapshot for as long as the call the tick is in
has run no longer than that call has ever taken to return in this process.
A tick in a call slower than anything it has done before, or in a call it has
never finished, stops being spoken for, the promise passes, and every reader
(host gates, the scheduler, makespan placement, the live GPU ladder, the
build fallback) stops counting the host. A cloud consumer's row whose promise
has passed is deleted where it is found; a local host keeps its last row as
the evidence that it went quiet.
What a pass reads of its own disk, and the scans block
A health pass reads, for every declared unit, the init system's state and
launchd's event log for that unit. The event read is the costly half (one
log show per unit, seconds each), and a pass once a minute repeated it for
units that had not changed. The host process now reads the log for a unit
only when the unit is not as it was at this process's previous read — its
pid, launchd run count or state changed — and the host is under its scan
load ceiling; otherwise the previous read's events are carried, and the
unit's beacon entry says so in events: ok for a read, carried: the unit is as it was at the previous read, or carried: the host is above its scan load ceiling, the previous read stands. The first read of a unit in a
process reads the log whatever the load. The read itself runs in the host's
background band (taskpolicy -b on macOS).
Every scan of the host's own disk — the beacon's event reads
(unit_events), a preview of the janitor's build-cache walk (build_caches,
Space) and the inventory walk a space report
runs on its own host (disk_inventory) — is admitted against one ceiling:
the one-minute load average must not be above host.scan_load_ceiling
(WC_HOST_SCAN_LOAD_CEILING), or above the host's logical core count when
nothing is declared. A janitor pass on a volume at the full threshold walks
whatever the load, since that walk frees the disk, and its reading counts as
an admission. A beacon about this host carries a scans block:
last, the reading the latest admission was decided on (load, ceiling,
ceiling_source: declared or measured_cores; null while no scan has
asked), and skipped, one entry per scan standing skipped with the reading
that refused it, since the first refusal of the run and latest the
newest; an admitted scan leaves the map. stado host health <target> prints
it as one line: scans: load <load> against ceiling <ceiling> (<source>); <scan> skipped since <time>, or scans: no scan has asked yet before any scan
asked, and scans: not reported for a beacon from before the block; stado host link <target> prints the same line as scans: and carries the block as
scans in its --json document (null for a beacon from before the block),
which is where Stado Desktop's Hosts inspector reads it. A scan that is skipped is
not silently absent: the skip is published every pass until the scan runs
again.
Who declares it
The host itself. The host process collects local disk and service state and
publishes it through the scoped host-health API; stado host beacon publish FILE hands in a document collected elsewhere — an authenticated PUT
requiring STADO_HOST_HEALTH_API_URL plus the dedicated
stado-host-health-beacon Skarbiec grant. Missing routing, an unreadable or
over-broad grant, an insecure non-loopback HTTP URL, failed authorization,
and backend errors all leave the prior beacon untouched and return failure;
there is no cloud CLI, provider SDK, direct bucket URL, ambient credential, or
cross-backend fallback in the writer (operations).
Who observes it
stado host health <target>, stado host beacon list, stado registry doctor, and the read side of the service layer: stado service list joins
the declared managed set against the latest beacons and issues no ssh at all.
Independent host-beacon reads start together and retain registry order in the
result, so one slow store read does not postpone every other host. stado service status <name> first narrows the declarations and reads beacons only
for hosts that declare that service; a status lookup does not fetch unrelated
hosts or build their unit rows. The beacon join reports unknown when a host
has gone quiet; service status of a failed unit separately attempts its
exit and stderr reads, carrying those readers' refusals as failure evidence
(stado-rs/src/deploy/service/model/managed/status.rs). The autonomy cycle's
service reconciliation reads the same documents (operations).
The host's declared service answering probes reuse one HTTP client and
connection pool across beacon passes. After a standing-service recovery
attempt, Stado cancels its in-flight question before asking again; a hung
request cannot retain another task and connection after each attempt. A
completed declaration pass also cancels questions and drops answers for
services no longer declared on this host. An HTTP client initialization
failure appears as a refused cause in the unit's answering reading, not
as a healthy process or an absent question. See Service.
For a declared service.answering question, stado service list and
stado service status <name> print an answering: <host> <unit>: <reading>
line beneath their table. Their --json rows carry the same reading as
answering: asked, waiting (with elapsed wait and possibly last),
stood (with the host's declared bound), unreadable with the actual
catalog, directory or resolver cause that prevented asking, or finished
with a result whose state is answered (HTTP status, the HTTP library's
successful verdict, elapsed time and the response detail on failure) or
refused (the actual transport or request-construction cause). Stado Desktop's
Managed services table marks a standing, unreadable, refused or unsuccessful
HTTP answer, or one with no HTTP status even if it claims success (including
the last result while a new question waits), when process state is active.
A malformed answering value, including an answered result without its
successful verdict, remains in the CLI's raw JSON row alongside
answering_error; the plain table prints answering-error with the decode
cause and observed value. Desktop keeps the other fleet rows visible and
shows that row-local cause in its table and inspector, rather than treating
a missing verdict as a failed or successful HTTP answer.
The beacon's reported_at still governs freshness: a stale reading is
historical evidence, not proof that the API answers now.
The read-only real-host journey tests/service_answering/cli.mjs requires
STADO_BIN, STADO_TEST_SOURCE_REVISION, STADO_ANSWERING_HOST and
STADO_ANSWERING_NAME for a committed build and a declared service. It reads
the actual host beacon, a fresh stado host ping verdict, service status and
fleet list. It requires a finished answer or a waiting question with a last
answered HTTP result whose successful verdict is true, and checks an
unknown-name refusal. asked, stood, unreadable, refused, or an HTTP
error answer do not prove the API works; a stale beacon does not either. Its
command exits, raw responses and hashes are kept under
.build/service-answering-cli-*.
Readers resolve a target to its beacon object through the slug
rule in monitor/host_health.rs — target name, declared hostnames, and their
first dot-labels, in order.
Where it lives in the store
host_health/<slug>.json, inside the deployment's namespace — the latest
document only, one per host (monitor/host_health.rs). Because the prefix
holds only the latest document, a gap closes over itself the moment the host
comes back; the durable record of the gap is the silence record,
state/host_silence/<host>/<started_at>.json, opened when the newest beacon
crosses the threshold and closed by the first fresher beacon
(monitor/host_silence/transitions.rs).
Silence, and what stale means
A host counts as silent when its newest beacon is older than the fleet-wide
threshold: STADO_SILENCE_THRESHOLD_SECONDS, default 300 seconds — five
minutes, because at a one-minute publication interval three consecutive
misses is a host that has stopped talking and one miss is a slow pmset
call. A value that is not a positive integer falls back to the default rather
than disabling the detector (monitor/host_silence/transitions.rs). No
beacon at all is silent — a host that has never published is not a host that
is fine. A beacon stamped in the future is not silent: clock skew on the
publisher is not an outage.
A stale beacon cannot describe the present, so nothing derives a confident
verdict from it. A unit read through a stale beacon is unknown, never
active and never missing (deploy/service/model/managed/status.rs;
operations: "a stale beacon is unknown, never
missing"), and staleness never authorizes a host mutation, with the single
beacon-unit exception operations documents. Unit states a
fresh beacon can report: active, inactive, failed; missing means the
beacon exists and does not carry the unit; unknown means no beacon or an
empty state (deploy/service/model/vocabulary.rs).
Commands that act on it
stado host beacon publish <file-or-dash>
stado host health <target>
stado host beacon list
stado host ping <target>
stado host link <target>
host link answers "why did this host go quiet": beacon age, recorded
silences, and the reader refusals filed against it. Full flags in the
cli reference.
Not to be confused with
- A probe. A beacon travels outward from the host; nothing dials in.
Endpoint reachability is a separate observation (
stado service verify). - A silence record. The beacon is the latest state; the silence record is the durable history of its gaps (observation).
- The registry. The registry declares which units the host is judged on; the beacon reports what launchd or systemd actually says about them.
Source: this website