Skip to content

Choosing a database

Minion has two independent choices, and they’re often confused:

  1. Where your agent writes traces — a local file, or over HTTP to a server.
  2. What that server stores them in — SQLite or Postgres.

Choice 1 is set in init(); choice 2 is set on the server with DATABASE_URL. Your agent code never knows or cares which database is behind the server.

You areUse
One developer, one laptopLocal SQLite. init(tracing=True, project="…"), then minion serve. Nothing to deploy.
A team, or agents on several machinesA shared server. Agents push over HTTP with trace_url.
A shared server, small volumeServer on SQLite (the default). One container, one volume.
A shared server, real volume or concurrent writersServer on Postgres.

Most projects start at the top row and never move. Don’t deploy a server until more than one machine needs to write traces.

minions.init(tracing=True, project="my-project")

Writes to ~/.minion/traces.db. Read it with minion serve. No container, no network, no credentials, and nothing leaves the machine.

Override the path when you want traces beside a project rather than in your home directory:

minions.init(tracing=True, project="my-project", db_path="./traces.db")
Terminal window
minion serve --db-path ./traces.db

The default for the container. One service, one volume — traces live at /root/.minion inside the container.

Good for a small team writing traces at a human pace. SQLite takes a database- level write lock, so concurrent pushes serialise. In practice that’s invisible until you have many agents writing at once.

Set DATABASE_URL and the same image switches:

Terminal window
DATABASE_URL=postgresql://user:pass@host:5432/minion

Reach for it when:

  • Several agents write concurrently. Postgres handles concurrent writers; SQLite serialises them.
  • You want managed backups and point-in-time restore. RDS, Supabase and Neon give you this; a Docker volume doesn’t.
  • You filter on metadata a lot. On Postgres, metadata is stored as JSONB with a GIN index and a multi-key filter is one indexed containment check. On SQLite it’s a json_extract comparison per key, with no index behind it.
  • Trace volume is large. Both dialects have indexes on runs and use keyset pagination, but Postgres has the better story as the table grows.

A bare postgresql:// URL is routed through psycopg 3 automatically — paste the connection string your provider gives you as-is.

There is no migration path between SQLite and Postgres. Point the server at a new DATABASE_URL and it starts with an empty database; the old traces stay in the old one.

That’s usually fine — traces are operational data, most valuable while recent. If it isn’t fine for you, plan to start on Postgres. What you don’t have to plan for is the schema: Alembic migrations run automatically at startup on both dialects, and they’re additive, so upgrading the image or the package never loses data.

There is no automatic retention or TTL. Traces accumulate until you delete them, which you can do from the dashboard by filter, by selection, or by project — see The dashboard. Deletes cascade to a run’s turns, tool calls and immediate sub-runs.

For a long-lived Postgres deployment, a scheduled DELETE FROM runs WHERE created_at < … is the pragmatic answer today.