Skip to content
Markdown

The sandbox environment

Every sandbox is a Firecracker microVM. It has its own Linux kernel and disk. Python, Node.js, Bun, git and a compiler are ready to use.

This page covers what is in the image, who you are inside it, and what it can reach. Anything missing installs in one command. Or you can install it once, for every sandbox, in a custom image.

Updated

On this page

What is installed#

The image is Ubuntu 24.04.5 LTS (noble) for amd64, on a 6.18 kernel.

Kind What
Python Python 3.12 (python3 and python), pip and pip3, venv, and uv 0.12.17 (uv, uvx)
Python data tools NumPy 1.26.4, pandas 2.1.4 and matplotlib 3.6.3, included in the current default image
JavaScript Node.js 24.21.0 (node, npm, npx), with pnpm and yarn through Corepack; Bun 1.4.0 (bun)
Build gcc, g++, make (build-essential)
Source and fetch git, curl, wget, ssh, zip, unzip, xz
Search and data rg (ripgrep), fd, jq, sqlite3
System sudo, adduser and useradd, chronyd, sshd (for SSH and editors), ip, iptables-legacy
Agent sandboxing bwrap (bubblewrap). Codex runs its commands under it. It is in the default image from its next build

Anything else installs the usual way: sudo apt-get install -y ..., pip install ..., npm install ..., or uv pip install .... A new sandbox has no apt package lists yet. Run sudo apt-get update once before the first apt-get install. To start every sandbox with your dependencies already there, build a custom image.

  • bubblewrap: until the default image includes bwrap, run sudo apt-get update && sudo apt-get install -y bubblewrap to add it. Codex, and other agents that sandbox their own commands with it, then work as on any Linux machine.
  • Java, Go and Rust: install Ubuntu's packages with sudo apt-get install. For a newer release, use the language's own installer, such as rustup for Rust. To have one in every sandbox, build a custom image whose recipe installs it. Or start the image from a public image that already has it.
  • Docker: sudo enable-docker installs Docker Engine, Buildx and Compose. The first docker command starts it for you. See Docker below.
  • A Dockerfile: run it with Docker in a sandbox. Or build it as a custom image and start sandboxes from that image. Multi-stage builds, COPY --from, heredocs and .dockerignore work as in Docker.
  • Browsers: they are not in the image, so its disk stays small. Playwright's Chromium installs in two commands:
Terminalsudo npx playwright install-deps chromiumnpx playwright install chromium

Who you are#

  • Commands run as the user runtime (uid 1000), in bash. USER and LOGNAME say so.
  • HOME is /workspace, which is also the default working directory. Files there belong to you.
  • sudo works without a password. You are root inside your own sandbox. To run one command as root, without typing sudo, give it user: "root". This works for exec, background processes and terminals. In the CLI, use --root. To run every command as root, create the sandbox with user: "root" (--user root in the CLI). Or set it later with :update. Its commands, background processes and terminals then run as root. The exception is a command that names user: "sandbox". A fork, or a copy of its snapshot, also runs as root. Files written through the files API and SSH logins stay runtime's. Root cannot change what the sandbox may reach. Root cannot change how much CPU and memory it has, or what it costs. The host enforces those, outside the sandbox.
  • PATH starts with /workspace/.local/bin, then /usr/local/bin, /usr/bin and /bin. LANG is C.UTF-8.
  • Variables you set with env at create, or change with :update, are in every command. They are also in every background process, terminal, SSH session and the image's start command. They stay for the sandbox's whole life, under each command's own env (API). Their values are never shown back. :update works on a paused or stopped sandbox too. A change reaches commands and processes started after it. A program already running keeps the values it started with, even through a pause and wake. Restart it to pick up a change.

pip install works without a virtual environment. As runtime, it installs to /workspace/.local, whose bin is first on PATH. Under sudo, it installs to /usr/local. The image's /etc/pip.conf sets break-system-packages. So the Ubuntu rule against installing into the system Python (PEP 668) does not stop you. Use python3 -m venv or uv when you want isolation.

npm install -g and bun add -g put their commands in /workspace/.local/bin too. So a tool such as claude or codex runs by name as soon as it is installed. With npm, sudo is optional. pnpm and yarn fetch the version that a project's packageManager names. They do this the first time they run.

The machine's identity#

A sandbox is not booted when it starts. It is a copy of a machine that booted once. Most of what a machine calls its own identity is made new for each copy. Not all of it is.

  • /etc/machine-id belongs to the sandbox. It is made when the sandbox first starts, and kept for its whole life. That includes pauses, wakes, stops and restarts. A fork, or a sandbox created from a snapshot, gets a new one. Use it when a program needs an ID for the machine it runs on.
  • /dev/urandom, getrandom and every program started in the sandbox get fresh randomness. Programs already running when a fork or snapshot was taken keep their own state. See randomness in copies.
  • The kernel's boot ID (/proc/sys/kernel/random/boot_id) and boot time (uptime, btime in /proc/stat) belong to the machine that booted. So sandboxes made from the same image can share them. The kernel cannot change them without a reboot. restart gives a sandbox its own. Do not use the boot ID to tell sandboxes apart.
  • The hostname is runtime-guest in every sandbox.

Disk, CPU and memory#

  • Disk: diskMiB includes the system image and installed packages. In a measurement on 24 September 2026, the current default 4 GiB sandbox had about 2.5 GiB available. If you install a lot, request a larger disk. The smallest disk is 3072 MiB, the size of the image file.
  • Disk speed: a sandbox shares its server's drives with the sandboxes beside it, in proportion. It has no fixed ceiling of its own. It gets at least about 40 MB/s each way. It gets up to about 1 GB/s when its neighbors are quiet. One busy sandbox cannot starve the others. A pause writes its memory ahead of everyone's traffic. So it stays quick on a busy server.
  • CPU: shared by default. It has a guaranteed floor (cpuFloorMillis, 50 thousandths of a vCPU unless you ask for more). It can burst up to vcpu cores. cpu: "reserved" sets that same floor on every vCPU (vcpu x 1000). You pay for the CPU the sandbox uses. A paid sandbox has up to 16 vCPUs. Without credit, it has up to 2.
  • Processor: AMD EPYC (x86-64, Zen). AVX, AVX2 and FMA are guaranteed on every server. Code that requires them, such as MuJoCo, can count on them. A paused sandbox or a snapshot wakes only on a server that shows it the same processor features. So a running process never loses one. AVX-512 is not offered.
  • Memory: what you ask for, up to 64 GiB on a paid sandbox and 4 GiB without credit.

When a program runs out of memory

A sandbox that runs out of memory stays up. Linux's out-of-memory killer ends the program that uses the most memory. The rest keeps running. Your programs share the sandbox's memory, minus a reserve for its own services. The reserve is a sixteenth of the memory, at least 128 MiB and at most 256 MiB. So while programs fill the memory, commands, file reads and kills still get a response. A program ended this way exits with -9 (137 from a shell). It often prints nothing. Runtime says why. The command's result, its process (read, get or list) and the stream's exit event carry outOfMemory:

JSON{  "exitCode": -9,  "outOfMemory": {    "code": "out_of_memory",    "processesKilled": 1,    "memoryMiB": 512,    "message": "The sandbox ran out of its 512 MiB of memory, and Linux's out-of-memory killer ended 1 process of this command.",    "hint": "Give the program more memory: create the sandbox with memoryMiB 1024 or more, or resize this one while it is paused (POST /v1/sandboxes/{id}:resize with memoryMiB). Or make the program use less."  }}

processesKilled counts the program and anything it started. So a shell whose child was killed says so too. A result without outOfMemory was not ended for memory. Each such command also sends the account event sandbox.out_of_memory (observability). Your backend hears of it even after the sandbox is gone. With check: true (check=True in Python), the call raises CommandError with the code out_of_memory and the hint. The CLI prints the message and the hint after the command's output. runtime sandboxes ps marks the process "(out of memory)".

See pricing for what each costs.

The network#

A sandbox has no network card. All outbound traffic goes through a proxy on the host. The image sets the environment variables that every tool needs to find the proxy:

textHTTP_PROXY=http://127.0.0.1:10800   http_proxy=http://127.0.0.1:10800HTTPS_PROXY=http://127.0.0.1:10800  https_proxy=http://127.0.0.1:10800NO_PROXY=localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16NODE_USE_ENV_PROXY=1

The private ranges in NO_PROXY are your own Docker containers. They are reached directly.

sudo keeps these settings. Programs that ignore proxy settings also work. Names resolve inside the sandbox. Their TCP connections go to the same proxy on their own. In a paid sandbox, git clone git@github.com:..., psql, database drivers, gRPC and ssh need no setup. A sandbox without credit reaches ports 443 and 80 only. So there, clone over HTTPS (git clone https://github.com/...).

A paid sandbox reaches any public host on any port, once your account has made a purchase. A sandbox without credit reaches ports 443 and 80. For both, private and internal addresses are refused. Mail ports (25, 465 and 587) are closed unless support enables them for your account. A few ports are never reachable: telnet, Windows RPC, NetBIOS and SMB, and IRC. TCP leaves the sandbox. A paid sandbox also sends UDP to any public address and port (outbound UDP). DNS is resolved inside the sandbox.

Bandwidth. Downloads are fast for everyone. Uploads are limited, and more strictly without credit, because spam and floods use uploads. Downloads have no speed limit. Each server's link is shared between the sandboxes using it. While the link is full, a paid sandbox gets four times a sandbox without credit's share. A sandbox paid for with credit uploads at up to 500 Mbit/s. After its first 10 GiB at that speed, it uploads at 200 Mbit/s. It earns the burst back at that rate while it sends less. A sandbox without credit uploads at up to 10 Mbit/s. A paid sandbox (any account with credit, bought or given) can move 500 GiB a day, in and out together. The window is 24 hours, and it starts with the sandbox's first byte. An account without credit can move 1 GiB a day. That amount is shared by all its sandboxes. Each sandbox's bytes count until its own window ends, even once it is deleted. Connections to your own sandboxes by name (<name>.sandbox.internal) are not uploads: they count toward neither the upload limits nor this daily amount (networking). Past the daily amount, open connections close. New requests get 429 Too Many Requests with X-Runtime-Egress: quota-exhausted. For the shared amount of an account without credit, the header is quota-exhausted:account. This lasts until the window ends. Credit added to an account without credit moves its running sandboxes to the paid amount. What they have used is kept. Inbound traffic is free. Each account's first 100 GiB out a month is free. After that, outbound traffic costs $0.02 per GB (10⁹ bytes) (pricing).

Connections. A sandbox of an account that keeps a top-up holds up to 512 outbound connections open at the same time for each vCPU, up to 4,096. Any other sandbox holds 64. Every sandbox opens up to 20 new connections a second. It can open a first 128 at the same time (40 without a kept top-up). A sandbox starts reaching up to 120 different hosts and ports a minute. So an agent can hold hundreds of connections to the APIs it uses. A scan stays slow. Past a rate, a new connection waits up to 30 seconds for its turn. It does not fail. For example, 500 connections opened at the same time to one host all connect within about 20 seconds. Past a limit, a new connection gets 503 Service Unavailable with Retry-After: 1. The limit appears in X-Runtime-Egress, such as too many open connections (512 of 512) or too many new connections (20 a second). It is never a reset. First, though, a connection silent for a minute or more is closed to make room. A sandbox holds about 1,000 outbound connections at the same time, even when its limit is higher. Past that, and whenever the sandbox cannot reach Runtime's proxy, a new connection gets 503 Service Unavailable with X-Runtime-Egress: proxy unreachable (<reason>), such as proxy unreachable (connection reset by peer). Each limit a sandbox meets also sends the account event sandbox.network.limited, at most once an hour per limit (observability). GET /v1/sandboxes/{id}/network shows a sandbox's limits. Reuse connections (HTTP keep-alive pools) rather than opening one per request.

Each sandbox has its own rules. Set them at create (network), or at any time after. They apply immediately, to open connections too:

TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({  network: { internet: true, allow: ["pypi.org", "*.pythonhosted.org"] },});await sbx.network.set({ internet: true, deny: ["example.com"] });await sbx.network.set({ internet: false });console.log(await sbx.network.get());
  • internet: false refuses every outbound connection. With an allow list, it is allow-list only. The sandbox reaches the listed destinations and nothing else. It is stored and shown as internet: true with that list. This holds at create and on network.set alike.
  • allow narrows the sandbox to a list, on every port it may use. The list takes domains, *.domain for every name under one, addresses, or CIDR ranges.
  • deny always wins.
  • connect adds host:port pairs beyond the web ports, such as your own database or github.com:22. Use it when allow narrows a paid sandbox and the pair is not in the list.
  • sbx.network.get() says openPorts: true for a sandbox that reaches every port.
  • A sandbox without credit, on the free usage, reaches ports 80 and 443 only. A connect rule on it is refused with connect_not_allowed (403). The message says which limit it met. Once the account has a kept top-up, its paid sandboxes reach every port. Topping up is the account owner's decision.

To give code in a sandbox an API key it can use but never read, store it as a secret. See Security.

The rules apply to root inside the sandbox too.

Runtime's API from inside a sandbox

Code in a sandbox can call Runtime's API. So an agent in one sandbox can start, run and stop others. Inside a sandbox, the API is at http://runtime.internal. Every Runtime SDK (JavaScript, Python, Go, Java and Ruby) uses it on its own. Calls for https://api.withruntime.com go there. An API origin you set yourself is left as it is. Give the sandbox a key as usual, as a secret or in env:

TypeScriptimport { Runtime } from "withruntime";// Inside a sandbox, with RUNTIME_API_KEY set.const runtime = new Runtime();await using child = await runtime.sandboxes.create();console.log((await child.exec("echo hello from a child")).stdout);

Any HTTP client works the same way, through the proxy settings above:

Terminalcurl -H "Authorization: Bearer $RUNTIME_API_KEY" http://runtime.internal/v1/me
  • The sandbox's host sends each request on to https://api.withruntime.com over HTTPS. The hop from your code to the host is plain HTTP. It never crosses a network. It runs over the sandbox's own private channel to its host, which no other sandbox can reach.
  • It is the same API with the same key. Your account's limits, roles and rate limits apply. Errors carry the same request IDs.
  • Paths under /v1/ are served, and /mcp for Runtime's MCP server.
  • In a Docker container inside the sandbox, set RUNTIME_API_URL=http://runtime.internal.
  • A sandbox holds up to 32 API requests open at the same time. Past that, a request gets 429 with X-Runtime-Egress: api-busy. Retry it. API requests count toward the sandbox's new-connection rate, bandwidth and daily amount. They are never charged as outbound traffic.
  • The sandbox's own rules apply. With internet: false and no allow list, or with an allow list that leaves out api.withruntime.com, requests are refused. The header is X-Runtime-Egress: internet-off or rule-not-allowed.

Nothing on the internet can connect in to a sandbox. These are the ways in. You open each one yourself:

  • A preview: an HTTPS address for one port that you share on purpose. It is private, with a token, by default. Search engines never index it. See JavaScript or npx withruntime sandboxes preview.
  • A connection from your own computer. runtime sandboxes ssh and runtime sandboxes port-forward reach any port on the sandbox's loopback. They go through Runtime's API, with your key. They need nothing set up. See SSH and editors.
  • For an account with a kept top-up: a custom domain, a public TCP port, or a WireGuard tunnel from your own network into your sandboxes (runtime networks wireguard, the private network). See networking.

A sandbox with none of these accepts no connections from anyone but you. For a sandbox created from 5 October 2026, what it sends back through a preview, a custom domain or a TCP port counts as outbound traffic. What visitors send in is free (pricing).

To start a server that keeps responding, use spawn (runtime sandboxes spawn, sbx.spawn), not exec. Everything an exec starts ends when its command does. That includes nohup … &.

Docker#

Terminalsudo enable-docker          # once per sandbox; the first docker command runs it toodocker run --rm hello-worlddocker compose up -d
  • enable-docker installs Docker Engine, Buildx and Compose from Ubuntu's archive, and starts them. After that, docker works as the sandbox user, without sudo.
  • Docker starts on the first docker command after a boot. So a sandbox that runs no containers spends nothing on it.
  • Images and containers live on the sandbox's disk. For a sandbox that runs containers, set more disk than the default, such as diskMiB: 8192.
  • Containers reach the web with no proxy settings. Their outbound connections go through the sandbox's relay. This is the same path as a program in the sandbox that ignores HTTP_PROXY. Docker Hub images come through Google's public mirror, mirror.gcr.io, first.
  • Compose services, and containers on one Docker network, reach each other by name, as anywhere.
  • A published port (-p 8080:80) listens inside the sandbox. runtime sandboxes port-forward <id> 8080 brings it to your machine. A preview shares it.
  • To start every sandbox with Docker installed, build a custom image whose recipe runs it: commands: ["enable-docker --no-start"].

FUSE filesystems#

FUSE is built into the kernel, and /dev/fuse is in every sandbox. So sshfs, rclone, bindfs, s3fs, gcsfuse, squashfuse or your own FUSE program can mount as the sandbox user. sudo is not needed:

Terminalsudo apt-get update && sudo apt-get install -y fuse3 sshfsmkdir -p ~/remote && sshfs me@example.com:/srv ~/remotefusermount3 -u ~/remote
  • allow_other lets other users, such as root or a container, see the mount. It is refused until you add user_allow_other to /etc/fuse.conf. Or mount with sudo.
  • A bucket: for S3, R2 or Google Cloud Storage, use Mount your own bucket. It keeps the key out of the sandbox.
  • Lifetime: a mount is a process in the sandbox. It lasts through a pause and a wake, and ends when the sandbox stops.

Time and lifetime#

  • The clock follows the host's clock. It is set again after every wake.
  • A sandbox runs while it works. It pauses itself when idle (idlePauseSeconds). It runs until you stop it, or until credit or the free usage runs out. Then it pauses or stops, as onTimeout says. It has no time limit unless timeoutSeconds sets one (60 to 86,400). extend moves that limit. A pause keeps its memory and processes.
  • A stop keeps the disk. It is billed as paused storage, until you delete the sandbox. restart starts it again, with its memory gone. A stop is still not a backup. Copy out what you need to keep.
  • A deleted sandbox is gone. DELETE /v1/sandboxes/{id}, sandbox.delete() or runtime sandboxes rm <id> stops it and deletes its disk and paused memory, in any state. Its snapshots stay.

Pause, stop or delete

Pause Stop Delete
Memory and running programs Kept Gone Gone
Files on its disk Kept Kept Gone
Comes back with wake: it carries on where it was restart: it boots fresh from its disk Nothing; create a new one
Happens on its own When idle, and by default when its time, credit or free usage runs out When its time, credit or free usage runs out, with onTimeout: "stop" The same, with onTimeout: "delete"
Billed Paused storage for its memory and disk Paused storage for its disk Nothing; its snapshots stay billed

Pause when you will come back to the same work and want its programs still running. Stop when only the files matter. Delete when you are done.

Timers wake an idle sandbox

A sandbox that pauses because it is idle wakes by itself when a process in it is due. At the pause, Runtime reads the sandbox's pending timers and records the soonest one: a sleep, a poll, select, epoll_wait or futex wait with a timeout, a timerfd, alarm or setitimer, or a POSIX timer (timer_create). The sandbox wakes about one second before that timer is due, as a request would wake it. So an agent that checks CI every ten minutes, or a loop that runs sleep 600, runs when it is due, and the sandbox costs nothing in between.

  • Only a timer that was pending for the whole idle time counts. A process that wakes every few seconds or every minute and does nothing new, such as a database's background writer or cron, did that while the sandbox was idle, and does not wake it again.
  • A sandbox with no pending timer stays paused until a request wakes it.
  • A timer wake is an ordinary wake. It is billed as one, and the sandbox pauses again after idlePauseSeconds with nothing happening.
  • Timers do not wake a sandbox that you paused, that paused at the end of its time limit, its credit or its free usage, or that has autoWake: false or wakeOnTimers: false. Timers of the init system (systemd timer units) and of Runtime's own services in the sandbox do not wake it either.
  • A wake that is refused, for example for lack of credit, is tried again every 30 seconds while the sandbox stays paused. A wake for an API key or agent that has since been revoked is not retried. A sandbox deleted at the end of its paused retention is not woken.
  • To keep request wakes and turn off timer wakes, set wakeOnTimers: false at create, or later with POST /v1/sandboxes/{id}:update. A fork keeps its source's setting. Turning it back on applies from the sandbox's next idle pause. In the SDKs this is wakeOnTimers: false (TypeScript), wake_on_timers=False (Python), wake_on_timers: false (Ruby), WakeOnTimers (Go) and wakeOnTimers(false) (Java). In the CLI it is --no-wake-on-timers at create, or runtime sandboxes update <id> --wake-on-timers off. Turning it off also cancels a timer wake that is already planned.
  • Only a timer due within 29 minutes of the pause wakes the sandbox, and only while the paused sandbox is kept in memory. After 30 minutes, or sooner when its server needs the memory, a paused sandbox is moved to disk, and its processes' clocks resume where they paused. A timer due later does not wake it, and it stays paused until a request wakes it.

Try it on your own work. Sign up, then paste the setup prompt into your coding agent.

Start free, no card

Was this page right?