Skip to main content
Modal Sandboxes provide an interactive way to run arbitrary code in a containerized environment. Unlike Functions, which execute a single task and terminate, Sandboxes give you a persistent container where you can execute multiple commands, stream I/O, and run long-lived processes.

Creating a sandbox

Create a Sandbox with modal.Sandbox.create():
import modal

app = modal.App.lookup("my-app", create_if_missing=True)
sandbox = modal.Sandbox.create("echo", "Hello, world!", app=app)
print(sandbox.stdout.read())
sandbox.wait()
The first argument and any subsequent positional arguments form the command to execute.

Sandbox configuration

Configure Sandboxes with the same options as Functions:
sandbox = modal.Sandbox.create(
    "python", "-c", "import torch; print(torch.cuda.is_available())",
    app=app,
    image=modal.Image.debian_slim().pip_install("torch"),
    gpu="A100",
    timeout=600,
    secrets=[modal.Secret.from_name("my-secret")]
)
sandbox = modal.Sandbox.create(
    "python", "script.py",
    app=app,
    image=modal.Image.debian_slim().pip_install("numpy")
)

Executing commands

Once a Sandbox is created, execute additional commands with .exec():
sandbox = modal.Sandbox.create("bash", app=app, pty=True)

# Execute a command
process = sandbox.exec("ls", "-la")
process.wait()
print(process.stdout.read())

# Execute another command in the same container
process = sandbox.exec("pwd")
print(process.stdout.read())

Working directory

Set the working directory for commands:
sandbox = modal.Sandbox.create(
    "bash",
    app=app,
    workdir="/app"
)

# Commands run in /app by default
process = sandbox.exec("pwd")
print(process.stdout.read())  # /app

Streaming I/O

Access stdout and stderr as they’re produced:
sandbox = modal.Sandbox.create("bash", "-c", "for i in {1..5}; do echo $i; sleep 1; done", app=app)

# Stream output line by line
for line in sandbox.stdout:
    print(f"Output: {line}")

sandbox.wait()

Reading stderr

Access stderr separately:
sandbox = modal.Sandbox.create("bash", "-c", "echo error >&2", app=app)
print("stdout:", sandbox.stdout.read())
print("stderr:", sandbox.stderr.read())
sandbox.wait()

Interactive terminals

Enable PTY (pseudo-terminal) support for interactive sessions:
sandbox = modal.Sandbox.create(
    "bash",
    app=app,
    pty=True
)

# Now you can send input
sandbox.stdin.write("echo Hello\n")
sandbox.stdin.write("exit\n")
sandbox.stdin.drain()

print(sandbox.stdout.read())
sandbox.wait()
PTY mode is useful for running interactive programs like shells, REPLs, or text editors that expect a terminal.

Timeouts

Set timeouts to prevent Sandboxes from running indefinitely:
# Sandbox terminates after 300 seconds
sandbox = modal.Sandbox.create(
    "sleep", "1000",
    app=app,
    timeout=300
)

try:
    sandbox.wait()
except modal.exception.SandboxTimeoutError:
    print("Sandbox timed out")

Idle timeout

Terminate Sandboxes that are idle:
sandbox = modal.Sandbox.create(
    "bash",
    app=app,
    timeout=3600,        # Maximum lifetime: 1 hour
    idle_timeout=300     # Terminate after 5 minutes of inactivity
)

Network access

Opening ports

Expose ports for network services:
sandbox = modal.Sandbox.create(
    "python", "-m", "http.server", "8000",
    app=app,
    encrypted_ports=[8000]
)

# Access the service via tunnel
print(sandbox.tunnels())

Blocking network

Restrict network access:
sandbox = modal.Sandbox.create(
    "curl", "https://example.com",
    app=app,
    block_network=True  # This will fail
)
Allow specific CIDRs:
sandbox = modal.Sandbox.create(
    "curl", "https://api.example.com",
    app=app,
    cidr_allowlist=["192.168.1.0/24", "10.0.0.0/8"]
)

File operations

Interact with files in the Sandbox:
# Create a sandbox
sandbox = modal.Sandbox.create("bash", app=app, pty=True)

# Write a file
file = sandbox.open("/tmp/test.txt", "w")
file.write("Hello, Sandbox!")
file.close()

# Read the file
process = sandbox.exec("cat", "/tmp/test.txt")
print(process.stdout.read())

sandbox.terminate()

Container snapshots

Create snapshots for faster startup:
sandbox = modal.Sandbox.create(
    "bash",
    app=app,
    _experimental_enable_snapshot=True
)

# Perform setup
setup = sandbox.exec("pip", "install", "some-package")
setup.wait()

# Create a snapshot
snapshot = sandbox.snapshot()

# Later, create new sandboxes from the snapshot
fast_sandbox = modal.Sandbox.from_snapshot(
    snapshot.object_id,
    app=app
)
Snapshots are an experimental feature and the API may change.

Lifecycle management

Waiting for completion

Wait for a Sandbox to complete:
sandbox = modal.Sandbox.create("sleep", "5", app=app)
result = sandbox.wait()  # Blocks until completion
print(f"Exit code: {result.returncode}")

Polling status

Check if a Sandbox is still running:
sandbox = modal.Sandbox.create("sleep", "100", app=app)

if sandbox.poll() is None:
    print("Still running")
else:
    print("Completed")

Terminating sandboxes

Manually terminate a Sandbox:
sandbox = modal.Sandbox.create("sleep", "1000", app=app)

# Do some work...

sandbox.terminate()

Use cases

Running Jupyter notebooks

sandbox = modal.Sandbox.create(
    "jupyter", "notebook", "--ip=0.0.0.0", "--port=8888", "--no-browser",
    app=app,
    image=modal.Image.debian_slim().pip_install("jupyter"),
    encrypted_ports=[8888],
    timeout=3600
)

print("Jupyter URL:", sandbox.tunnels()[8888].url)

Interactive Python REPL

sandbox = modal.Sandbox.create(
    "python",
    app=app,
    image=modal.Image.debian_slim().pip_install("numpy", "pandas"),
    pty=True
)

sandbox.stdin.write("import numpy as np\n")
sandbox.stdin.write("print(np.__version__)\n")
sandbox.stdin.write("exit()\n")
sandbox.stdin.drain()

print(sandbox.stdout.read())
sandbox.wait()

Building projects

sandbox = modal.Sandbox.create(
    "bash",
    app=app,
    image=modal.Image.debian_slim().apt_install("build-essential"),
    workdir="/build",
    volumes={"/build": modal.Volume.from_name("build-cache")}
)

# Run build commands
make = sandbox.exec("make", "all")
make.wait()

if make.returncode == 0:
    print("Build successful")
else:
    print("Build failed:", make.stderr.read())

sandbox.terminate()

Running tests

sandbox = modal.Sandbox.create(
    "pytest", "-v",
    app=app,
    image=modal.Image.debian_slim().pip_install("pytest"),
    workdir="/tests",
    timeout=600
)

sandbox.wait()
print("Test output:", sandbox.stdout.read())

Best practices

Always wait or terminate

Ensure Sandboxes don’t run indefinitely:
sandbox = modal.Sandbox.create("bash", app=app)
try:
    # Do work...
    sandbox.wait()
finally:
    sandbox.terminate()

Use appropriate timeouts

Set realistic timeouts based on expected execution time:
# Short-lived task
sandbox = modal.Sandbox.create(
    "quick-command",
    app=app,
    timeout=60
)

# Long-running service
sandbox = modal.Sandbox.create(
    "server",
    app=app,
    timeout=3600,
    idle_timeout=600
)

Handle I/O carefully

Read output to prevent buffer overflow:
sandbox = modal.Sandbox.create("command-with-lots-of-output", app=app)

# Stream output instead of reading all at once
for line in sandbox.stdout:
    process_line(line)

sandbox.wait()

Use volumes for persistence

Store data that should persist beyond the Sandbox lifetime:
volume = modal.Volume.from_name("my-data", create_if_missing=True)

sandbox = modal.Sandbox.create(
    "process-data",
    app=app,
    volumes={"/data": volume}
)

Build docs developers (and LLMs) love