# How to run SQL queries in a sandbox (SQLite and PostgreSQL) Run the SQL against a throwaway database inside a Linux microVM: SQLite with the `sqlite3` shell, or PostgreSQL installed from apt. **On Runtime SQLite is already in every sandbox, and a full PostgreSQL server is one apt install away.** Each sandbox is a Firecracker microVM running Ubuntu 24.04.5 with `sqlite3` in the image and passwordless `sudo`, so a query a model wrote can run against a copy of the data, never your production database. A new sandbox took 351 ms from the create request to its first Python result at the median, measured on 24 September 2026 ([speed](/docs/speed)). ## SQLite or PostgreSQL? | Question | SQLite | PostgreSQL | | --------------------------------- | --------------------------------------- | -------------------------------------------- | | In the default image? | Yes, the `sqlite3` shell | No: `sudo apt-get install -y postgresql` | | Ubuntu 24.04 package, 25 Sep 2026 | `sqlite3` 3.45.1 | `postgresql` 16 (`postgresql-16` 16.15) | | Runs as | A command on a file | A server on port 5432 | | Also reachable from | Python's `sqlite3` module, in the image | `psql`, any driver, on `localhost` | | Best for | One-off queries, grading, CSV analysis | Testing SQL that production runs on Postgres | ## Run a query on SQLite Pass the SQL on `stdin` and name the database as an argument. With `-bail` the shell stops at the first error; `-json` prints rows as JSON for your code to parse ([sqlite3 command-line shell](https://sqlite.org/cli.html)). ```ts import { Sandbox } from "withruntime"; const setup = `CREATE TABLE orders(id INTEGER PRIMARY KEY, customer TEXT, total REAL); INSERT INTO orders(customer, total) VALUES ('ada', 30.5), ('bo', 12), ('ada', 7.25);`; const query = "SELECT customer, SUM(total) AS spent FROM orders GROUP BY customer ORDER BY spent DESC;"; await using sbx = await Sandbox.create({ network: { internet: false }, timeoutSeconds: 300, onLeaseEnd: "stop", }); await sbx.exec(["sqlite3", "-bail", "shop.db"], { stdin: setup, check: true }); const rows = await sbx.exec(["sqlite3", "-bail", "-safe", "-json", "-readonly", "shop.db"], { stdin: query, timeoutMs: 30_000, }); console.log(rows.exitCode, rows.stdout); // [{"customer":"ada","spent":37.75}, ...] ``` ```python from withruntime import Sandbox setup = """CREATE TABLE orders(id INTEGER PRIMARY KEY, customer TEXT, total REAL); INSERT INTO orders(customer, total) VALUES ('ada', 30.5), ('bo', 12), ('ada', 7.25);""" query = "SELECT customer, SUM(total) AS spent FROM orders GROUP BY customer ORDER BY spent DESC;" with Sandbox.create(network={"internet": False}, timeout_seconds=300, on_lease_end="stop") as sbx: sbx.exec(["sqlite3", "-bail", "shop.db"], stdin=setup, check=True) rows = sbx.exec(["sqlite3", "-bail", "-safe", "-json", "-readonly", "shop.db"], stdin=query, timeout_ms=30_000) print(rows.exit_code, rows.stdout) ``` Three flags make the shell safer for SQL you did not write: - `-readonly` opens the database read-only. - `-safe` turns off every shell feature that reads or writes a file other than the database: `ATTACH`, `readfile()`, `writefile()`, `load_extension()`, `.shell`, `.system`, `.import`, `.output` and more. - `timeoutMs` ends a runaway query from outside; the result says `timedOut: true`. To query your own data, upload the database file with `sbx.files.write` or a folder with `sbx.files.upload`, and download the changed file after. ## Run PostgreSQL in the sandbox Ubuntu's `postgresql` package creates a cluster named `16/main`. Start it with `pg_ctlcluster`, which needs no service manager, and create a role for the sandbox user so `psql` connects over the local socket without a password. ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ timeoutSeconds: 1800, onLeaseEnd: "stop" }); const slow = { check: true, timeoutMs: 600_000 } as const; await sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q postgresql", slow); await sbx.exec("sudo pg_ctlcluster 16 main status || sudo pg_ctlcluster 16 main start", slow); await sbx.exec( "sudo -u postgres createuser --superuser runtime && createdb -O runtime runtime", slow, ); await sbx.network.set({ internet: false }); await sbx.files.write( "/workspace/check.sql", "CREATE TABLE t(n int); INSERT INTO t SELECT generate_series(1, 1000); SELECT count(*), sum(n) FROM t;", ); const run = await sbx.exec(["psql", "-v", "ON_ERROR_STOP=1", "-At", "-f", "check.sql"], { timeoutMs: 60_000, }); console.log(run.exitCode, run.stdout); // 1000|500500 ``` ```python check from withruntime import Sandbox with Sandbox.create(timeout_seconds=1800, on_lease_end="stop") as sbx: sbx.exec("sudo apt-get update -q && sudo apt-get install -y -q postgresql", check=True, timeout_ms=600_000) sbx.exec("sudo pg_ctlcluster 16 main status || sudo pg_ctlcluster 16 main start", check=True) sbx.exec("sudo -u postgres createuser --superuser runtime && createdb -O runtime runtime", check=True) sbx.network.set(internet=False) run = sbx.exec(["psql", "-v", "ON_ERROR_STOP=1", "-At", "-c", "SELECT version();"], timeout_ms=60_000) print(run.exit_code, run.stdout) ``` `ON_ERROR_STOP=1` makes `psql` stop at the first failing statement and exit non-zero, so a broken migration fails the command. The network rule refuses connections out of the sandbox; `localhost` inside it keeps working. ## Start every sandbox with Postgres running An image can install PostgreSQL and say how a sandbox starts it. The create call answers once port 5432 is listening ([start and ready commands](/docs/images#start-and-ready-commands)): ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await runtime.images.build({ name: "postgres", recipe: { apt: ["postgresql"] }, start: { command: "sudo pg_ctlcluster 16 main start && sleep infinity", readyPort: 5432, readyTimeoutSeconds: 60, }, }); await using sbx = await runtime.sandboxes.create({ image: "postgres", network: { internet: false }, }); console.log(sbx.info.start); // { state: "ready", ... } await sbx.exec("sudo -u postgres psql -c 'SELECT 1'", { check: true }); ``` `pg_ctlcluster` returns once the server is up, and a start command that ends counts as `exited`, so `sleep infinity` keeps it running. For many test runs against the same seeded data, load it once, then [fork](/glossary/sandbox-fork) the running sandbox: each copy starts with the server up and the data in memory, and nothing one copy writes reaches another. ## What it costs A database that sits idle between queries costs the CPU floor, a twentieth of a vCPU at $0.025 per vCPU-hour, plus memory at $0.0075 per GiB-hour. A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour waiting and $0.08 an hour fully busy ([pricing](/docs/pricing)). New accounts get 50 free sandbox hours, no card: ```bash no-run npx withruntime sandbox run --trial -- sqlite3 :memory: 'select sqlite_version();' ``` See also [a data analysis agent](/use-cases/data-analysis-agent), [agent evals](/use-cases/agent-evals-and-swe-bench) and [running untrusted LLM code](/use-cases/run-untrusted-llm-code). ## Sources Checked 25 September 2026. - [SQLite command-line shell](https://sqlite.org/cli.html): `-bail`, `-json`, `-readonly`, `-safe` - Ubuntu 24.04 (noble) packages: [sqlite3](https://packages.ubuntu.com/noble/sqlite3), [postgresql](https://packages.ubuntu.com/noble/postgresql), [postgresql-16](https://packages.ubuntu.com/noble/postgresql-16), [postgresql-common file list](https://packages.ubuntu.com/noble/all/postgresql-common/filelist) (`pg_ctlcluster`) Facts on this page were checked on 25 September 2026.