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. |
| Custom domains | ✅ Supported | custom_domain on create; tunnel hostnames become subdomains of your domain. Requires prior setup by Modal — see Custom domains. |
| 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(). |
| 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 parameter on create. |
| 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. |
| GPUs | ❌ Not yet | |
| Memory snapshots | ❌ Not yet | Filesystem snapshots do work. |
Sandbox.from_name() | ❌ Not yet | Use Sandbox.from_id(sb.object_id) instead. |
modal shell (CLI) | ❌ Not yet | Connecting to a running V2 Sandbox via modal shell <sandbox-id> / <task-id> is not supported; use sb.exec(...) instead. |
| Connect tokens | ❌ Not yet | |
| Unencrypted tunnels | ❌ Not yet | Use encrypted_ports instead. |
| Network file systems | ❌ Not yet |
Retrieving a Sandbox by ID
Because Sandbox.from_name() does not work with V2 Sandboxes, store the object_id after creation and use Sandbox.from_id to reattach:
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)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.