# Sandboxes

This page is a high-level guide to Sandboxes,
secure containers for executing untrusted user or agent code on Modal.

For reference documentation on the `modal.Sandbox` interface,
see [this page](/docs/reference/modal.Sandbox).

## What are Sandboxes and why should I use them?

In addition to the Function interface, Modal has a direct
interface for defining containers *at runtime* and securely running arbitrary code
inside them.

This can be useful if, for example, you want to:

* Execute code generated by a language model.
* Create isolated environments for running untrusted code.
* Check out a git repository and run a command against it, like a test suite, or
  `npm lint`.
* Run containers with arbitrary dependencies and setup scripts.

Each individual job is called a **Sandbox** and can be created using the
[`Sandbox.create`](/docs/reference/modal.Sandbox#create) constructor:

<CodeTabs>
  {#snippet python()}

```python
sb_app = modal.App.lookup("my-app", create_if_missing=True)
sb = modal.Sandbox.create(app=sb_app)

p = sb.exec("python", "-c", "print('hello')", timeout=3)
print(p.stdout.read())

p = sb.exec("bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done", timeout=5)
for line in p.stdout:
    # Avoid double newlines by using end="".
    print(line, end="")

sb.terminate()
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python
sb_app = await modal.App.lookup.aio("my-app", create_if_missing=True)
sb = await modal.Sandbox.create.aio(app=sb_app)

p = await sb.exec.aio("python", "-c", "print('hello')", timeout=3)
print(await p.stdout.read.aio())

p = await sb.exec.aio("bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done", timeout=5)
async for line in p.stdout:
    # Avoid double newlines by using end="".
    print(line, end="")

await sb.terminate.aio()
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
import { ModalClient } from "modal";

const modal = new ModalClient();
const app = await modal.apps.fromName("my-app", {
  createIfMissing: true,
});
const image = modal.images.fromRegistry("python:3.13-slim");

const sb = await modal.sandboxes.create(app, image);

const p = await sb.exec(["python", "-c", "print('hello')"], {
  timeoutMs: 3 * 1000,
});
console.log(await p.stdout.readText());

const p2 = await sb.exec(
  ["bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done"],
  { timeoutMs: 5 * 1000 },
);
for await (const line of p2.stdout) {
  process.stdout.write(line);
}

await sb.terminate();
```

{/snippet}

{#snippet go()}

```go notest
package main

import (
	"context"
	"fmt"
	"io"
	"os"
	"time"

	modal "github.com/modal-labs/modal-client/go"
)

func main() {
	ctx := context.Background()
	mc, _ := modal.NewClient()

	app, _ := mc.Apps.FromName(ctx, "my-app", &modal.AppFromNameParams{
		CreateIfMissing: true,
	})
	image := mc.Images.FromRegistry("python:3.13-slim", nil)

	sb, _ := mc.Sandboxes.Create(ctx, app, image, nil)
	defer sb.Terminate(ctx, nil)

	p, _ := sb.Exec(ctx, []string{"python", "-c", "print('hello')"}, &modal.SandboxExecParams{
		Timeout: 3 * time.Second,
	})
	stdout, _ := io.ReadAll(p.Stdout)
	fmt.Println(string(stdout))

	p2, _ := sb.Exec(ctx, []string{"bash", "-c", "for i in {1..10}; do date +%T; sleep 0.5; done"}, &modal.SandboxExecParams{
		Timeout: 5 * time.Second,
	})
	io.Copy(os.Stdout, p2.Stdout)
}
```

{/snippet} </CodeTabs>

**Note:** you can run the above example as a script directly with `python my_script.py`. `modal run` is not needed here since there is no [entrypoint](/docs/guide/apps#entrypoints-for-ephemeral-apps).

Sandboxes require an [`App`](/docs/guide/apps) to be passed when spawned from outside
of a Modal container. You may pass in a regular `App` object or look one up by name with
[`App.lookup`](/docs/reference/modal.App#lookup). The `create_if_missing` flag on `App.lookup`
will create an `App` with the given name if it doesn't exist.

## Lifecycle

### Events

Every Sandbox moves through a series of lifecycle events as it progresses from
creation to completion. Understanding these events is useful for monitoring,
debugging, and building automations that react to Sandbox state changes.

The lifecycle events, in order, are:

1. **Created** — The Sandbox has been requested and registered with Modal. At this
   point the Sandbox object exists and has an ID, but no compute resources have been
   allocated yet. This is the initial state immediately after calling `Sandbox.create`.

2. **Scheduled** — The Sandbox has been scheduled to a specific worker. The
   worker is now provisioning the resources the Sandbox needs (CPU, memory, GPU,
   volumes, etc.) and preparing the container environment. The Sandbox will
   transition to **Started** once the container is fully initialized.

3. **Started** — The Sandbox's container has been launched on a worker and the
   entrypoint process (if any) is running. At this point you can begin executing
   commands inside the Sandbox with `sandbox.exec(...)`. Network tunnels and volume
   mounts are active.

4. **Ready** — If [readiness probes](/docs/guide/sandboxes#readiness-probes) are
   enabled for the Sandbox, this event fires once the probe succeeds, indicating that
   the service inside the Sandbox is fully initialized and ready to accept traffic.
   This is especially useful for Sandboxes running web servers or other services
   that need warm-up time before they can handle requests. If readiness probes are
   not configured, this event is skipped.

5. **Finished** — The Sandbox has stopped running. This can happen for several
   reasons: the entrypoint process exited on its own, the Sandbox was explicitly
   terminated (via the dashboard or `sandbox.terminate()`), the timeout or idle timeout
   was reached, or an out-of-memory condition occurred. Once finished, no further
   commands can be executed inside the Sandbox. You can learn more about why a Sandbox
   stopped running in the dashboard or by examining the exit code returned from
   `sandbox.poll()`.

### Timeouts

Sandboxes have a default maximum lifetime of 5 minutes. You can change this by passing
a `timeout` of up to 24 hours to the `Sandbox.create(...)` function.

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(app=sb_app, timeout=10*60)  # 10 minutes
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(app=sb_app, timeout=10*60)  # 10 minutes
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sb = await modal.sandboxes.create(app, image, {
  timeoutMs: 10 * 60 * 1000, // 10 minutes
});
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
	Timeout: 10 * time.Minute,
})
defer sb.Detach()
```

{/snippet} </CodeTabs>

If you need a Sandbox to run for more than 24 hours, we recommend using
[Filesystem Snapshots](/docs/guide/sandbox-snapshots) to preserve its state,
and then restore from that snapshot with a subsequent Sandbox.

### Idle Timeouts

Sandboxes can also be automatically terminated after a period of inactivity - you can do this by setting the `idle_timeout` parameter. A Sandbox is considered active if any of the following are true:

1. It has an active [command](/docs/guide/sandbox-spawn) running (via [`sb.exec(...)`](/docs/reference/modal.Sandbox#exec))
2. Its stdin is being written to (via [`sb.stdin.write()`](/docs/reference/modal.Sandbox#stdin))
3. It has an open TCP connection over one of its [Tunnels](/docs/guide/tunnels)

### Readiness Probes

<Callout variant="beta" />

After a Sandbox starts, you often need to run custom initialization logic before it's
ready for use — pulling code with `git pull`, installing dependencies, starting a server,
writing config files, or other setup that isn't baked into the image. Readiness probes
give you a way to track when that initialization is complete, so you don't have to build
the polling or signaling yourself. Modal also uses probe results to give you
observability into how long this startup phase typically takes.

A readiness probe is a check that Modal runs automatically inside the Sandbox at a
configurable interval. You can then call `wait_until_ready()` to block until the probe
succeeds.

There are two types of readiness probes:

* **TCP probe** — Checks whether a TCP port inside the Sandbox is accepting connections.
  This is the most common choice when your startup logic includes launching a server.
* **Exec probe** — Runs an arbitrary command inside the Sandbox and succeeds when the
  command exits with status code 0. Use this for any other readiness condition: checking
  that a file exists, verifying a setup script has completed, confirming dependencies are
  installed, etc.

Both probe types accept an `interval_ms` parameter (default: 100ms) that controls how
frequently the check is retried until it succeeds.

#### TCP readiness probe

Use a TCP probe when your Sandbox starts a server and you want to wait until
it's listening on a port:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(
    "python3", "-m", "http.server", "8080",
    readiness_probe=modal.Probe.with_tcp(8080),
    app=sb_app,
)

# Blocks until port 8080 is accepting connections
sb.wait_until_ready()

# The server is now ready — interact with it via tunnels, exec, etc.
sb.terminate()
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(
    "python3", "-m", "http.server", "8080",
    readiness_probe=modal.Probe.with_tcp(8080),
    app=sb_app,
)

# Blocks until port 8080 is accepting connections
await sb.wait_until_ready.aio()

# The server is now ready — interact with it via tunnels, exec, etc.
await sb.terminate.aio()
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
import { ModalClient, Probe } from "modal";

const modal = new ModalClient();
const app = await modal.apps.fromName("my-app", { createIfMissing: true });
const image = modal.images.fromRegistry("python:3.13-slim");

const sb = await modal.sandboxes.create(app, image, {
  command: ["python3", "-m", "http.server", "8080"],
  readinessProbe: Probe.withTcp(8080),
});

// Blocks until port 8080 is accepting connections
await sb.waitUntilReady();

// The server is now ready — interact with it via tunnels, exec, etc.
await sb.terminate();
```

{/snippet}

{#snippet go()}

```go notest
package main

import (
	"context"
	"time"

	modal "github.com/modal-labs/modal-client/go"
)

func main() {
	ctx := context.Background()
	mc, _ := modal.NewClient()

	app, _ := mc.Apps.FromName(ctx, "my-app", &modal.AppFromNameParams{
		CreateIfMissing: true,
	})
	image := mc.Images.FromRegistry("python:3.13-slim", nil)
	probe, _ := modal.NewTCPProbe(8080, nil)

	sb, _ := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
		Command:        []string{"python3", "-m", "http.server", "8080"},
		ReadinessProbe: probe,
	})
	defer sb.Detach()

	// Blocks until port 8080 is accepting connections
	sb.WaitUntilReady(ctx, 5*time.Minute)

	// The server is now ready — interact with it via tunnels, exec, etc.
	sb.Terminate(ctx, nil)
}
```

{/snippet} </CodeTabs>

#### Exec readiness probe

Use an exec probe when readiness depends on something other than a TCP port — for
example, waiting for a file to be created or a setup script to complete:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(
    "bash", "-c", "sleep 5 && touch /tmp/ready && sleep 3600",
    readiness_probe=modal.Probe.with_exec(
        "sh", "-c", "test -f /tmp/ready",
        interval_ms=250,
    ),
    app=sb_app,
)

# Blocks until "test -f /tmp/ready" exits with code 0
sb.wait_until_ready()

# The sandbox is now ready
p = sb.exec("cat", "/tmp/ready")
sb.terminate()
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(
    "bash", "-c", "sleep 5 && touch /tmp/ready && sleep 3600",
    readiness_probe=modal.Probe.with_exec(
        "sh", "-c", "test -f /tmp/ready",
        interval_ms=250,
    ),
    app=sb_app,
)

# Blocks until "test -f /tmp/ready" exits with code 0
await sb.wait_until_ready.aio()

# The sandbox is now ready
p = await sb.exec.aio("cat", "/tmp/ready")
await sb.terminate.aio()
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
import { ModalClient, Probe } from "modal";

const modal = new ModalClient();
const app = await modal.apps.fromName("my-app", { createIfMissing: true });
const image = modal.images.fromRegistry("python:3.13-slim");

const sb = await modal.sandboxes.create(app, image, {
  command: ["bash", "-c", "sleep 5 && touch /tmp/ready && sleep 3600"],
  readinessProbe: Probe.withExec(["sh", "-c", "test -f /tmp/ready"], {
    intervalMs: 250,
  }),
});

// Blocks until "test -f /tmp/ready" exits with code 0
await sb.waitUntilReady();

// The sandbox is now ready
await sb.terminate();
```

{/snippet}

{#snippet go()}

```go notest
package main

import (
	"context"
	"time"

	modal "github.com/modal-labs/modal-client/go"
)

func main() {
	ctx := context.Background()
	mc, _ := modal.NewClient()

	app, _ := mc.Apps.FromName(ctx, "my-app", &modal.AppFromNameParams{
		CreateIfMissing: true,
	})
	image := mc.Images.FromRegistry("python:3.13-slim", nil)
	probe, _ := modal.NewExecProbe(
		[]string{"sh", "-c", "test -f /tmp/ready"},
		&modal.ExecProbeParams{Interval: 250 * time.Millisecond},
	)

	sb, _ := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
		Command:        []string{"bash", "-c", "sleep 5 && touch /tmp/ready && sleep 3600"},
		ReadinessProbe: probe,
	})
	defer sb.Detach()

	// Blocks until "test -f /tmp/ready" exits with code 0
	sb.WaitUntilReady(ctx, 5*time.Minute)

	// The sandbox is now ready
	sb.Terminate(ctx, nil)
}
```

{/snippet} </CodeTabs>

**Note:** Readiness probes will run for a maximum of 5 minutes. If the probe does not
succeed within that window, `wait_until_ready()` will raise a `TimeoutError`. The probe
timeout does **not** automatically terminate the Sandbox — you may want to catch the
`TimeoutError` and explicitly terminate the Sandbox if readiness is never achieved:

<CodeTabs>
  {#snippet python()}

```python notest
try:
    sb.wait_until_ready()
except TimeoutError:
    print("Sandbox failed to become ready")
    sb.terminate()
    sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python notest
try:
    await sb.wait_until_ready.aio()
except TimeoutError:
    print("Sandbox failed to become ready")
    await sb.terminate.aio()
    await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
try {
  await sb.waitUntilReady();
} catch (err) {
  console.log("Sandbox failed to become ready");
  await sb.terminate();
}
```

{/snippet}

{#snippet go()}

```go notest
if err := sb.WaitUntilReady(ctx, 5*time.Minute); err != nil {
	fmt.Println("Sandbox failed to become ready")
	sb.Terminate(ctx, nil)
}
```

{/snippet} </CodeTabs>

If you call `wait_until_ready()` on a Sandbox that was not configured with a readiness
probe, an error will be raised. Similarly, calling it after the Sandbox has been
terminated will raise an error. However, calling `wait_until_ready()` after the Sandbox
has already become ready returns immediately.

## Return Codes

[Unix-style exit codes](https://tldp.org/LDP/abs/html/exitcodes.html) are provided to help diagnose conditions such as success, manual termination, or out-of-memory.

They are available on both:

* Processes in the sandbox (via [`ContainerProcess.returncode`](/docs/reference/modal.container_process#returncode) / [`ContainerProcess.poll()`](/docs/reference/modal.container_process#poll))
* The Sandbox itself (via [`Sandbox.returncode`](/docs/reference/modal.Sandbox#returncode) / [`Sandbox.poll()`](/docs/reference/modal.Sandbox#poll))

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(app=sb_app)

# Read returncode of individual process
p = sb.exec("sh", "-c", "exit 42")
p.wait()
print(p.returncode) # 42

# Read returncode of finished sandbox
# Terminate sends a SIGKILL, code 137
sb.terminate(wait=True)
print(sb.returncode) # 137
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(app=sb_app)

# Read returncode of individual process
p = await sb.exec.aio("sh", "-c", "exit 42")
await p.wait.aio()
print(p.returncode) # 42

# Read returncode of finished sandbox
# Terminate sends a SIGKILL, code 137
await sb.terminate.aio(wait=True)
print(sb.returncode) # 137
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sb = await modal.sandboxes.create(app, image);

// Read returncode of individual process
const p = await sb.exec(["sh", "-c", "exit 42"]);
const returnCode = await p.wait();
console.log(returnCode); // 42

// Read returncode of finished sandbox
// Terminate sends a SIGKILL, code 137
const returnCodeSb = await sb.terminate({ wait: true });
console.log(returnCodeSb); // 137
```

{/snippet}

{#snippet go()}

```go notest
sb, _ := mc.Sandboxes.Create(ctx, app, image, nil)

// Read returncode of individual process
p, _ := sb.Exec(ctx, []string{"sh", "-c", "exit 42"}, nil)
returnCode, _ := p.Wait(ctx)
fmt.Println(returnCode) // 42

// Read returncode of finished sandbox
// Terminate sends a SIGKILL, code 137
returnCodeSb, _ := sb.Terminate(ctx, &modal.SandboxTerminateParams{Wait: true})
fmt.Println(returnCodeSb) // 137
```

{/snippet} </CodeTabs>

## Configuration

Sandboxes support nearly all configuration options found in regular `modal.Function`s.
Refer to [`Sandbox.create`](/docs/reference/modal.Sandbox#create) for further documentation
on Sandbox configs.

For example, Images and Volumes can be used just as with functions:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(
    image=modal.Image.debian_slim().pip_install("pandas"),
    volumes={"/data": modal.Volume.from_name("data-volume", create_if_missing=True)},
    app=sb_app,
)
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(
    image=modal.Image.debian_slim().pip_install("pandas"),
    volumes={"/data": modal.Volume.from_name("data-volume", create_if_missing=True)},
    app=sb_app,
)
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const image = modal.images.fromRegistry("python:3.13-slim");
const volume = modal.volumes.fromName("my-volume");
const sb = await modal.sandboxes.create(app, image, {
  volumes: { "/data": volume },
  workdir: "/repo",
});
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
image := mc.Images.FromRegistry("python:3.13-slim", nil)
volume := mc.Volumes.FromName("my-volume", nil)
sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Volumes: map[string]*modal.Volume{"/data": volume},
  Workdir: "/repo",
})
defer sb.Detach()
```

{/snippet} </CodeTabs>

## Environments

### Environment variables

You can set environment variables using inline secrets:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
secret = modal.Secret.from_dict({"MY_SECRET": "hello"})

sb = modal.Sandbox.create(
    secrets=[secret],
    app=sb_app,
)
p = sb.exec("bash", "-c", "echo $MY_SECRET")
print(p.stdout.read())
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
secret = modal.Secret.from_dict({"MY_SECRET": "hello"})

sb = await modal.Sandbox.create.aio(
    secrets=[secret],
    app=sb_app,
)
p = await sb.exec.aio("bash", "-c", "echo $MY_SECRET")
print(await p.stdout.read.aio())
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const secret = modal.secrets.fromObject({ MY_SECRET: "hello" });
const image = modal.images.fromRegistry("python:3.13-slim");

const sb = await modal.sandboxes.create(app, image, {
  secrets: [secret],
});
const p = await sb.exec(["bash", "-c", "echo $MY_SECRET"]);
console.log(await p.stdout.readText());
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
secret, err := mc.Secrets.FromMap(ctx, map[string]string{"MY_SECRET": "hello"}, nil)
image := mc.Images.FromRegistry("python:3.13-slim", nil)

sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Secrets: []*modal.Secret{secret},
})
defer sb.Detach()
p, err := sb.Exec(ctx, []string{"bash", "-c", "echo $MY_SECRET"}, nil)
stdout, err := io.ReadAll(p.Stdout)
fmt.Println(string(stdout))
```

{/snippet} </CodeTabs>

### Custom Images

Sandboxes support [custom images](/docs/guide/images) just as Functions do. These can be defined using [method chaining](/docs/guide/images) or by referencing an [existing Image in an external container registry](/docs/guide/existing-images).

While you'll typically invoke a Modal Function with the `modal run` cli, and deploy a new version of your `App` on every change to the image, Sandboxes are often created with a separate lifecycle. Therefore, there are a few things to keep in mind:

#### Image builds and caching

Modal [caches built Images](https://modal.com/docs/guide/images#image-caching-and-rebuilds). For Sandboxes, this means that every time a Sandbox is created, the Image definition it references will be checked against the cache, and if there is no hit the Image will be built before the Sandbox is created.

On-the-fly Image builds can be useful when the Image must be defined at runtime rather than statically in advance (for example, when using a language model to determine dependencies), but they can also introduce undesirable latency affecting end users.

#### Separating Image builds from Sandbox creation

To avoid slowing down the creation of new Sandboxes while an Image build is happening, we recommend separating the Image build process from Sandbox creation. Use [`Image.build`](/docs/reference/modal.Image#build) to trigger Image builds as part of a deployment flow or at a regular interval (e.g., in a [scheduled job](/docs/guide/cron) or CI pipeline). When the build is complete, store the returned `image_id` and create Sandboxes using `Image.from_id()`:

<CodeTabs>
  {#snippet python()}

```python notest
# deploy.py
app = modal.App.lookup("sandbox-app", create_if_missing=True)

# Method-chained image
image = modal.Image.debian_slim().pip_install("pandas")

# Or, for an external registry image with a fixed tag:
# image = modal.Image.from_registry("ubuntu:24.04")

with modal.enable_output():
    image.build(app)

# Store the image_id for later use, e.g. in a Modal Dict
images = modal.Dict.from_name("images", create_if_missing=True)
images["latest-sandbox-image"] = image.object_id

# app.py
app = modal.App.lookup("sandbox-app", create_if_missing=True)
images = modal.Dict.from_name("images")

image = modal.Image.from_id(images["latest-sandbox-image"])
sb = modal.Sandbox.create(app=app, image=image)
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python notest
# deploy.py
app = await modal.App.lookup.aio("sandbox-app", create_if_missing=True)

# Method-chained image
image = modal.Image.debian_slim().pip_install("pandas")

# Or, for an external registry image with a fixed tag:
# image = modal.Image.from_registry("ubuntu:24.04")

with modal.enable_output():
    await image.build.aio(app)

# Store the image_id for later use, e.g. in a Modal Dict
images = modal.Dict.from_name("images", create_if_missing=True)
images["latest-sandbox-image"] = image.object_id

# app.py
app = await modal.App.lookup.aio("sandbox-app", create_if_missing=True)
images = modal.Dict.from_name("images")

image = modal.Image.from_id(images["latest-sandbox-image"])
sb = await modal.Sandbox.create.aio(app=app, image=image)
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
// deploy.js
const app = await modal.apps.fromName("sandbox-app", { createIfMissing: true });

// Use an explicit tag rather than :latest to get predictable caching behavior
const image = await modal.images.fromRegistry("ubuntu:24.04").build(app);

// Store image.imageId for later use (e.g. in a database or config)
const imageId = image.imageId;

// app.js
const image = await modal.images.fromId(imageId);
const sb = await modal.sandboxes.create(app, image);
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
// deploy.go
app, _ := mc.Apps.FromName(ctx, "sandbox-app", &modal.AppFromNameParams{
  CreateIfMissing: true,
})

// Use an explicit tag rather than :latest to get predictable caching behavior
image, _ := mc.Images.FromRegistry("ubuntu:24.04", nil).Build(ctx, app)

// Store image.ImageID for later use (e.g. in a database or config)
imageID := image.ImageID

// app.go
image, _ = mc.Images.FromID(ctx, imageID)
sb, _ := mc.Sandboxes.Create(ctx, app, image, nil)
defer sb.Detach()
```

{/snippet} </CodeTabs>

<Callout variant="info">

* **Modal treats image tags as immutable once pulled.** For [external registry](/docs/guide/existing-images) images, `Image.build` always returns the cached version — Modal does not detect upstream changes to mutable tags like `:latest`.
* To pick up a new version of an external registry image, update the tag in your deploy script (for example, `ubuntu:24.04` → `ubuntu:24.04-20240523`).

</Callout>

#### Image build logs

You may need to manually enable output streaming to see your image build logs:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
image = modal.Image.debian_slim().pip_install("pandas", "numpy")

with modal.enable_output():
    sb = modal.Sandbox.create(image=image, app=sb_app)
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
image = modal.Image.debian_slim().pip_install("pandas", "numpy")

with modal.enable_output():
    sb = await modal.Sandbox.create.aio(image=image, app=sb_app)
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const image = modal.images
  .fromRegistry("python:3.13-slim")
  .dockerfileCommands(["RUN pip install pandas numpy"]);

const sb = await modal.sandboxes.create(app, image);
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
image := mc.Images.FromRegistry("python:3.13-slim", nil).
  DockerfileCommands([]string{"RUN pip install pandas numpy"}, nil)

// Note: Image build logs are automatically streamed in Go
sb, err := mc.Sandboxes.Create(ctx, app, image, nil)
defer sb.Detach()
```

{/snippet} </CodeTabs>

## Running a Sandbox with an entrypoint

In most cases, Sandboxes are treated as a generic container that can run arbitrary
commands. However, in some cases, you may want to run a single command or script
as the entrypoint of the Sandbox. You can do this by passing command arguments to the
Sandbox constructor:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create("python", "-m", "http.server", "8080", app=sb_app, timeout=10)
for line in sb.stdout:
    print(line, end="")
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio("python", "-m", "http.server", "8080", app=sb_app, timeout=10)
async for line in sb.stdout:
    print(line, end="")
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sb = await modal.sandboxes.create(app, image, {
  command: ["python", "-m", "http.server", "8080"],
  timeoutMs: 10 * 1000,
});
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Command: []string{"python", "-m", "http.server", "8080"},
  Timeout: 10 * time.Second,
})
sb.Detach()
```

{/snippet} </CodeTabs>

This functionality is most useful for running long-lived services that you want
to keep running in the background. See our [Jupyter notebook example](/docs/examples/jupyter_sandbox)
for a more concrete example of this.

## Referencing Sandboxes from other code

If you have a running Sandbox, you can retrieve it using the `from_id` method.

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(app=sb_app)
sb_id = sb.object_id
sb.detach()

# ... later in the program ...

sb2 = modal.Sandbox.from_id(sb_id)
p = sb2.exec("echo", "hello")
print(p.stdout.read())
sb2.terminate()
sb2.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(app=sb_app)
sb_id = sb.object_id
await sb.detach.aio()

# ... later in the program ...

sb2 = await modal.Sandbox.from_id.aio(sb_id)
p = await sb2.exec.aio("echo", "hello")
print(await p.stdout.read.aio())
await sb2.terminate.aio()
await sb2.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sb = await modal.sandboxes.create(app, image);
const sbId = sb.sandboxId;
await sb.detach();

// ... later in the program ...

const sb2 = await modal.sandboxes.fromId(sbId);
const p = await sb2.exec(["echo", "hello"]);
console.log(await p.stdout.readText());
await sb2.terminate();
```

{/snippet}

{#snippet go()}

```go notest
sb, err := mc.Sandboxes.Create(ctx, app, image, nil)
defer sb.Detach()
sbId := sb.SandboxID

// ... later in the program ...

sb2, err := mc.Sandboxes.FromID(ctx, sbId)
defer sb2.Terminate(ctx, nil)
p, err := sb2.Exec(ctx, []string{"echo", "hello"}, nil)
stdout, err := io.ReadAll(p.Stdout)
fmt.Println(string(stdout))
```

{/snippet} </CodeTabs>

A common use case for this is keeping a pool of Sandboxes available for executing tasks
as they come in. You can keep a list of `object_id`s of Sandboxes that are "open" and
reuse them, closing over the `object_id` in whatever function is using them.

## Named Sandboxes

You can assign a name to a Sandbox when creating it. Each name must be unique within an app -
only one *running* Sandbox can use a given name at a time. Note that the associated app must be
a deployed app. Once a Sandbox completely stops running, its name becomes available for reuse.
Some applications find Sandbox Names to be useful for ensuring that no more than one Sandbox is
running per resource or project. If a Sandbox with the given name is already running, `create()`
will raise an error.

<CodeTabs>
  {#snippet python()}

```python notest
sb1 = modal.Sandbox.create(app=sb_app, name="my-name")
# This will raise a modal.exception.AlreadyExistsError.
sb2 = modal.Sandbox.create(app=sb_app, name="my-name")
```

{/snippet}

{#snippet python\_async()}

```python notest
sb1 = await modal.Sandbox.create.aio(app=sb_app, name="my-name")
# This will raise a modal.exception.AlreadyExistsError.
sb2 = await modal.Sandbox.create.aio(app=sb_app, name="my-name")
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sb1 = await modal.sandboxes.create(app, image, { name: "my-name" });
// this will raise an AlreadyExistsError
const sb2 = await modal.sandboxes.create(app, image, { name: "my-name" });
```

{/snippet}

{#snippet go()}

```go notest
sb1, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Name: "my-name",
})
// this will return an error
sb2, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Name: "my-name",
})
```

{/snippet} </CodeTabs>

A named Sandbox may be fetched from a deployed app using `from_name()` *but only
if the Sandbox is currently running*. If no running Sandbox is found, `from_name()` will raise
an error.

<CodeTabs>
  {#snippet python()}

```python notest
sb_app = modal.App.lookup("my-app", create_if_missing=True)
sb1 = modal.Sandbox.create(app=sb_app, name="my-name")
# Returns the currently running Sandbox with the name "my-name" from the
# deployed app named "my-app".
sb2 = modal.Sandbox.from_name("my-app", "my-name")
assert sb1.object_id == sb2.object_id # sb1 and sb2 refer to the same Sandbox
sb1.detach()
sb2.detach()
```

{/snippet}

{#snippet python\_async()}

```python notest
sb_app = await modal.App.lookup.aio("my-app", create_if_missing=True)
sb1 = await modal.Sandbox.create.aio(app=sb_app, name="my-name")
# Returns the currently running Sandbox with the name "my-name" from the
# deployed app named "my-app".
sb2 = await modal.Sandbox.from_name.aio("my-app", "my-name")
assert sb1.object_id == sb2.object_id # sb1 and sb2 refer to the same Sandbox
await sb1.detach.aio()
await sb2.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const app = await modal.apps.fromName("my-app", { createIfMissing: true });
const sb1 = await modal.sandboxes.create(app, image, { name: "my-name" });
// returns the currently running Sandbox with the name "my-name" from the
// deployed app named "my-app".
const sb2 = await modal.sandboxes.fromName("my-app", "my-name");
console.assert(sb1.sandboxId === sb2.sandboxId); // sb1 and sb2 refer to the same Sandbox
sb1.detach();
sb2.detach();
```

{/snippet}

{#snippet go()}

```go notest
app, err := mc.Apps.FromName(ctx, "my-app", &modal.AppFromNameParams{
  CreateIfMissing: true,
})
sb1, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Name: "my-name",
})
// returns the currently running Sandbox with the name "my-name" from the
// deployed app named "my-app".
sb2, err := mc.Sandboxes.FromName(ctx, "my-app", "my-name", nil)
// sb1 and sb2 refer to the same Sandbox
fmt.Println(sb1.SandboxID == sb2.SandboxID)
defer sb1.Detach()
defer sb2.Detach()
```

{/snippet} </CodeTabs>

Sandbox Names may contain only alphanumeric characters, dashes, periods, and underscores, and must
be shorter than 64 characters.

## Tagging

Sandboxes can also be tagged with arbitrary key-value pairs. These tags can be used
to filter results in `Sandbox.list`.

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sandbox_v1_1 = modal.Sandbox.create("sleep", "10", app=sb_app)
sandbox_v1_2 = modal.Sandbox.create("sleep", "20", app=sb_app)

sandbox_v1_1.set_tags({"major_version": "1", "minor_version": "1"})
sandbox_v1_2.set_tags({"major_version": "1", "minor_version": "2"})

for sandbox in modal.Sandbox.list(app_id=sb_app.app_id):  # All sandboxes.
    print(sandbox.object_id)

for sandbox in modal.Sandbox.list(
    app_id=sb_app.app_id,
    tags={"major_version": "1"},
):  # Also all sandboxes.
    print(sandbox.object_id)

for sandbox in modal.Sandbox.list(
    app_id=sb_app.app_id,
    tags={"major_version": "1", "minor_version": "2"},
):  # Just the latest sandbox.
    print(sandbox.object_id)

sandbox_v1_1.detach()
sandbox_v1_2.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sandbox_v1_1 = await modal.Sandbox.create.aio("sleep", "10", app=sb_app)
sandbox_v1_2 = await modal.Sandbox.create.aio("sleep", "20", app=sb_app)

await sandbox_v1_1.set_tags.aio({"major_version": "1", "minor_version": "1"})
await sandbox_v1_2.set_tags.aio({"major_version": "1", "minor_version": "2"})

async for sandbox in modal.Sandbox.list.aio(app_id=sb_app.app_id):  # All sandboxes.
    print(sandbox.object_id)

async for sandbox in modal.Sandbox.list.aio(
    app_id=sb_app.app_id,
    tags={"major_version": "1"},
):  # Also all sandboxes.
    print(sandbox.object_id)

async for sandbox in modal.Sandbox.list.aio(
    app_id=sb_app.app_id,
    tags={"major_version": "1", "minor_version": "2"},
):  # Just the latest sandbox.
    print(sandbox.object_id)

await sandbox_v1_1.detach.aio()
await sandbox_v1_2.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sandboxV1_1 = await modal.sandboxes.create(app, image, {
  command: ["sleep", "10"],
});
const sandboxV1_2 = await modal.sandboxes.create(app, image, {
  command: ["sleep", "20"],
});

await sandboxV1_1.setTags({ major_version: "1", minor_version: "1" });
await sandboxV1_2.setTags({ major_version: "1", minor_version: "2" });

// All sandboxes.
for await (const sandbox of modal.sandboxes.list({ appId: app.appId })) {
  console.log(sandbox.sandboxId);
}

// Also all sandboxes.
for await (const sandbox of modal.sandboxes.list({
  appId: app.appId,
  tags: { major_version: "1" },
})) {
  console.log(sandbox.sandboxId);
}

// Just the latest sandbox.
for await (const sandbox of modal.sandboxes.list({
  appId: app.appId,
  tags: { major_version: "1", minor_version: "2" },
})) {
  console.log(sandbox.sandboxId);
}
sandboxV1_1.detach();
sandboxV1_2.detach();
```

{/snippet}

{#snippet go()}

```go notest
sandboxV1_1, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Command: []string{"sleep", "10"},
})
sandboxV1_2, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
  Command: []string{"sleep", "20"},
})
defer sandboxV1_1.Detach()
defer sandboxV1_2.Detach()

sandboxV1_1.SetTags(ctx, map[string]string{"major_version": "1", "minor_version": "1"})
sandboxV1_2.SetTags(ctx, map[string]string{"major_version": "1", "minor_version": "2"})

// All sandboxes.
it, _ := mc.Sandboxes.List(ctx, &modal.SandboxListParams{
  AppID: app.AppID,
})
for sandbox := range it {
  fmt.Println(sandbox.SandboxID)
}

// Also all sandboxes.
it, _ = mc.Sandboxes.List(ctx, &modal.SandboxListParams{
  AppID: app.AppID,
  Tags:  map[string]string{"major_version": "1"},
})
for sandbox := range it {
  fmt.Println(sandbox.SandboxID)
}

// Just the latest sandbox.
it, _ = mc.Sandboxes.List(ctx, &modal.SandboxListParams{
  AppID: app.AppID,
  Tags:  map[string]string{"major_version": "1", "minor_version": "2"},
})
for sandbox := range it {
  fmt.Println(sandbox.SandboxID)
}
```

{/snippet} </CodeTabs>

## Cleaning up Client-side Connections

Unlike other Modal objects, the local Sandbox will hold a direct connection to
its compute substrate. While this connection should be automatically closed
during garbage collection, we recommend explicitly cleaning up the resources
once you are finished interacting with the Sandbox by calling its `detach()` method:

<CodeTabs>
  {#snippet python()}

```python fixture:sb_app
sb = modal.Sandbox.create(app=sb_app)
sb.detach()
```

{/snippet}

{#snippet python\_async()}

```python fixture:sb_app
sb = await modal.Sandbox.create.aio(app=sb_app)
await sb.detach.aio()
```

{/snippet}

{#snippet javascript()}

```javascript notest
const sb = await modal.sandboxes.create(app, image);
sb.detach();
```

{/snippet}

{#snippet go()}

```go notest
sb, err := mc.Sandboxes.Create(ctx, app, image, nil)
defer sb.Detach()
```

{/snippet} </CodeTabs>

After calling `detach`, any operation using the Sandbox object is not guaranteed to
work. If you want to continue interacting with a running sandbox, use `Sandbox.from_id`
to get a new Sandbox object that references the original sandbox. In the Python SDK,
`terminate` leaves your sandbox attached, so we recommend calling `detach` after you
are done with your terminated sandbox. In the Go/JS SDK, `Terminate` will also detach
your sandbox.
