Lifecycle and TTL
States, transitions, idle auto-stop, and why nothing runs forever.
States
creating → running → stopped → destroyed
↘ ↗
error| State | Meaning | Billing |
|---|---|---|
creating | Machine requested, image pulling, healthcheck pending | Compute, from start |
running | Healthcheck passed (or no healthcheck and the process is up) | Compute per second |
stopped | Machine stopped, rootfs and volume kept | Nothing |
destroyed | Gone. Volume deleted unless detached first | Nothing |
error | Provider or image failure. Reason in error | Nothing after the transition |
Transitions are idempotent: calling briq_stop on a stopped briq is a no-op that returns the
current state, not an error. The same goes for starting a running briq, which matters because a
briq can auto-stop between two of an agent's calls. Invalid transitions (start a destroyed briq)
return invalid_state (HTTP 409).
Idle auto-stop
A briq follows the agent's working rhythm, not the wall clock. Every call that touches it —
briq_exec, the file tools, briq_logs, briq_expose — marks it as in use. When nothing has
touched it for idleMinutes (default 60), the reconciler stops it.
A stopped briq is not billed. Its disk is kept, so it comes back with its data where it was. On
sizes up to 2 GB of memory (nano and small) the machine is suspended with a memory snapshot
rather than fully stopped, so the start takes about a second instead of a cold boot; larger sizes
cold-boot, and a snapshot that cannot be resumed falls back to one too.
This is the difference between paying for a database while an agent thinks, and paying for it only while the agent uses it. An agent that runs a five-minute task against a Postgres and then moves on stops paying an hour later, without having to remember to clean up.
Each briq carries both deadlines: idleStopsAt (moves every time you use it) and stopsAt (the
TTL, fixed). MCP results show whichever comes first.
Coming back
You do not have to notice that a briq idled out. The next call that needs it running — an exec, a file read or write, an expose — starts it again and then does what you asked. The agent sees a slightly slower call, not an error.
That resume is deliberate about why the briq stopped. Only an idle stop restarts on access. A
briq you stopped yourself, one that hit its TTL, and one stopped by a spend cap all stay stopped,
and the call returns invalid_state naming the reason and pointing at briq_start — a spend cap
that restarted itself on the next call would not be a cap.
Holding a briq open
briq only sees its own control plane. Traffic to a public URL, or a long job running inside the briq with nothing calling out, looks exactly like idleness. Two things handle that:
briq_exposeturns the idle auto-stop off for that briq, automatically. Once a port has a public URL, people can be using it without briq ever knowing, so the TTL takes over as the only deadline. The expose result says so.- Set it yourself with
idleMinutes: 0at run time, or on a live briq withbriq_extend({ briq, idleMinutes: 0 }). The same call sets it back to a real number later.
You never need briq_extend for ordinary work — using a briq already pushes its idle deadline
back. Extend is for the TTL, and for these cases where the work is invisible to briq.
Where the work happens
Provider calls never run inside an HTTP request. briq_run validates the policy, writes the briq
row as creating, and enqueues a job. The job talks to the provider, waits for readiness, and
moves the row to running. If your client times out, the machine is still tracked and still
subject to the TTL and the reconciler.
TTL and stopsAt
Every briq has a ttlMinutes (default 480, maximum 10080) and a computed stopsAt. When the
clock passes it, the reconciler stops the briq, whether or not it is in use. It is the backstop
under the idle auto-stop: idle time ends a forgotten briq, the TTL ends a busy one.
The default TTL is 480 minutes (8 hours) and the maximum is 10080 (7 days), capped per key by
maxTtlMinutes. It sits well above the idle deadline on purpose: if the two were equal, a briq
would always hit its TTL first and the idle auto-stop could never fire.
A briq stopped by its TTL stays stopped until you start or destroy it. It costs nothing while it
sits there, so there is no rush, but briq_destroy is still the right call when you are
finished.
- Extend with
briq_extend({ briq, minutes }); the new TTL is still capped by the key'smaxTtlMinutes. - Every response about a briq includes
stopsAt, and every MCP tool result ends with it, so a model running a long job can extend before the deadline rather than lose its work.
The reconciler
A worker runs every 30 seconds. It lists machines at the provider per team, compares them to the database, and:
- emits usage events for metering and spend-cap enforcement,
- enforces
stopsAt, - enforces spend caps per key,
- stops briqs that have been idle for
idleMinutes, - destroys orphans (machines briq does not know about).
A briq stopped in the same pass keeps the most specific reason it earned: ttl, then cap, then
idle.
This is why a provider incident, a crashed API process or a lost HTTP response cannot leak a machine for more than a reconciler cycle.