Comparison to Alternatives
There are generally two responses when you show people Monty:
- This solves so many problems, I want it.
- Why not X?
Oddly often these responses are combined: people have not found an alternative that works for them, but are incredulous that there is really no better option than writing a Python implementation from scratch.
This page runs through the most obvious alternatives and why they were not right for what we wanted: somewhere to run code written by a model, per request, with nothing else in the loop. All of these technologies are impressive and widely used. Most were not conceived as an LLM sandbox, which is why they are not necessarily great at being one.
The chart is the time to create a sandbox and then run ten REPL commands in it; both halves are measured below.
| Tech | Language completeness | Security | Start latency | FOSS | Setup | File mounting | Snapshotting |
|---|---|---|---|---|---|---|---|
| Monty | partial | strict | 0.08 ms warm, 5 ms cold | free / OSS | easy | easy | interpreter, kilobytes |
| Full Monty | partial, or full via proxy | strict + OS-level | 2 ms warm, 4 ms cold | not free | easy | easy | interpreter, kilobytes |
| Docker | full | good | 195 ms | free / OSS | intermediate | easy | CRIU image, experimental |
| Pyodide | full | poor | 2700 ms | free / OSS | intermediate | easy | no |
| starlark-rust | very limited | good | 1.3 ms | free / OSS | easy | not available? | no |
| WASI / wasmtime | partial, almost full | strict | 16 ms | free / OSS | intermediate | easy | no |
| sandboxing service | full | strict | 1500 ms | not free | intermediate | hard | VM memory image, 100s of MB |
| YOLO Python | full | non-existent | 0.1 ms / 30 ms | free / OSS | easy | easy / scary | no |
Snapshotting means pausing code mid-execution, serialising its state, and resuming it later, possibly elsewhere or more than once. Only an interpreter built for it can do that at the interpreter level; a microVM can do it for its whole memory, at a thousand times the size, and without knowing what the paused code was waiting for. Durable-execution frameworks such as Temporal are not snapshotting: they replay a workflow written for them, which a script a model just wrote is not.
Start latency is the time from requesting a sandbox to receiving the result of 1 + 1.
The agent run below is ten REPL commands against a sandbox that already exists.
Both come from
scripts/startup_performance.py; the
chart adds them.
Start latency measures one execution. An agent in code mode sends several blocks to one environment, each building on the last, so the same script also times ten REPL feeds against a sandbox that already exists, and the chart above adds the two:
| Sandbox | Cold start | Agent run, warm† | Combined |
|---|---|---|---|
| Monty, warm pool | 0.08 ms | 0.4 ms | 0.5 ms |
| Monty, cold start | 5 ms | 0.4 ms | 5 ms |
| Full Monty, client pool already open | 2 ms | 4 ms | 6 ms |
| Full Monty, cold start | 4 ms | 4 ms | 7 ms |
| WASI / wasmtime, precompiled CPython | 16 ms | 180 ms | 200 ms |
Docker, running container, docker exec | 195 ms | 700 ms | 900 ms |
| Sandboxing service, existing Daytona sandbox | 1500 ms | 400 ms | 1900 ms |
| Pyodide, running Deno sandbox | 2700 ms | 35 ms | 2700 ms |
The two Monty rows differ only in whether a worker already exists in the pool; the chart uses the cold one. Full Monty gives every session a fresh worker, so its two rows differ only on the client side: whether the pool object and event loop already exist.
† 10 commands run in a REPL, as you might expect from a simple agent with code mode.
Monty and Full Monty keep the session, so each command is one feed_run.
None of the others has a persistent interpreter to feed: python.wasm is a WASI command module whose _start runs
once, a container or a service runs one program per request, and the Pyodide sandbox evaluates each call in fresh
globals.
For those, command n re-runs commands 1 to n, the cheapest strategy that gives the same result, so the cost is ten
interpreter starts plus the replayed work.
The commands themselves are in AGENT_BLOCKS in the script: a list of orders, a function, comprehensions, json, and
an f-string report; every setup must print the same report.
Every row was measured on 2026-09-03 (Full Monty on 2026-09-04) on an Apple M3 Max (96 GB, macOS 26.5.2) in London, from CPython 3.14.7, with a single sample per cold start unless stated. The tables round the numbers; the measured cold-start values are in the text below.
- Monty:
pydantic-monty0.0.21 with a release build of themontyworker binary, driven throughMonty()/pool.checkout()/session.feed_run(), the package’s only execution API. Cold start creates the pool, which spawns the worker subprocess, completes the protocol handshake, checks out a session and runs1 + 1; the median of 7 runs is 4.5 ms. Warm pool is the median of 20checkout()+feed_run()round trips against a pool whose worker already exists. The agent run is tenfeed_runcalls on one checkout, so state persists and nothing is replayed. - Full Monty: the Full Monty container image (0.0.22, a native
linux/arm64build) running in Docker Desktop 29.6.2 on the same machine, dialled withpydantic-monty0.0.22’sAsyncMontyWebsocketoverws://localhost. The client runs in a second container on the same host so the figure is the server’s own overhead over loopback, not Docker Desktop’s port-forwarding proxy. Cold start creates the client pool and opens the WebSocket connection, on which the server spawns a worker for the session, then checks out a session and runs1 + 1; the median of 7 runs is 3.5 ms. The client-pool row is the median of 20 furthercheckout()+feed_run()round trips on that pool, at 1.6 ms; each is a new connection and a new worker, because the server never lets one process serve two clients. The worker spawns inside the Linux container, where Monty’s own cold start measures 2.4 ms against 4.5 ms on macOS, so the Full Monty rows are not directly comparable with the macOS rows above. The agent run is tenfeed_runcalls on one checkout, each a WebSocket round trip to the same worker. - WASI / wasmtime: the CPython 3.14.7 WASI build (
python.wasmplus itslib/directory, preopened as/withPYTHONHOME=/) run in-process through thewasmtime48.0.0 Python package. The module is compiled once to a.cwasmfile ahead of time, as a deployment would; the timed cold start deserialises it (about 1.5 ms), instantiates, and runspython -c 'print(1 + 1)', which is dominated by CPython’s own startup inside the module. Compiling from wasmtime’s cache instead costs about 95 ms, and from scratch about 340 ms. The agent run deserialises once and creates oneStoreper command, replaying the earlier commands; deserialising a new module while the previous store is still alive would add about 200 ms of page faults per command. - Docker: Docker Desktop 29.6.2 with the
python:3.14-alpineimage already pulled. Cold start isdocker run --rm python:3.14-alpine python -c 'print(1 + 1)'. The agent run keeps one container alive (docker run -d --rm python:3.14-alpine sleep infinity) and executes each replayed program withdocker exec <container> python -c ..., so it pays fordocker execand a CPython start per command but not for a container start. - Sandboxing service: Daytona through the
daytona0.207.0 SDK, sandboxes in Daytona’s EU region, called from London. Cold start isDaytona().create()followed bysandbox.process.code_run("print(1 + 1)"). The agent run creates a sandbox, warms it with one call, then makes tencode_runcalls with the replayed programs, so each command is one HTTPS round trip plus a CPython start on the sandbox; the sandbox is deleted afterwards. Daytona advertises sub-90 ms sandbox creation; the 1.5 s measured here includes the network round trips from London. - Pyodide:
mcp-run-python0.0.22, which starts a Deno 2.5.5 process running Pyodide 0.28.2 and exposes it as an MCP server over stdio. Cold start iscode_sandbox(), which spawns Deno and loads Pyodide, followed by oneeval; installing a package such asnumpyat start adds about 200 ms more. The agent run reuses a started sandbox and makes tenevalcalls with the replayed programs; each call is an MCP round trip into the already-loaded Pyodide, which keeps no globals between calls. - starlark-rust:
starlark-pyo32026.1.1, in-process; the 1.3 ms is the firstparse+evalafter import, later evaluations take about 0.01 ms. It has no agent-run row because the commands are Python, not Starlark. - YOLO Python:
eval("1 + 1")in the measuring process (about 0.1 ms) andpython -c 'print(1 + 1)'as a subprocess (about 30 ms). Replaying the agent run through ten subprocesses takes about 180 ms; tenexeccalls into one namespace take 0.3 ms.
- Language completeness: no class inheritance, limited stdlib, no third-party libraries. See the Python subset.
- Security: explicitly controlled filesystem, network and environment access; limits on execution time and memory usage, off unless you set them. See the security model.
- Start latency: a warm checkout is one message to a worker that already exists; a cold start spawns the worker.
- Setup complexity:
pip install pydantic-montyornpm install @pydantic/monty, about 4.5 MB download. - File mounting: strictly controlled, see filesystem access.
- Snapshotting:
feed_start()pauses at a host call anddump()serialises the interpreter, paused call stack included, to a few kilobytes; restore it once to resume, or several times to fork. See snapshots.
Full Monty is the commercial server: the same monty workers behind a WebSocket, as a container image.
- Language completeness: the same subset as Monty, or full CPython when the server proxies a session to a CPython sandbox.
- Security: the Monty sandbox plus OS-level isolation; escaping the sandbox reaches an empty container, not the machine running your application.
- Start latency: a WebSocket connection plus a worker spawn for the session, 4 ms measured over loopback inside a Linux container; a deployment adds its network round trip.
- FOSS: closed-source and commercial; the client,
AsyncMontyWebsocket, ships in the MITpydantic-montypackage. - Setup complexity: run the container image with one environment variable, the dump-signing key.
- File mounting: client directories are mounted over the wire, the same
MountDiras a local pool. - Snapshotting: as Monty, and a draining server hands each session a signed dump to restore elsewhere.
- Language completeness: full CPython with any library.
- Security: process and filesystem isolation, network policies, but container escapes exist; memory limitation is possible.
- Start latency: container startup overhead, 195 ms measured.
- Setup complexity: requires the Docker daemon, container images and orchestration;
python:3.14-alpineis 50 MB and Docker cannot be installed from PyPI. - File mounting: volume mounts work well.
- Snapshotting: not of a running process, except experimentally with CRIU on Linux; committing a container to an image saves its filesystem, not its execution state.
- Language completeness: full CPython compiled to WASM, almost all libraries available.
- Security: relies on the browser/WASM sandbox and is not designed for server-side isolation; Python code can run arbitrary code in the JS runtime; only Deno allows isolation, and memory limits are hard or impossible to enforce with Deno.
- Start latency: loading the WASM runtime is slow, 2700 ms cold start measured.
- Setup complexity: load the WASM runtime and handle async initialisation; the Pyodide npm package is about 12 MB and Deno about 50 MB, so Pyodide cannot be used with PyPI packages alone.
- File mounting: virtual filesystem via browser APIs.
- Snapshotting: no; a running Pyodide heap has no serialised form.
See starlark-rust.
- Language completeness: a configuration language, not Python; no classes, exceptions or async.
- Security: deterministic and hermetic by design.
- Start latency: runs embedded in the process; 1.3 ms for the first evaluation, around 0.01 ms after that.
- Setup complexity: usable from Python via starlark-pyo3.
- File mounting: no file handling by design, as far as we know.
- Snapshotting: no.
CPython compiled to WebAssembly (WASI), run by wasmtime.
- Language completeness: almost full CPython; pure-Python packages work from a mounted directory, packages with C
extensions need their own WASI build.
In the WASI build
socket.socket()andsubprocess.run()raiseOSError,threading.Thread.start()raisesRuntimeError, andctypesdoes not import. - Security: the WebAssembly sandbox plus WASI’s capability model; the guest sees only the directories and environment variables you preopen.
- Start latency: 16 ms with the module precompiled to a
.cwasmfile ahead of time, as a deployment would; about 95 ms when wasmtime compiles from its cache and about 340 ms compiling from scratch. Measured in-process through thewasmtimePython package with the CPython 3.14.7 WASI build. - Setup complexity:
pip install wasmtimeplus a CPython WASI build, a 13 MB download that unpacks to about 54 MB with the standard library; you manage the module, its precompilation and the stdlib directory yourself. - File mounting: preopened directories.
- Snapshotting: no; a paused interpreter cannot be serialised, and pre-initialisation tools like Wizer only snapshot a module before it starts running.
Services like Daytona, E2B and Modal. Running your own sandbox setup on Kubernetes has similar characteristics, with more setup complexity but lower network latency.
- Language completeness: full CPython with any library.
- Security: professionally managed container isolation.
- Start latency: a network round trip plus container startup. We measured 1.5 s to create a sandbox and run one line with Daytona EU from London, and about 40 ms per call to an existing sandbox; Daytona advertises sub-90 ms latency, presumably for the latter.
- FOSS: pay per execution or compute time; some implementations are open source.
- Setup complexity: API integration and auth tokens; fine for startups but often a non-starter for enterprises.
- File mounting: upload and download via API calls.
- Snapshotting: a microVM’s whole memory can be paused and saved, which E2B and Modal offer as pause and resume; it is hundreds of megabytes, takes hundreds of milliseconds or more, is tied to the host’s CPU and kernel, and the host cannot see what the paused code was waiting for.
Running Python directly via exec() (about 0.1 ms) or a subprocess (about 30 ms).
- Language completeness: full CPython with any library.
- Security: none; full filesystem, network, environment variable and system command access.
- Start latency: near zero for
exec(), about 30 ms for a subprocess. - Setup complexity: none.
- File mounting: direct filesystem access, which is the problem.
- Snapshotting: no;
picklecan save the globals between blocks, which is a session dump, not a paused frame.