V2 Sandboxes
Modal’s next-generation Sandbox backend has a number of advantages over the existing backend:
- Create sandboxes with higher throughput
- Lower time-to-interactive
- Run more sandboxes concurrently
The new backend is recommended to users wanting:
- >20 sandbox creates per second
- >10,000 concurrent sandboxes
To use it, call Sandbox._experimental_create instead of Sandbox.create. Beyond that,
the interface is exactly the same. Most features are supported, but a few are not —
see the Feature support section below for details. If all features you
use are supported, changing the “create” method is the only change you’ll need to make!
Example usage
Make sure you are on the latest version of the Modal client:
uv pip install --upgrade modalAll of the core Sandbox operations — exec, wait, poll, terminate,
reading stdout/stderr, and writing to stdin — work the same way as
they do in the standard Sandbox API:
sb = modal.Sandbox._experimental_create(
"sleep", "300",
app=sb_app,
cpu=2,
memory=4096, # MiB
)
p = sb.exec("python", "-c", "print('hello world')")
print(p.stdout.read())
p.wait()
assert p.returncode == 0
sb.terminate()sb = await modal.Sandbox._experimental_create.aio(
"sleep", "300",
app=sb_app,
cpu=2,
memory=4096, # MiB
)
p = await sb.exec.aio("python", "-c", "print('hello world')")
print(await p.stdout.read.aio())
await p.wait.aio()
assert p.returncode == 0
await sb.terminate.aio()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.experimentalCreate(app, image, {
command: ["sleep", "300"],
cpu: 2,
memoryMiB: 4096,
});
const p = await sb.exec(["python", "-c", "print('hello world')"]);
const output = await p.stdout.readText();
console.log(output);
const exitCode = await p.wait();
console.assert(exitCode === 0);
await sb.terminate();sb, _ := mc.Sandboxes.ExperimentalCreate(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"sleep", "300"},
CPU: 2,
MemoryMiB: 4096,
})
p, _ := sb.Exec(ctx, []string{"python", "-c", "print('hello world')"}, nil)
stdout, _ := io.ReadAll(p.Stdout)
fmt.Println(string(stdout))
exitCode, _ := p.Wait(ctx, nil)
fmt.Println("exit code:", exitCode)
sb.Terminate(ctx, nil)Feature support
| Feature | Status | Notes |
|---|---|---|
| Exec | ✅ Supported | sb.exec(...) |
| Stdin / stdout / stderr | ✅ Supported | Stream I/O with running processes. |
| CPU and memory configuration | ✅ Supported | cpu and memory on create. |
| Custom images | ✅ Supported | Any modal.Image via the image parameter. |
| Secrets and environment variables | ✅ Supported | secrets and env on create, or per-exec. |
| Volumes | ✅ Supported | modal.Volume via the volumes parameter. |
| Reload volumes | ✅ Supported | sb.reload_volumes() blocks until the reload completes; pass a timeout (default 55s) to bound the wait. Not supported on VM Sandboxes. |
| Cloud bucket mounts | ✅ Supported | modal.CloudBucketMount with static credentials or OIDC. |
| Encrypted tunnels | ✅ Supported | encrypted_ports on create, sb.tunnels() to retrieve. |
| Unencrypted tunnels | ✅ Supported | unencrypted_ports on create, sb.tunnels() to retrieve. Prefer encrypted tunnels when possible. |
| Custom domains | ✅ Supported | custom_domain on create; tunnel hostnames become subdomains of your domain. Requires prior setup by Modal — see Custom domains. |
| Connect tokens | ✅ Supported | sb.create_connect_token(...) for authenticated HTTP/WebSocket access — see Connecting to Sandboxes. |
| Filesystem API | ✅ Supported | Read and write files via sb.filesystem — see Filesystem access. Requires Python SDK ≥ 1.5.2.dev4 (a nightly build, until 1.5.2 is released — see above). The deprecated sb.open() / sb.ls() / sb.mkdir() / sb.rm() / sb.watch() methods are not supported. |
| Filesystem snapshots | ✅ Supported | sb.snapshot_filesystem(). |
| Directory snapshots | ✅ Supported | sb.snapshot_directory(). |
| Memory snapshots | ✅ Supported | sb._experimental_snapshot() / Sandbox._experimental_from_snapshot(), with _experimental_enable_snapshot=True on _experimental_create. In alpha. |
| Region placement | ✅ Supported | region parameter. |
| Network control | ✅ Supported | block_network and CIDR allowlists. |
| Private networking (i6pn) | ✅ Supported | i6pn=True; pin sandboxes to the same region. See Private networking. |
| Names | ✅ Supported | name on create, or sb._experimental_set_name(...) afterwards. Look up a running named Sandbox with Sandbox._experimental_from_name(app_name, name) — see below. |
| OIDC identity tokens | ✅ Supported | include_oidc_identity_token, oidc_auth_role_arn on CloudBucketMount. |
| Proxies | ✅ Supported | proxy parameter. |
| VM runtime | ✅ Supported | experimental_options={"vm_runtime": True}. See VM Sandboxes. |
| Reattach by ID | ✅ Supported | Store sb.object_id; use Sandbox.from_id(id). See below. |
| Listing sandboxes | ✅ Supported | Sandbox._experimental_list(app_id=...). Sandbox.list() does not return V2 sandboxes. Tags are supported. |
modal shell (CLI) | ✅ Supported | Connect to a running V2 Sandbox with modal shell <sandbox-id> or modal shell <task-id>. |
| GPUs | ❌ Not yet | |
| Network file systems | ❌ Not yet |
Retrieving a Sandbox by ID
You can reattach to any V2 Sandbox by storing its object_id after creation and using Sandbox.from_id:
sb = modal.Sandbox._experimental_create("sleep", "300", app=sb_app)
sandbox_id = sb.object_id
print(f"Sandbox ID: {sandbox_id}")
# Later, reattach:
sb2 = modal.Sandbox.from_id(sandbox_id)
p = sb2.exec("echo", "reattached")
print(p.stdout.read())
p.wait()
sb2.terminate()sb = await modal.Sandbox._experimental_create.aio("sleep", "300", app=sb_app)
sandbox_id = sb.object_id
print(f"Sandbox ID: {sandbox_id}")
# Later, reattach:
sb2 = await modal.Sandbox.from_id.aio(sandbox_id)
p = await sb2.exec.aio("echo", "reattached")
print(await p.stdout.read.aio())
await p.wait.aio()
await sb2.terminate.aio()import { ModalClient } from "modal";
const modal = new ModalClient();
const app = await modal.apps.fromName("my-app", { createIfMissing: true });
const image = modal.images.fromRegistry("alpine:3.21");
const sb = await modal.sandboxes.experimentalCreate(app, image, {
command: ["sleep", "300"],
});
const sandboxId = sb.sandboxId;
console.log(`Sandbox ID: ${sandboxId}`);
// Later, reattach:
const sb2 = await modal.sandboxes.fromId(sandboxId);
const p = await sb2.exec(["echo", "reattached"]);
const output = await p.stdout.readText();
console.log(output);
await p.wait();
await sb2.terminate();sb, _ := mc.Sandboxes.ExperimentalCreate(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"sleep", "300"},
})
sandboxID := sb.SandboxID
fmt.Printf("Sandbox ID: %s\n", sandboxID)
// Later, reattach:
sb2, _ := mc.Sandboxes.FromID(ctx, sandboxID, nil)
p, _ := sb2.Exec(ctx, []string{"echo", "reattached"}, nil)
stdout, _ := io.ReadAll(p.Stdout)
fmt.Println(string(stdout))
sb2.Terminate(ctx, nil)Retrieving a Sandbox by name
You can assign a name to a V2 Sandbox and later look it up by that name, as long
as its App is deployed. Names are unique within an App: only one running
Sandbox may hold a given name at a time. A name is released for reuse once
the Sandbox stops. Set the name with the name parameter on _experimental_create and resolve it with Sandbox._experimental_from_name:
app = modal.App.lookup("my-app", create_if_missing=True)
sb = modal.Sandbox._experimental_create("sleep", "300", app=app, name="my-sandbox")
# Later, from a deployed App, resolve the running Sandbox by name:
sb2 = modal.Sandbox._experimental_from_name("my-app", "my-sandbox")
assert sb.object_id == sb2.object_idapp = await modal.App.lookup.aio("my-app", create_if_missing=True)
sb = await modal.Sandbox._experimental_create.aio("sleep", "300", app=app, name="my-sandbox")
# Later, from a deployed App, resolve the running Sandbox by name:
sb2 = await modal.Sandbox._experimental_from_name.aio("my-app", "my-sandbox")
assert sb.object_id == sb2.object_idimport { ModalClient } from "modal";
const modal = new ModalClient();
const app = await modal.apps.fromName("my-app", { createIfMissing: true });
const image = modal.images.fromRegistry("alpine:3.21");
const sb = await modal.sandboxes.experimentalCreate(app, image, {
command: ["sleep", "300"],
name: "my-sandbox",
});
// Later, from a deployed App, resolve the running Sandbox by name:
const sb2 = await modal.sandboxes.experimentalFromName("my-app", "my-sandbox");
console.assert(sb.sandboxId === sb2.sandboxId);app, _ := mc.Apps.FromName(ctx, "my-app", &modal.AppFromNameParams{CreateIfMissing: true})
image := mc.Images.FromRegistry("alpine:3.21", nil)
sb, _ := mc.Sandboxes.ExperimentalCreate(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"sleep", "300"},
Name: "my-sandbox",
})
// Later, from a deployed App, resolve the running Sandbox by name:
sb2, _ := mc.Sandboxes.ExperimentalFromName(ctx, "my-app", "my-sandbox", nil)
fmt.Println(sb.SandboxID == sb2.SandboxID)If you created a Sandbox without a name, you can assign one later once with sb._experimental_set_name("my-sandbox") (sb.experimentalSetName(...) in
JavaScript, sb.ExperimentalSetName(...) in Go), then resolve it with _experimental_from_name as shown above.
Miscellaneous
- When
modal.Sandbox._experimental_create()returns, the sandbox is already scheduled onto a machine (though not yet ready). This differs from v1 semantics, wherecreate()returns before the sandbox is scheduled. As such, once the_experimental_create()call returns, you can expect a shorter, more consistentwait_until_ready:You can read more about the sandbox lifecycle in the sandbox documentation.sb = modal.Sandbox._experimental_create(app=app, readiness_probe=some_probe) sb.wait_until_ready()
If you hit a rough edge that isn’t listed here, please reach out via Slack or email us at support@modal.com.