Skip to main content
Three classes of incident account for almost all of it: paying VM startup cost you did not need to, letting one box exhaust the host, and leaking boxes that outlive the process that made them. None of the fixes require weakening isolation.

The checklist

Work down this list before putting an agent that drives sandboxes into production. Each row links to the section that explains it, so this doubles as the page outline.

A production skeleton

The most concise and stable production skeleton: a SimpleBox with resources and strong isolation configured, cleaned up automatically by async with.
The example above uses the SimpleBox “lazy box creation” path. If you need finer-grained options across multiple sandboxes (strong isolation, volumes, secrets, etc.), use the runtime Boxlite + BoxOptions directly (see the concurrency and security sections below).

Concurrency model

Two modes; choose based on your isolation needs. Choosing between the two modes: A single sandbox can carry many exec() calls; each exec spawns a new process inside the same VM. The VM startup cost is paid once, and the VM boundary itself already provides hardware isolation from the host. This fits the vast majority of agent scenarios.
Note: when called through the wrapper, runtime.create(...) returns a native Box (an async context manager). Here we obtain the box via await runtime.create(...) and then stop/remove it manually. The native signature of box.exec(...) takes args: list as its second argument, and its timeout parameter is timeout_secs (unlike the high-level SimpleBox.exec’s timeout).

Mode B: one sandbox per agent / task

When you need strong cross-tenant isolation, different images, or independent resource ceilings, give each task its own sandbox.

Timeouts and zombie-process protection

asyncio.wait_for() only cancels the Python coroutine; it does not kill the process inside the sandbox --- the process keeps running inside the VM. The two correct approaches follow. The high-level SimpleBox.exec has a built-in timeout (float, seconds). On timeout, BoxLite terminates the process on the sandbox side via signal, leaving no stray process.
Key point: a SimpleBox.exec timeout does not raise TimeoutError --- it returns an ExecResult whose exit_code is negative (the process is terminated by SIGTERM, so exit_code == -15). Always check result.exit_code; do not use except TimeoutError.

Approach 2: manual wait_for + kill() (native Execution)

When you use the native box.exec(...) directly to obtain an Execution, you must call kill() explicitly in the timeout branch.

Resource ceilings

Set hard ceilings on a sandbox via the BoxOptions / SimpleBox constructor parameters to prevent a runaway agent from exhausting host resources.
Resource defaults: when cpus/memory_mib are not passed, the underlying engine allocates a default. To inspect the actual resources inside a box, use nproc / /proc/meminfo. For predictability and cost control, set them explicitly in production. See Compute Resources.
Choose a configuration by workload (the following are suggested starting points; adjust as needed):

Observe runtime resource usage

Use the runtime-level metrics() for global metrics, noting the actual field names. Although Boxlite is a synchronous context manager, metrics() is an async method (requires await), so call it inside an async function.

Security: strong isolation

Security options are passed via advanced; there is no top-level security= keyword. AdvancedBoxOptions is not exported at the top level and must be imported from boxlite.boxlite.

Security presets

SecurityOptions has three preset static methods (there is no .minimum(); the weak preset is called development()): You can also construct a custom one (keyword arguments):
In the Python bindings, network_enabled is currently a macOS-side control. On Linux, network isolation is typically achieved via NetworkSpec (BoxOptions(network=...)) together with publishing no ports. To disconnect the network entirely, use it together with BoxOptions(network=NetworkSpec(...)); see Network Access.

Read-only volumes: the third element is a bool, not a string

When mounting data into a sandbox, use a read-only volume to prevent it from being overwritten. The third element of a volume is the bool read_only (True = read-only / False = read-write), not the string "ro"/"rw"; a 2-tuple also works (default read-write).
Passing a string raises directly: TypeError: 'str' object cannot be cast as 'bool' (see Troubleshooting). The CLI’s -v host:box:ro syntax uses ro/rw strings and belongs to the CLI parsing layer --- it is different from the SDK’s bool, so do not confuse them.

Inject secrets (Secret / Vault entry point)

Do not write tokens in cleartext into commands or the environment; use BoxOptions(secrets=[Secret(...)]).
For complete secret usage, see Secrets and Security.

Cleanup: guarantee sandboxes are reclaimed

Boxes outlive the process that created them, so a hard kill leaves them running and holding disk and host ports. Recover from the command line:
List with runtime.list_info() (not list()); it is an async method (requires await). Note the distinction: box.info() is a synchronous getter (do not await it), whereas runtime.list_info() / runtime.metrics() / runtime.remove() / runtime.create() / runtime.shutdown() are all async (require await). BoxInfo.state is a BoxStateInfo, and the sandbox status string is in info.state.status.

File transfer (production patterns)

Recommendation: use copy_in/copy_out for dynamic, per-request files; read-only volumes for shared datasets; and inline base64 only for tiny payloads.

Troubleshooting

Passing a string for a volume -> TypeError: 'str' object cannot be cast as 'bool'

The third element is the bool read_only: use True for read-only, False or omit it (2-tuple) for read-write. The CLI’s :ro/:rw belongs to the CLI layer syntax and is unrelated to the SDK API.

AttributeError: module 'boxlite' has no attribute 'AdvancedBoxOptions'

AdvancedBoxOptions lives in the boxlite.boxlite submodule, not the top level — import it from there:
And security options are passed via advanced; there is no top-level security=: BoxOptions(advanced=AdvancedBoxOptions(security=SecurityOptions.maximum())).

See Also