Engineering

Sandbox the Code, Not the Database

An AI agent needs its own database to test against. You can shrink the database to fit the agent, or leave the database alone and shrink the agent instead. Only one of those still answers questions about the table that does not fit.

Everyone has arrived at the same conclusion in the last year: an AI agent needs its own database to test against. You cannot let an agent try an index on production, and you cannot let it learn by breaking things. The interesting question is what “its own database” actually means, because there are two ways to give an agent one and they produce very different answers.

The first way is to make the database small enough to travel. Compile Postgres to WebAssembly, hand the agent an instance that starts in milliseconds inside its own sandbox, and let it work. PGlite does this well. It is real Postgres, not an imitation. It went from one million to thirteen million weekly downloads in a year, and for a great many jobs it is exactly right. We used it.

We moved off it, and the reason was mundane rather than damning. PGlite runs single-user with one exclusive connection, and one database instance at a time. We wanted several at once. We tried splitting our schemas across separate databases to work around that, and the list of databases we needed grew past the point where the workaround was worth it. If you want to see other people meeting the same wall, issue 324 on the PGlite repository where a group of engineers discover that their parallel tests run serially. That is a real constraint, honestly documented in a project that is upfront about being alpha.

Then we thought about it differently, and the second way turned out to be better for what we needed. Leave the database alone. Put the sandbox around the agent’s code instead.

That inversion matters more than it sounds, and here is why. The point of an experiment is to find out what will happen in production. Everything you change about the database moves the answer away from the truth you are trying to measure. The sandbox has to go somewhere, so it should go around the part where being constrained costs you nothing. Constraining the agent’s code costs nothing. Constraining the database costs you the entire result.

The statistics are the experiment

Matching the major version is table stakes. The part people underestimate is the statistics.

Postgres does not choose a query plan by reading your SQL and applying rules. It chooses by looking at what it knows about your data: how many rows are in the table, how values are distributed within a column, how selective a filter is likely to be. Change those numbers and the planner makes a different decision, correctly, because a different decision is right for different data. This is the same reason a query that flies in staging crawls in production.

So a clone of the right major version, with no statistics in it, is not a small version of your database. It is a different database that happens to speak the same dialect. An experiment against it runs perfectly, returns a confident answer, and tells the agent something that is not true about your production system. That is worse than telling the agent nothing, because nothing is obviously nothing, and a wrong answer arrives dressed as a right one.

DBGorilla’s clones carry the statistics from your production database. That is what makes the planner in the experiment behave like the planner you actually run, and it is the reason an answer from one of these experiments is worth anything at all.

What actually runs

When an agent wants to try something, DBGorilla starts a one-shot job running real Postgres. The engine and the major version come from what we already know about your database, not from what the agent asked for, so an agent cannot accidentally test against Postgres 17 semantics when you run 15. The clone carries your statistics.

The agent’s code runs somewhere else entirely. It writes a Python script, and that script executes under CPython in a WebAssembly sandbox with the standard library and nothing else. No network. No filesystem. No packages it can reach for. We mount exactly one capability into that sandbox: a function that runs SQL against the clone and hands back rows.

One capability is the whole design. The agent can ask the database anything it likes and can do nothing else at all. It cannot call out, it cannot read a file, it cannot touch your production system, and it does not need to, because the only thing it came here to do is find out what a query does against your data. When the job fails for an infrastructure reason it returns a clean failure rather than hanging, because an experiment that never finishes is a worse outcome than one that says it did not work.

Why this is the right shape

Both approaches accept the same premise, that an agent needs somewhere safe to try things. They disagree about what to shrink.

Shrinking the database is the obvious move, and it buys you speed and portability that are genuinely useful for a lot of work. There is a ceiling built into it. Moving the data to the agent works as far as the data fits in a process, and the questions that decide whether a change is safe are usually about the table that does not fit. What does the planner do on the fifty-million-row table? Which index does it pick at production cardinality? Does this migration take a lock that matters? Those are questions about data you cannot move, and statistics are how you answer them without moving it.

Shrinking the agent instead costs almost nothing. An agent running an experiment does not need the network or the filesystem. Take those away and it can still do the whole job. Take away the statistics and it cannot do the job at all, no matter how good the rest of the setup is.

Give the agent a real database, and give it a very small room to stand in while it works.

Start free