Skip to main content
This guide explains how a sandbox progresses from creation to shutdown, including the states it passes through, how to start and wait for it, and how to stop it cleanly. Use this guide when you need to choose between creation patterns, control startup timing, or handle shutdown reliably in your SDK code.

Lifecycle states

Every sandbox passes through a series of states as it starts, runs, and shuts down. The SDK represents these as SandboxStatus values. PENDING and CREATING are transient. The SDK polls through them automatically. TERMINATING is a transient backend state that wait_until_complete() and stop().result() drive through to a terminal state. RUNNING is the stable operational state. COMPLETED and FAILED are terminal. UNSPECIFIED is mapped to COMPLETED by the SDK at poll time.

Creation patterns

You can create a sandbox in two ways, differing in when the start RPC fires. The following sections describe each pattern and when to choose it.

Sandbox.run(): immediate start

Sandbox.run() creates a sandbox and calls start().result() internally, blocking until the backend accepts the request:
The first positional argument is the command, the rest are arguments:

session.sandbox(): deferred start

session.sandbox() creates a sandbox object without making any network call. The start RPC fires on first use:

Main command lifetime

The command passed to run() or session.sandbox() is the sandbox’s main process. When it exits, the sandbox transitions to COMPLETED:
If you need to run a short command and capture its output, use exec() on a long-running sandbox rather than making the short command the main process.

Start a sandbox

After a sandbox object exists, you can start it explicitly or rely on the SDK to start it on first use. The following sections describe both approaches and how context managers interact with them.

Explicit start

start() sends the start RPC and returns an OperationRef[None]:
This is useful when you want to control timing or handle start errors separately from operation errors.

Auto-start

Most operations auto-start the sandbox if it hasn’t been started yet. In the common case, you create a sandbox and start using it.

Context managers and start

Context managers (with/async with) call start() on entry but do not call wait(). If you need the sandbox to be RUNNING before your first operation, call wait() explicitly:
In practice, this rarely matters because exec() waits for RUNNING internally. Explicit wait() is useful when you want to separate startup failures from operation failures.

Wait for a sandbox

The SDK provides two waiting methods depending on whether you need the sandbox to be ready for operations or to finish its main command. The following sections describe each method and how startup and operation timeouts relate.

wait(): block until RUNNING

wait() polls until the sandbox reaches RUNNING, TERMINATING, or a terminal state:
The polling uses exponential backoff: starting at 0.2s intervals, scaling by 1.5x, capping at 2.0s. wait() returns self for method chaining:
If the sandbox reaches a terminal state during startup, wait() handles it:

wait_until_complete(): block until terminal

wait_until_complete() blocks until the sandbox reaches a terminal state, polling through TERMINATING automatically. Use this for sandboxes where the main command does the work:
The raise_on_termination parameter controls whether wait_until_complete() raises SandboxTerminatedError after this client called stop(). With the default raise_on_termination=True, the SDK raises. External stops and lifetime-exceeded events surface as COMPLETED without a distinct error, because the backend doesn’t yet provide termination reason metadata.

Timeout phases

Startup wait time and operation timeouts are separate phases:
  • Startup wait: Time spent in PENDING/CREATING before reaching RUNNING. Controlled by the timeout parameter on wait() or wait_until_complete(). Typically 30 to 60 seconds depending on backend scheduling.
  • Operation timeout: Time for an individual exec, read, or write. Controlled by timeout_seconds on exec(), or request_timeout_seconds in SandboxDefaults. Doesn’t include startup wait.

Operations and lifecycle

Operations like exec(), read_file(), and write_file() auto-start the sandbox if needed, then wait for RUNNING before proceeding:
The operation timeout (timeout_seconds) applies only after the sandbox is RUNNING. Startup time is not counted against it.

Stop a sandbox and end of life

This section covers how to shut down a sandbox, what happens after a stop, and when to use stop() versus delete().

stop()

stop() sends a stop request and returns OperationRef[None]. The sandbox transitions through TERMINATING (grace period draining) before reaching a terminal state. The returned OperationRef resolves when the backend confirms the terminal state, not only when the stop RPC succeeds:
Parameters:
stop() handles in-flight starts: if a start is still being processed, it waits for start to complete before stopping. Concurrent or repeated calls to stop() share one stop operation and don’t issue duplicate stop RPCs. This makes repeated stop() calls safe and cheap.

Post-stop behavior

After stop() is called, the sandbox transitions through TERMINATING (the grace period draining state) and then reaches a terminal state (COMPLETED or FAILED). After stop() has been called, the sandbox is unusable. Further operations raise SandboxNotRunningError:
The status property is cached from the last API call. For fresh data before stopping, use get_status():

Context manager exit

Context managers call stop() automatically on exit:
If an exception is in-flight, the context manager suppresses stop errors to avoid masking the original exception.

stop() compared to delete()

Use stop() for sandboxes you’re actively using. Use delete() for cleanup of sandboxes discovered through Sandbox.list() or Sandbox.from_id():
See the Cleanup patterns guide for orphan management and batch cleanup strategies.

Under the hood

The SDK runs all gRPC operations on a background daemon thread with its own asyncio event loop. This design means:
  • The sync API (.result()) blocks the calling thread while the background loop handles the network call.
  • The async API (await) bridges to the same background loop, so both patterns use the same underlying implementation.
  • The background loop starts lazily on first use. gRPC channels are also created lazily.
  • Auto-start works by checking if sandbox_id is None before each operation and triggering start() if so.
  • On process exit, cleanup handlers (atexit + signal handlers) stop all sandboxes in registered sessions. A second Ctrl+C during cleanup forces immediate exit.
This architecture avoids cross-event-loop issues and works in Jupyter notebooks without nest_asyncio. See the Sync compared to async patterns guide for usage patterns.

Common patterns

The following examples show end-to-end patterns that combine the lifecycle steps described above.

Quick one-off

Run a command and get the result. Context manager handles cleanup:

Controlled startup

Separate start errors from operation errors:

Long-running sandbox

Wait for the main command to complete:

Reconnection

Reattach to a sandbox from a previous session or process:

Parallel batch with session

Create multiple sandboxes and wait for results:
Last modified on May 29, 2026