Back to Microsandbox

Lifecycle

docs/sandboxes/lifecycle.mdx

0.6.1714.8 KB
Original Source

A sandbox has a simple lifecycle: create it, use it, stop it when it is idle, start it again when you need it, and remove it when you are finished. Stopping preserves the sandbox's configuration and filesystem, while removing deletes its sandbox-owned state.

Create a sandbox

<Note> Locally, an attached SDK handle stops its sandbox when the client process exits. Cloud sandboxes are service-owned, so stop or remove them explicitly. </Note>

Creating a sandbox starts the microVM and waits until it is ready to accept commands. Give each sandbox a name so you can find and manage it later. Names must be non-empty and no longer than 128 UTF-8 bytes.

<CodeGroup> ```typescript TypeScript // Local attached handle: sandbox stops when your process exits await using sb = await Sandbox.builder("worker").image("python").create();

// Detached: sandbox survives after your process exits const detached = await Sandbox.builder("worker") .image("python") .detached(true) .create();


```rust Rust
let sb = Sandbox::builder("worker")
    .image("python")
    .create()
    .await?;
python
sb = await Sandbox.create("worker", image="python")
go
sb, err := m.CreateSandbox(ctx, "worker", m.WithImage("python"))
ruby
sb = Microsandbox::Sandbox.create("worker", image: "python")
bash
msb create python --name worker
</CodeGroup>

An attached local SDK handle normally stops its sandbox when the client process exits. See Keep a sandbox running when the sandbox should outlive that process.

Stop and start again

Stopping gracefully terminates guest processes and shuts down the VM. The sandbox moves to Stopped, but its configuration and filesystem remain available for the next start.

<CodeGroup> ```typescript TypeScript await sb.stop();

const sb = await Sandbox.start("worker");


```rust Rust
sb.stop().await?;

let sb = Sandbox::start("worker").await?;
python
await sb.stop()

sb = await Sandbox.start("worker")
go
_ = sb.Stop(ctx)

sb, err := m.StartSandbox(ctx, "worker")
ruby
sb.stop

sb = Microsandbox::Sandbox.start("worker")
bash
msb stop worker
msb start worker
</CodeGroup>

Use restart when you want to stop and start the sandbox in one operation. If it is already stopped or crashed, restart starts it directly. SDK receiver methods preserve the sandbox's identity and configuration while replacing only the running VM instance.

bash
msb restart worker

On microsandbox cloud, restart and destroy can request a graceful stop, but timeout expiry cannot escalate to a force kill. Force controls are local-only, and detached-start controls affect only local process ownership.

Reuse or create a named sandbox

Use connect_or_create when your application wants a sandbox with a stable name but does not know whether it already exists. This is useful for workers, development environments, and services that reconnect after the client process restarts.

The operation:

  • Connects if the sandbox is running
  • Starts it if it is stopped or crashed
  • Creates it if the name does not exist
<CodeGroup> ```typescript TypeScript const sb = await Sandbox.builder("worker") .image("python") .memory(MiB(1024)) .connectOrCreate(); ```
rust
let sb = Sandbox::builder("worker")
    .image("python")
    .memory(1024)
    .connect_or_create()
    .await?;
python
sb = await Sandbox.connect_or_create(
    "worker",
    image="python",
    memory=1024,
)
go
sb, err := m.ConnectOrCreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithMemory(1024),
)
ruby
sb = Microsandbox::Sandbox.connect_or_create(
  "worker",
  image: "python",
  memory: 1024
)
</CodeGroup> <Warning> Creation options are used only when a new sandbox is created. If `worker` already exists, its saved image, resources, environment, mounts, and other configuration are kept. To apply new configuration, use the [local replacement workflow](/sandboxes/overview#naming-conflicts). On microsandbox cloud, where replacement is unavailable, remove the existing sandbox and then create it again. </Warning>

If you already have a SandboxHandle, use connect_or_start to connect to that exact sandbox or start it when needed.

<CodeGroup> ```typescript TypeScript const handle = await Sandbox.get("worker"); const sb = await handle.connectOrStart(); ```
rust
let handle = Sandbox::get("worker").await?;
let sb = handle.connect_or_start().await?;
python
handle = await Sandbox.get("worker")
sb = await handle.connect_or_start()
go
handle, err := m.GetSandbox(ctx, "worker")
sb, err := handle.ConnectOrStart(ctx)
ruby
handle = Microsandbox::Sandbox.get("worker")
sb = handle.connect_or_start
</CodeGroup>

Keep a sandbox running

Detach a local sandbox when it should keep running after the client process exits. You can reconnect to it later by name.

<CodeGroup> ```typescript TypeScript const sb = await Sandbox.builder("worker") .image("python") .detached(true) .create(); ```
rust
let sb = Sandbox::builder("worker")
    .image("python")
    .detached(true)
    .create()
    .await?;
python
sb = await Sandbox.create("worker", image="python", detached=True)
await sb.detach()
go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithDetached(),
)
ruby
sb = Microsandbox::Sandbox.create("worker", image: "python", detached: true)
sb.detach
bash
msb run -d python --name worker
</CodeGroup>

You can also detach an existing SDK receiver with detach() (Detach in Go).

List and inspect

List sandboxes to discover what exists, or get one by name when you already know which sandbox you need.

<CodeGroup> ```typescript TypeScript const page = await Sandbox.list(); for (const handle of page.sandboxes) { console.log(`${handle.name}: ${handle.status}`); }

const handle = await Sandbox.get("worker"); console.log(handle.status);


```rust Rust
for handle in Sandbox::list().await?.sandboxes {
    println!("{}: {:?}", handle.name(), handle.status_snapshot());
}
python
for handle in (await Sandbox.list()).sandboxes:
    print(f"{handle.name}: {handle.status}")

handle = await Sandbox.get("worker")
print(handle.status)
go
page, err := m.ListSandboxes(ctx)
for _, handle := range page.Sandboxes {
    fmt.Printf("%s: %s\n", handle.Name(), handle.Status())
}

handle, err := m.GetSandbox(ctx, "worker")
fmt.Println(handle.Status())
bash
msb ls
msb ps worker
</CodeGroup>

Wait for a state

Use wait_until_stopped when another part of your application is responsible for stopping the sandbox and you only need to wait for it to finish.

<CodeGroup> ```typescript TypeScript const result = await sb.waitUntilStopped(); ```
rust
let result = sb.wait_until_stopped().await?;
python
result = await sb.wait_until_stopped()
go
result, err := sb.WaitUntilStopped(ctx)
ruby
result = sb.wait_until_stopped
</CodeGroup>

Use wait_for_status (waitForStatus in TypeScript and WaitForStatus in Go) when you need to wait for a specific lifecycle state. It has no built-in timeout, so use the language's normal timeout or cancellation primitive around it.

Change configuration

Use msb modify, or the SDK modify() methods, to change an existing sandbox without recreating it. Some changes apply immediately, some affect future commands, and some take effect after a restart.

<CodeGroup> ```typescript TypeScript const plan = await sandbox.modify({ cpus: 4, memory: 4096 }); ```
rust
let plan = sb.modify()
    .cpus(4)
    .memory(4096)
    .apply()
    .await?;
python
plan = await sb.modify(cpus=4, memory=4096)
go
plan, err := sb.Modify(ctx, m.ModifyOptions{CPUs: 4, MemoryMiB: 4096})
bash
msb modify api --cpus 4 --memory 4G
</CodeGroup>

See Live Modify for the change model, CPU and memory resize, labels, environment variables, secrets, and storage sizing.

Check health and keep a sandbox active

<Note> Ping and touch are currently available only for local sandboxes. </Note>

Use ping to check that a running sandbox's guest agent is reachable. A ping is only a health check and does not reset the idle timer. Use touch when you intentionally want to keep the sandbox active.

<CodeGroup> ```typescript TypeScript const ping = await sb.ping(); console.log(`agent reachable in ${ping.latencyMs.toFixed(1)} ms`);

await sb.touch();


```rust Rust
let ping = sb.ping().await?;
println!("agent reachable in {:?}", ping.latency);

sb.touch().await?;
bash
msb ping worker
msb touch worker

# Check health, then keep the sandbox active if it is reachable
msb ping worker --touch
</CodeGroup>

Drain before stopping

<Note> Draining is currently available only for local sandboxes. Use a graceful stop on microsandbox cloud. </Note>

Use a drain when existing commands should finish but new commands should be rejected. The sandbox moves to Draining, waits for in-flight commands, and then stops. This is useful when rotating worker sandboxes without interrupting active jobs.

<CodeGroup> ```typescript TypeScript await sb.requestDrain(); ```
rust
sb.request_drain().await?;
python
await sb.request_drain()
go
err := sb.RequestDrain(ctx)
</CodeGroup>

Stop an unresponsive sandbox

<Note> Force kill is currently available only for local sandboxes. Use a graceful stop on microsandbox cloud. </Note>

If a sandbox does not respond to a graceful stop, force-kill it. This ends the VM immediately without waiting for guest processes to shut down.

<CodeGroup> ```typescript TypeScript await sb.kill(); ```
rust
sb.kill().await?;
python
await sb.kill()
go
err := sb.Kill(ctx)
bash
msb stop --force worker
</CodeGroup>

Destroy or remove

Use destroy when you have an SDK receiver and want to stop and remove that exact sandbox in one operation. It requests a graceful stop by default, escalates after the configured timeout, and refuses to act on a replacement that reused the name.

<CodeGroup> ```typescript TypeScript await sb.destroy(); ```
rust
sb.destroy().await?;
python
await sb.destroy()
go
err := sb.Destroy(ctx)
ruby
sb.destroy
</CodeGroup>

Use remove when the sandbox is already stopped and you want to delete it by name.

<CodeGroup> ```typescript TypeScript await Sandbox.remove("worker"); ```
rust
Sandbox::remove("worker").await?;
python
await Sandbox.remove("worker")
go
err := m.RemoveSandbox(ctx, "worker")
ruby
Microsandbox::Sandbox.remove("worker")
bash
msb rm worker
</CodeGroup>

For a local sandbox, removal deletes sandbox-owned state while leaving independently managed resources intact:

RemovedKept
Sandbox record, configuration, status, labels, and run historyCached OCI images and layers
Managed OCI writable root disk (upper.ext4) and its guest filesystem changesNamed volumes and their contents
Captured sandbox logsSnapshots created from the sandbox
Runtime staging files, including generated scriptsBind-mounted host files or directories
Root filesystem pin metadata for this sandboxUser-supplied root disk images

Removing a sandbox does not undo writes made to a named volume, bind mount, or user-supplied disk image. On cloud, removal deletes the remote sandbox resource; the local disk details above do not apply.

Automatic lifecycle policies

For production workloads, configure a maximum lifetime or idle timeout so sandboxes shut down automatically.

<CodeGroup> ```typescript TypeScript await using sb = await Sandbox.builder("worker") .image("python") .maxDuration(3600) // maximum sandbox lifetime in seconds .idleTimeout(300) // auto-drain after 5 minutes of inactivity .create(); ```
rust
let sb = Sandbox::builder("worker")
    .image("python")
    .max_duration(3600)
    .idle_timeout(300)
    .create()
    .await?;
python
sb = await Sandbox.create(
    "worker",
    image="python",
    max_duration=3600,
    idle_timeout=300,
)
go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithMaxDuration(time.Hour),
    m.WithIdleTimeout(5*time.Minute),
)
</CodeGroup>

Lifecycle states

Most applications only need to distinguish between Running and Stopped. The complete set is useful for status displays and recovery logic.

StatusMeaning
CreatedConfiguration has been saved, but the sandbox has not started yet.
StartingThe VM and guest agent are starting. Commands are not ready yet.
RunningThe sandbox is ready for exec, shell, and filesystem operations.
DrainingExisting commands may finish, but new commands are rejected. The sandbox stops when the drain completes.
StoppedThe VM is off. Configuration and sandbox state are preserved for a later start.
CrashedThe VM exited unexpectedly and can be started again.

Some backends can also report Paused. Resume is not currently exposed through the SDKs, so start and connect operations do not treat a paused sandbox as stopped.

Names, handles, and concurrent callers

A sandbox name finds the current sandbox saved under that name. A Sandbox or SandboxHandle also carries an opaque stable ID (ID() in Go) for one exact saved sandbox. Treat the ID as an opaque value and use it for logging, correlation, and equality checks.

If worker is removed and a different sandbox is later created with the same name, an old receiver will not act on the replacement. Lifecycle methods return SandboxReplaced or the language's typed equivalent when they can detect this situation; an ID-addressed cloud request may instead report that the old resource no longer exists.

Concurrent callers are safe to use with the named lifecycle APIs. create remains strict, so only one same-name creation succeeds. connect_or_create may reuse the sandbox created by another caller, and connect_or_start remains attached to the exact identity held by its handle. Calls that encounter Starting wait for the sandbox to become ready instead of launching a second runtime.

Logs and diagnostics

Use msb logs or the SDK logs() method to read captured output from running, stopped, or crashed sandboxes. For source semantics, boot errors, and diagnostic flows, see Logs.

Reference

For exact lifecycle APIs, see TypeScript, Rust, Python, or Go. For lifecycle commands and the REST surface, see Sandbox commands and the Cloud API.