Skip to main content
Which state a box is in decides whether your next exec starts it, restarts it and reuses its disk, or fails. Two switches then decide how long it lives: auto_remove and detach.

The states

Two transitions are worth committing to memory:
  • Creation is lazy. Constructing a box returns a handle; the microVM comes up on first use. A box that never runs costs nothing but a database row.
  • Stop is not remove. stop() frees the VM but keeps the disk, so restarting reuses everything the box had installed. That is what makes a box a reusable workspace rather than a one-shot container.
Read the current state with box.info().state.status. info() is synchronous — do not await it, and it triggers no VM operation. Field names are exact and easy to get wrong: see BoxInfo / BoxStateInfo.

Walking the states in code

The example below runs standalone: create → exec → stop → remove. auto_remove=False is used to demonstrate the separate stop and remove steps.
Key point: Boxlite is a synchronous context manager (with), whereas a Box’s methods (exec/stop/metrics) are async (require await). box.info() is synchronous and must not be awaited.

detach and auto_remove: two independent switches

BoxOptions has two boolean switches that control the lifecycle and do not affect each other: Common combinations:
auto_remove=True together with detach=True is invalid (a detached Box needs manual lifecycle control); setting both fails validation with an error.

Restarting a stopped Box (rootfs reuse)

For an auto_remove=False Box, the disk persists after stop(). Re-acquire a handle and exec again, and the VM restarts reusing the original rootfs:
Note: data written to tmpfs paths such as /tmp does not persist across restarts; only data written to ordinary paths on the rootfs (such as /root) persists.

reattach: getting a second handle in the same process

runtime.get(id_or_name) returns a new handle to an existing Box; the Box may be running or stopped. When get finds nothing it returns None (it does not raise).

Cross-process sharing

One process creates and initializes a Box with detach=True and exits; the Box keeps running, and another process takes it over with runtime.get(box_id).
Important: do not use Boxlite.default() for cross-process scenarios. default() is a process-wide static singleton that keeps holding the runtime lock and does not release it even after a child process exits. Use boxlite.Boxlite(boxlite.Options()) instead to construct a releasable runtime instance.
The following is a self-contained script: the parent forks a child that creates a detached Box, and after the child exits the parent takes it over.
Box state persists in the runtime database, so it can be queried (get_info), taken over (get), and restarted (by exec-ing a stopped Box again) across process boundaries.

Bulk shutdown: runtime.shutdown()

runtime.shutdown(timeout=None) gracefully shuts down every Box under that runtime. timeout is in seconds: None = default 10 seconds, -1 = wait indefinitely. After shutdown, any creation-type operation raises RuntimeError.

Parameters and Returns

Note: name is not a BoxOptions field; it is a separate parameter of create(options, name=...) / get_or_create(options, name=...). Once named, you can operate by name with get(name) / remove(name).
* Provide one of image or rootfs_path; constructing with neither raises an error. ** auto_remove=True and detach=True are mutually exclusive (a detached Box needs manual lifecycle management); setting both fails at validation.

Runtime methods (called on a Boxlite instance)

Box methods (async, require await, except info())

BoxInfo / BoxStateInfo (state access)

box.info() returns a BoxInfo; its state field is a BoxStateInfo, and the state string is at state.status:

Troubleshooting

Awaiting Box.info() as if it were async

info() is a synchronous method.

Calling remove() on a Box / using list()

Removal and listing both happen on the runtime, with the method names remove/list_info:

Removing a running Box errors

remove(id, force=False) requires the Box to be stopped, otherwise it errors. Either stop() first, or use force=True (stop then remove):

Using Boxlite.default() across processes leaves the lock unreleased

default() is a process-wide static singleton that holds the runtime lock until the process ends. Cross-process scenarios (a child creates, the parent takes over) must use boxlite.Boxlite(boxlite.Options()) to construct a releasable instance, otherwise the taking-over process blocks or fails because it cannot acquire the lock.
Current limitation: if the process holding the default() lock exits abnormally (crash, kill, or exiting without close()/with), a lock file may remain under $BOXLITE_HOME/locks/, causing the next Boxlite.default() to report Another BoxliteRuntime is already using directory. After confirming no boxlite process is alive (lsof $BOXLITE_HOME/locks/* shows no holder), you can clean it up manually with rm -f $BOXLITE_HOME/locks/* ($BOXLITE_HOME defaults to ~/.boxlite). A runtime that exits normally via with/close() cleans up after itself.

execution.stdout() yields str, not bytes

The underlying Execution.stdout() / Execution.stderr() async iterators yield already-decoded str chunks, not bytes. Use them directly; calling chunk.decode(...) as if they were bytes raises AttributeError: 'str' object has no attribute 'decode'.

exec exits non-zero without raising

exec reports a non-zero exit through exit_code / exitCode, not by raising. See Error Handling.

No virtualization causes startup failure (environment constraint)

BoxLite requires hardware virtualization:
  • Linux: requires KVM (/dev/kvm available; under WSL2 the user must be in the kvm group);
  • macOS: uses Apple Hypervisor.framework, no /dev/kvm required (macOS arm64 supported); macOS Intel is not supported.
Without virtualization, Box startup fails (the process stays alive and can be caught with try/except to inform the user).

Advanced: lifecycle when using SimpleBox

When you use the higher-level SimpleBox (an async context manager), it lazily creates and starts the Box on async with entry and cleans up on exit according to auto_remove. The lifecycle switches are passed the same way via constructor arguments:
SimpleBox.exec’s timeout parameter is timeout (float) and its env is a dict; whereas the underlying Box.exec’s timeout parameter is timeout_secs and its env is a list[tuple]. For detach / cross-process / explicit restart, continue to use Boxlite + Box directly, because those operations require access to the Box id and the runtime.