Box lifecycle
Box states (pending, provisioning, starting, running, stopping, stopped, hibernating, hibernated, deleting, failed, unknown), the requests that move a Box between them, auto-hibernation 60 minutes after each start, what stop, hibernate and delete keep, restartRequired after host restarts, failures, deletion, desired vs observed state and generations, and ssh.ready with what each ssh.reason means and what to do.
The SDK's lifecycle methods and the CLI's --wait flag wait for each change for you; see
Waiting. Read on if you poll the HTTP API yourself, or want to know what each
state and field means.
In words: create takes a new Box through pending, provisioning and starting to running.
Stop takes a running Box to stopped, and start brings it back. Hibernate, or automatic
hibernation 60 minutes after a start, takes a running Box to hibernated, and start resumes it.
Stop takes a hibernated Box to stopped and discards its saved memory. Delete works from any
state: the Box shows deleting and is then removed. failed and unknown are not drawn; see
What each state means.
What each state means
| State | Meaning | What you can do |
|---|---|---|
pending | The Box was just created, and its Node has not reported on it yet. | Wait. |
provisioning | The Node is creating the Box's disk from its base image or snapshot. | Wait. |
starting | The VM is booting, or resuming from hibernation. | Wait for ssh.ready. |
running | The guest has booted, its SSH server answers and its accounts are applied. | Exec and SSH once ssh.ready is true, the console once console.ready is true (readiness). Stop, hibernate, delete. |
stopping | The guest is shutting down. | Wait. |
stopped | The VM is not running. The disk is kept. | Start, delete. Snapshot and restore once it is stopped at the current generation. |
hibernating | The Node is saving the Box's memory to its disk. | Wait. |
hibernated | Memory and processes are saved on the disk, and the VM is not running. | Start to resume it, stop to discard the saved memory, or delete. |
deleting | The Node is removing the VM, its disk and its snapshots. Only Box listings, and a create repeated with its Idempotency-Key, show this state. | Wait until the Box leaves the listing. |
failed | A start or another Node action failed, or the guest stopped on its own. observed.error says why. | Start to try again, stop, or delete. |
unknown | The Node has not reported recently: its latest report is over 30 s old or predates a restart of the Node's service or host, or the Node has sent no heartbeat for 20 s. | Treat the Box as neither running nor stopped. Requests are still accepted. |
stateis the Node's latest report, so right after a request it can still show the state from before it. Under the hood explains how to tell when a request has taken effect.- You can send start, stop or delete in any state, and hibernate whenever the desired state is
running, apart from the conflicts listed under How requests move a Box. The Node finishes its current step, then applies the latest request. - The
BoxStatetype also listsabsent, which the API does not return, anddeleted, which appears only when a create repeated with itsIdempotency-Keyreturns a Box that its Node has already removed, together withdeletion: "complete". See Idempotency keys.
How requests move a Box
| Request | Desired state | SDK | CLI |
|---|---|---|---|
| create | running, or stopped with start: false | cubicle.boxes.create() | cubicle boxes create |
| start | running | box.start() | cubicle boxes start |
| stop | stopped | box.stop() | cubicle boxes stop |
| hibernate | hibernated | box.hibernate() | None; use the SDK, the API or the Dashboard |
| delete | deleted | box.delete() | cubicle boxes delete |
| From | Request or event | Passes through | To |
|---|---|---|---|
| New Box | create (start: true, the default) | pending, provisioning, starting | running |
| New Box | create with start: false | pending, sometimes provisioning | stopped |
stopped | start | starting, after provisioning if the disk does not exist yet | running |
running | stop | stopping | stopped |
running | hibernate, or 60 minutes after the start | hibernating | hibernated |
running | hibernate, when saving the memory fails | hibernating | running; retried automatically |
running | start | None | running at a new generation |
running | the guest shuts down, reboots or panics | None | failed |
hibernated | start | starting | running, with memory and processes resumed |
hibernated | stop | stopping | stopped; the saved memory is discarded |
failed | start | starting, after provisioning if the disk does not exist yet | running |
failed | stop | stopping, if the VM is still running | stopped |
| Any | delete | deleting | Removed |
Lifecycle requests need a grant on the Box, or the owner or admin role in its organization. They
return 409 conflict in these cases:
- Hibernate, unless the desired state is
running:only a running box can hibernate. A Box that is already hibernating or hibernated accepts it without change. - Hibernate, while the Box waits for a start after a host restart:
the box requires an explicit Start after a host reboot. - Start, stop, hibernate and delete, while a snapshot or restore of the Box is pending or running:
a snapshot or restore is in progress for this box. A running exec does not block them. - Delete, while another Box is still being created from one of this Box's snapshots.
Read Generations before you start a Box that is already running.
Boxes hibernate 60 minutes after each start
- When a Box first reports
runningafter a start,hibernateAtis set to 60 minutes later. At that time Cubicle hibernates it, as if you had sent hibernate. - SSH sessions, execs, console use and network traffic do not extend the deadline.
- Every start sets a fresh 60 minutes, including a start on a Box that is already running.
hibernateAtisnullwhile the desired state is notrunning, while the Box waits for a start after a host restart, and before the firstrunningreport.- Hibernation interrupts running execs. They end
unknownand are never run again.
Hibernation pauses the Box's vCPUs and saves its memory and device state on its disk. Start resumes the Box from that saved state, with its processes where they were. SSH and private-network connections to the Box end, and other open connections may not survive.
When saving fails, the guest keeps running. The desired state returns to running, the error is in
observed.error, and hibernationRetryAt says when Cubicle tries again: after 30 s, then twice as
long each time, up to 5 minutes. A hibernate you request yourself retries the same way.
When the Node's QEMU version, CPU, kernel or the Box's configuration changed after the save, resume
fails: the Box goes to failed with
Saved VM configuration is incompatible; Stop then Start for a cold boot. Stop it to discard the
saved memory, then start it.
Stop, hibernate and wake a Box has the steps in the SDK, the API and the Dashboard; the CLI has no hibernate command.
What stop, hibernate and delete keep
| Stop | Hibernate | Delete | |
|---|---|---|---|
| What the guest sees | A shutdown request, then a forced stop after 15 s | Paused, with no shutdown | A forced stop, with no shutdown |
| Disk contents | Persists | Persists | Does not persist |
| Memory | Does not persist | Persists | Does not persist |
| Running processes | Does not persist | Persists | Does not persist |
| Private IP address | Persists | Persists | Does not persist |
| SSH host key | Persists | Persists | Does not persist |
| Grants | Persists | Persists | Does not persist |
| Snapshots | Persists | Persists | Does not persist |
| Open SSH and private-network connections | Does not persist | Does not persist | Does not persist |
| Reserved vCPUs, memory and disk | Persists | Persists | Released when the deletion finishes |
| Name | Persists | Persists | Free for reuse at once |
| Next start | Cold boot | Resumes the saved memory | Not possible |
- Stopping a hibernated Box discards its saved memory and keeps the disk. The Dashboard calls this Discard saved memory.
- Snapshot and restore need a stopped Box. Stop a hibernated Box first. See Snapshots.
- A free Omarchy Box is the exception: it gives back its
vCPUs and memory while stopped or hibernated, and is deleted at
expiresAtunless you start it.
A host restart needs an explicit start
When a Node's host restarts, Cubicle does not start its Boxes again by itself. Every Box that should have been running shows:
restartRequired: truestate: "stopped", reported at the generation the Node last applied, whiledesired.stateis stillrunningssh.reasonandconsole.reasonrestart_requiredhibernateAt: null; hibernate returns409 conflict
Start the Box to boot it again, or stop it to leave it stopped. Either clears restartRequired.
Its memory was lost with the host, so the start is a cold boot.
Hibernated Boxes stay hibernated through a host restart. Start resumes them as usual.
Failures are reported, not retried
A Box whose start or other Node action fails shows state: "failed", with the reason in
observed.error. box failed: <error> lists each error with
its cause and fix.
- A Box does not restart in place. A reboot or shutdown from inside the Box leaves it
failed; start it again. - Cubicle does not retry a failed start. Start the Box to try again, or stop or delete it.
- A failed hibernation is not
failed: the Box keeps running. See Boxes hibernate 60 minutes after each start. - A
failedreport keepsobserved.generationat the last generation the Node applied, so right after a new start it can be left over from the earlier one. Following a request says how to tell whether it belongs to your request.
Deleting a Box
DELETE /v1/boxes/{box}, box.delete() and cubicle boxes delete acme/job-1 delete a Box. The
request returns 202 with desired.state: "deleted" and deletion: "pending". Deletion is
permanent.
When the deletion is accepted:
- running execs end
unknown, and operations that have not started fail withbox deleted - the Node stops the VM without a guest shutdown, then removes its disk and every snapshot of it
GET /v1/boxes/{box}and every other route for the Box return404- the name is free for a new Box
Box listings (GET /v1/orgs/{org}/boxes, GET /v1/boxes and cubicle.boxes.list()) keep showing
the Box with deletion: "pending", usually in state deleting, until its Node confirms the
removal. The Box then disappears from the listings, and its vCPUs, memory, disk and private IP
address are released. Deletion shows how to wait for that, and
Waiting and cubicle boxes delete say what the
SDK and the CLI wait for.
Under the hood: desired and observed state
You change a Box by requesting a new desired state. Every lifecycle request returns 202 with the
Box as it is at that moment, or 200 for a create repeated with the same Idempotency-Key. The
Box's Node carries out the change afterwards and reports back. A Box record carries both sides:
desiredis what was last requested:{ state, generation }.observedis the Node's latest report:{ state, generation, observedAt, ageSeconds, fresh, error }. It isnulluntil the Node first reports on the Box.stateis derived fromobserved. It isunknownwhen the report is not fresh.
This Box has accepted a stop, but its Node last reported it running:
{
"state": "running",
"desired": { "state": "stopped", "generation": 5 },
"observed": {
"state": "running",
"generation": 4,
"observedAt": "2026-11-05T10:00:02.000Z",
"ageSeconds": 1,
"fresh": true,
"error": null
},
"ssh": { "ready": false, "reason": "not_requested" }
}Once the Node has stopped the Box, it reports stopped at generation 5.
Following a request gives the full polling loop.
Generations show when a request has taken effect
Each Box has two counters: desired.generation and connectionGeneration.
desired.generation advances with each of these:
- every accepted start and stop, even when the Box is already in that state
- hibernate, unless the Box is already hibernating or hibernated
- automatic hibernation
- a failed hibernation, which returns the desired state to
running - delete
observed.generation is the generation the Node last applied. A request has taken effect when the
Box reports its target state and observed.generation equals desired.generation. For a start,
use ssh.ready, which is true only at the current generation.
connectionGeneration advances whenever desired.generation does, except after a failed
hibernation. It also advances with every restore and when the Node's host
restarts. Guest accounts, private-network access and the published SSH host key from an earlier
value no longer apply, so SSH and private-network traffic reaches the Box only once it runs at the
new value. The host key itself does not change. Reconnect once ssh.ready is true again.
A start on a Box that is already running does not reboot it, but it still does all of this:
- advances both counters
- makes
ssh.readyfalse, with the reasongeneration_pending, until the Node has applied the new generation. The Node does that once execs already running on the Box have finished. - keeps SSH and private-network traffic from reaching the Box until then; open connections may not survive the gap
- applies the Box's accounts again
- restarts the 60-minute hibernation clock
So check desired.state before you send a start. The SDK's box.exec(argv, { wake: true })
starts the Box only when it is not already meant to be running, or when it needs a start after a
host restart.
An exec is also bound to the generation at which it was accepted. See Commands and operations.
When a Box is ready
ssh.ready is true when the Box is running at its current generation on a healthy Node and its
SSH host key is published. Exec, new SSH connections and new private-network connections work
then. When it is false, ssh.reason names the first check that failed, in the order of the
first ten rows of the table below.
ssh.ready describes the Box, not you: it does not check your grant or your SSH keys.
GET /v1/boxes/{box}/ssh (reference) reports
whether you can connect. It runs the Box's checks first and then yours, so it can also return the
last five reasons in the table. See SSH into a Box.
Where you see the reason:
cubicle boxes listshows the Box's reason asssh=<reason>.cubicle ssh-configalso checks your own access, and prints(not ready: <reason>).- The Dashboard words each reason as quoted in the table: the Box's under SSH readiness on the State tab of the Box's details, and yours on its Access tab.
| Reason | Meaning | What to do |
|---|---|---|
deleted | The Box is deleted. Dashboard: "Box is deleted". | None. Create a new Box. |
not_requested | The desired state is not running. Dashboard: "Start or Resume is required for SSH". | Start it: cubicle boxes start acme/job-1 --wait. |
restart_required | The Node's host restarted. Dashboard: "Host rebooted; start it again to clear the hold". | Start it. See A host restart needs an explicit start. |
node_unhealthy | The Node has sent no heartbeat for 20 s. Dashboard: "Owning Node missed its heartbeat". | Wait for the Node to report again. |
inventory_stale | The Node has not reported on its Boxes as a whole for over 30 s. Dashboard: "Node inventory is stale". | Wait. |
observation_stale | The Node's latest report on this Box is over 30 s old, or predates a restart of the Node's service or host. Dashboard: "Observation is from an old Node session or boot". | Wait. |
not_running | The Node does not report the Box running yet. Dashboard: "Not observed running yet". | Wait while it boots. If it is failed, start it again. |
generation_pending | The Box is running, but the Node has not applied the latest request yet. Dashboard: "Node has not applied the latest generation". | Wait for the Node to apply your latest request. |
endpoint_missing | The Box has no private IP address yet. Dashboard: "No private endpoint allocated yet". | Wait. |
host_key_pending | The Box's SSH host key is not published for its current connection generation yet. Dashboard: "Host key not published for this connection generation". | Wait. |
user_disabled | Your Cubicle account is disabled. Dashboard: "Your account is disabled". | Ask a Cubicle operator. |
membership_inactive | You are not an active member of the Box's organization. Dashboard: "You are not an active member of its organization". | Ask an owner or admin to add you. |
grant_inactive | You have no active grant on the Box. Dashboard: "You have no active grant on this Box". | Ask an owner or admin for a grant. |
no_active_key | Your account has no active SSH key. Dashboard: "You have no active SSH key (add one under Account)". | Add one: cubicle keys add. |
authorization_pending | The Node has not yet applied your guest account and SSH keys for the Box's current connection generation. Dashboard: "Guest accounts and keys are still being applied". | Wait a few seconds. |
console.ready reports whether the desktop console can be used. It does not
depend on SSH: it needs the VM's display, reported by the Node for the Box's current connection
generation within the last 15 s.
Check console readiness lists each console.reason
and what to do.
Related
Boxes
What a Box is, its images, sizes, names and references.
Commands and operations
Exec, operation states and idempotency keys.
Stop, hibernate and wake a Box
The steps, in the SDK, the API and the Dashboard.
Snapshots
Disk snapshots of stopped Boxes, restore and clones.
Waiting
How the SDK waits for states and operations.
Requests and waiting
Following a request over the HTTP API.
Organizations and access
How Cubicle access control works, covering organizations and personal organizations, Box creation approval, the owner, admin and member roles, Box grants, guest accounts in a Box, revoking access and what service accounts can do.
Commands and operations
How exec runs a command in a Box (argv without a shell, no stdin or TTY, your own guest account, sudo and API keys), its results and limits, the grant and running Box it needs, generations, operation states, who can read an operation, idempotency keys and running several commands at once.

