Skip to content
Runtime

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. A query that a model wrote runs against a copy of the data, not your production database.

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, 26 Oct 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, so your code can parse them (sqlite3 command-line shell).

TypeScriptimport { 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}, ...]
Pythonfrom withruntime import Sandboxsetup = """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 query that runs too long. The result then shows timedOut: true.

To query your own data, upload the database file with sbx.files.write, or a folder with sbx.files.upload. Download the changed file afterward.

Run PostgreSQL in the sandbox

Ubuntu's postgresql package creates a cluster named 16/main. Start it with pg_ctlcluster, which does not need a service manager. Then create a role for the sandbox user, so psql connects over the local socket without a password.

TypeScriptimport { 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
Pythonfrom withruntime import Sandboxwith 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 define how a sandbox starts it. The create call returns once port 5432 is listening (start and ready commands):

TypeScriptimport { 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. A start command that ends counts as exited, so sleep infinity keeps it running.

For many test runs against the same seeded data, load the data once, then fork the running sandbox. Each copy starts with the server running and the data in memory. A write in one copy does not reach any other copy.

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 while it waits and $0.08 an hour when fully busy (pricing). A new account gets 100 free hours of a 1 GB machine in its first 30 days, with no card required:

Terminalnpx withruntime sandboxes run -- sqlite3 :memory: 'select sqlite_version();'

See also a data analysis agent, agent evals and running untrusted LLM code.

Sources

Checked 26 October 2026.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card