LLM Sandbox Goes Containerless: Bubblewrap and NsJail Backends

How LLM Sandbox runs each command in a fresh native Linux sandbox while keeping session files, plus controlled benchmarks against Docker and the tradeoffs that matter.

Hey mate! LLM Sandbox now has another way to run generated code: native Linux sandboxes through Bubblewrap and NsJail, using the same SandboxSession API.

The original introduction focused on Docker. The 2025 update expanded that approach to multiple container backends. These new adapters change how the execution environment is created: they use installed host runtimes inside isolated process trees, without a container image or container daemon.

For applications making lots of short code calls, that changes both deployment requirements and execution overhead. Let's look at the mechanism, a short example, and what the benchmarks actually tell us.

A persistent workspace, a fresh process every time

A native session owns a private workspace, temporary directory and home directory. When you open it, LLM Sandbox creates that storage, checks that the configured sandbox can launch, and performs language setup.

Each subsequent command gets a fresh isolated process tree. Source code is staged in the workspace and executed through Bubblewrap or NsJail. The session collects stdout, stderr and the exit code, then cleans up the command's processes.

The files stay available until you close the session. The Python interpreter does not.

That means one call can create a CSV and the next can analyze it. A variable assigned in the first call will not exist in the second. Installed packages in the session environment remain available, but background processes are not intended to survive between commands. Calls within one session are serialized.

Private session storage and a fresh isolated process tree for each commandThe application calls SandboxSession, which launches Bubblewrap or NsJail for each command. The fresh process tree sees selected host runtime files read-only and reads or writes private session storage. Results return to the application. Private workspace, virtual environment and temporary files persist until close. NativeSandbox A private session workspace, fresh processes per command app Your application session SandboxSession private session lifecycle app->session open / run launcher Bubblewrap / NsJail namespaces, mounts + seccomp session->launcher process Fresh isolated process tree new for each command launcher->process workspace Private session storage workspace + venv + temporary files process->workspace read / write output Results to your application stdout, stderr, exit code process->output runtime Selected host runtime files read-only runtime->process read-only mounts cleanup close() removes private session storage workspace->cleanup at session close
Default native execution: files persist for the session; each command gets a fresh process tree. Both backends share the host Linux kernel.

The workspace connects successive calls. There is no container image to prepare and no persistent Python kernel behind run(). The runtime files come from the host; the writable session data lives in private storage.

The API still looks familiar

The example below targets the native implementation on main, benchmarked at commit d79a0e2. Check that your installed version includes these adapters and that your host meets the backend prerequisites.

For Bubblewrap, you need a compatible Linux host, a recent bwrap, system libseccomp and Python with its venv components. Installing the Python package alone does not install those system dependencies.

from llm_sandbox import SandboxSession

with SandboxSession(backend="bubblewrap", lang="python") as session:
    session.run(
        "from pathlib import Path\n"
        "Path('answer.txt').write_text('42')"
    )

    result = session.run(
        "from pathlib import Path\n"
        "print(Path('answer.txt').read_text())"
    )
    print(result.stdout.strip())  # 42

    session.copy_from_runtime("answer.txt", "answer.txt")

The second call starts another Python process and reads the file left by the first. copy_from_runtime() retrieves the result before the context manager closes the session and removes its private storage.

Change backend="bubblewrap" to backend="nsjail" to use NsJail, provided its launcher and kernel requirements are satisfied. The native adapters do not need a Python container SDK. Docker remains the default when you omit the backend.

Normal Python setup creates a private virtual environment with system site packages visible. It works offline and does not download an interpreter. The host still supplies Python, libraries and other language toolchains.

What provides the isolation?

Several OS controls work together.

Namespaces and mounts give each command its process and filesystem view. Selected runtime directories are exposed read-only, while the workspace and private /tmp provide writable storage. Both backends share the host Linux kernel. “Containerless” describes their provisioning and lifecycle; it does not mean a VM boundary.

Network access is disabled by default. Choosing network="host" is an explicit, broad grant, including reachable local services. It is not a domain allowlist. The convenience package-installation APIs require that explicit grant; offline packages can instead be copied into the workspace and installed with an explicit command.

Seccomp is enabled by default. Bubblewrap receives a filter compiled through libseccomp; NsJail receives a Kafel policy. The default policy denies selected operations, including tracing another process, changing mounts and loading kernel modules. It is a denylist, not a complete syscall allowlist.

Execution controls include deadlines, output caps and optional per-process resource limits. Those limits do not replace aggregate cgroup quotas or external supervision for a hostile multi-tenant service.

There are two practical details worth keeping in mind. Read-only files are still readable, so runtime grants must not include secrets the payload should not see. And source-code screening through is_safe() remains a separate, advisory API; run() does not automatically call is_safe().

Missing prerequisites and unsupported policies produce errors. The adapters do not quietly retry execution as an ordinary host process. See the security documentation for the complete boundary and cleanup limitations.

Measuring the overhead

The expanded benchmark compared Bubblewrap, NsJail and Docker on one Linux x86-64 host with an AMD EPYC 7763, a four-CPU quota and a 16 GiB memory limit. Docker 28.4.0 used VFS storage.

All three used the same CPython 3.13.5 binary/build and standard library. The native backends mounted an extracted copy of that runtime read-only. Host and container OS libraries could still differ, and the backends do not provide identical security boundaries.

For the baseline, each backend completed 30 fresh sessions, with ten measured warm calls per session. Backend order was randomized within each round. Images were cached, networking was disabled, and language setup was skipped.

The primary estimate is the median of the session medians. Brackets show pointwise 95% bootstrap intervals, resampling sessions rather than treating every call as an independent experiment.

Backend Warm run(), ms [95% CI] Session open, ms [95% CI]
Bubblewrap 21.65 [21.55, 21.83] 10.64 [10.22, 10.94]
NsJail 21.14 [20.71, 21.31] 9.65 [9.46, 9.91]
Docker 114.73 [113.82, 116.46] 2,582.48 [2,535.31, 2,613.05]

On this host, Docker's warm-call estimate was about 5.3–5.4 times the native estimates. These timings include source staging, interpreter startup and backend transport. They do not mean Python computation itself became five times faster.

A broader task that generated, serialized, parsed, filtered and grouped 10,000 JSON records took about 70 ms through the native backends and 158 ms through Docker. Its payload CPU time was around 28–29 ms across all three. That is consistent with execution overhead mattering more for short calls; this experiment does not isolate the contribution of each implementation detail.

The suite also measured 1 MiB and 16 MiB transfers and payload CPU/peak RSS. Those resource measurements cover the Python process, not total daemon, launcher or controller costs. The full benchmark report includes intervals, raw evidence and reproduction instructions.

Warm marker and JSON API latency for Docker, Bubblewrap and NsJail, with 95% bootstrap intervals.
Median of session medians with pointwise 95% bootstrap intervals. Warm marker: 30 sessions per backend; JSON: 10. Cached runtimes; environment setup skipped. One host with a four-CPU quota.

Startup includes whatever you ask it to prepare

The roughly 10 ms native open figures above use skip_environment_setup=True. They do not describe the default example's full Python setup.

With a fresh offline virtual environment, open time was about 2.56 seconds for the native adapters and 5.88 seconds for Docker. This separate profile used ten fresh sessions per backend. Docker's usual pip-upgrade command ran with PIP_NO_INDEX=1; no package downloads were measured.

If your application needs a new environment for every request, include that preparation cost. If it makes several calls in one session, the environment and files can be reused while each command still receives a fresh process sandbox.

Docker close also took about 10.18 seconds with the tested image and default stop behavior. That is a separate lifecycle measurement. Pooling and alternative init/stop configurations were not tested, so it should not be treated as an unavoidable cost of every Docker deployment.

More workers help, up to the limits of this test

Concurrency tests used separate sessions at 1, 4 and 16 workers. Each worker made ten calls, submitting its next call after the previous one finished. Initialization and warmup happened before the shared timer; teardown waited until all measured execution completed.

For the JSON task, throughput at four workers was about 45–50 validated calls/second for the native backends and 21 for Docker. At sixteen workers, it was about 50–51 and 21.5 respectively.

Those estimates come from five independent batches per setting. Their intervals are exploratory, and the host had only a four-CPU quota. This is worker-count scaling on one shared host, not a production capacity limit or a fixed incoming request-rate test. The throughput window includes validation; individual API latency timers exclude it.

Validated calls per second at 1, 4 and 16 workers, comparing instrumented marker and JSON workloads.
Median throughput across five batches per setting with pointwise 95% bootstrap intervals. Exploratory results on one host with a four-CPU quota. The shared window includes validation; initialization and teardown are excluded.

Where containers still fit

Native backends are useful when you control the host toolchain and want local code execution without managing images or a daemon.

Containers remain useful when you need image-based environments, remote container services or Kubernetes orchestration. Docker also remains the default, and the container paths support capabilities such as pooling and interactive sessions that these native adapters do not.

There are experimental Windows options too: Windows MXC uses native PSEC process-container APIs through a helper, while WSL2 Bubblewrap runs the Linux mechanism inside an explicitly selected WSL2 distribution. Their prerequisites and restrictions differ. Neither was benchmarked on this Linux host, so there is no Windows performance claim here. Podman and Kubernetes were also unavailable on the test host and have no measured results in this comparison.

If your application already uses SandboxSession, try a native backend against the workload you actually run. Check its prerequisites and isolation policy, measure setup separately from repeated calls, and keep the backend that fits your deployment. The implementation is available in LLM Sandbox on GitHub; the benchmark documentation provides the evidence to compare it on your own host.

Subscribe to The Technician

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe