- Durability off. Crash recovery is abandoned by contract: if the machine dies, you delete the data directory and start over. In exchange, every commit returns without waiting on disk.
- Memory-first storage with graceful spill. Data lives in RAM as much as possible and spills to disk only under memory pressure — without the hard capacity cliff of a ramdisk.
- Ephemeral databases, managed by the server. Seed a template once, and every test worker gets a private clone just by connecting to a name like
tdb_myapp_tpl__w3. The database is minted on first connection and reaped automatically once it goes idle — noCREATE DATABASE, no teardown code, no cleanup sweeps in your harness.
Quick start
Three moves: start with the test profile, seal a template, connect by name.1. Spin up the database with --profile test
--profile test is the only server configuration you need: it applies the non-durable preset, arms the janitor (database prefix tdb_), and enables minting for every role. No config files to edit.2. Create your template, then seal it
createdb, run your migrations, load seed data — then make it clonable with one call:3. Connect with a tdb_ name to get a clone
tdb_<template>__<token> materializes a fresh clone of that template on first use — invent a token (worker id, test id, uuid), connect, done. Every connection using the same name shares the same clone. When the last connection closes and the grace period passes, the server drops it for you: no teardown code.The --profile test launch flag
pgrust --profile test is macro-expansion into -c arguments at that argv position — nothing more. It expands to:
-c overrides a profile value (pgrust --profile test -c fsync=on runs with fsync=on), and profile values override postgresql.conf, exactly as -c does. The same settings ship as a config file in the Pgrust repository if you prefer include-managed configuration — the flag and the file are kept in sync by a test.
The profile enables minting for every role (mint_roles = *): a server launched with --profile test is disposable by declaration, so gating who may mint would be friction without protection. On servers that arm the janitor without the profile — durable dev or preview boxes — the GUC’s own default ('', minting off) keeps the fail-closed posture; see the configuration reference.
Unknown profile names fail at startup with the list of known profiles (currently just test).
The filesystem recipe
You might reach fortmpfs here — don’t. tmpfs has no copy-on-write clone support (which would defeat fast template clones), and a ramdisk hard-caps at its allocation, so “spilling to disk” means swap thrashing.
The key insight: once fsync is off, an ordinary on-disk filesystem already behaves like an in-memory filesystem with disk spill. Every write lands in the kernel page cache and returns immediately; writeback happens lazily in the background, and only memory pressure forces pages out. Reads of recently written test data are cache hits. That is exactly “in memory as much as possible, spill to disk when needed” — implemented by the kernel, with no capacity cliff.
So this layer is just a filesystem choice:
- macOS
- Linux
Ephemeral databases
This is the piece that removes database lifecycle code from your harness entirely. It has four parts: templates you seal, names that mint databases on connection, a janitor that reaps them, and an optional warm pool.Templates and sealing
A template is an ordinary database you build once — migrations, seeds, extensions,ALTER DATABASE ... SET settings — then seal:
VACUUM (FREEZE, ANALYZE) inside the template, then ALTER DATABASE myapp_tpl WITH IS_TEMPLATE true ALLOW_CONNECTIONS false. Both flags are stock Postgres (template0 ships exactly this way): IS_TEMPLATE makes the database clonable and drop-protected, and ALLOW_CONNECTIONS false makes it immutable, which lets the server skip redundant flush work on later clones. The freeze half prevents anti-wraparound autovacuum from ever touching the template, and the ANALYZE bakes planner statistics into every clone — important because test mode runs with autovacuum = off, so clones never get analyzed on their own. The function requires owner-or-superuser and returns ordinary SQL errors; the manual statements remain valid if you prefer them.
Minted databases inherit the template’s contents and its ALTER DATABASE ... SET / ALTER ROLE ... IN DATABASE ... SET settings (stock CREATE DATABASE ... TEMPLATE drops those; the mint path copies them).
__ in template names: the first __ in a database name splits template from token, so a template whose own name contains __ can never be selected by the mint grammar.Mint-on-connect
Any connection to a nonexistent database whose name matches the ephemeral grammar mints it from a template, transparently, during connection startup:- The token is your harness’s choice: a worker id, a uuid, a run id. The first
__splits template from token (tdb_a__b__cselects templatea, tokenb__c). The whole database name must fit the standard 63-byte identifier limit. - Every connection using the same name shares the same database — connection pools work, commits are real, visibility across connections is real.
- The first connection with a new name pays the mint (about a millisecond from the warm pool; tens of milliseconds for a cold clone of a typical test template). Everyone else pays nothing.
- Minting is post-authentication and gated by
pgrust.ephemeral_db_mint_roles. Under--profile testevery role may mint. An unauthorized role connecting to a missing database gets the stock “does not exist” error, indistinguishable from a typo; an authorized role connecting to a prefix-matching name without the__separator gets a clear FATAL naming the required form. tdb_spare_<digits>is the warm pool’s reserved namespace and never mints.
The janitor
A background worker owns every database under the configured prefix:- Reap on idle. A minted database with zero connections continuously for the grace period (
15sunder the profile) is dropped, batched and checkpoint-amortized. Disconnecting is teardown. - Startup sweep. Ephemeral databases do not survive a server restart: on the first janitor cycle after startup, every prefix-matching non-template database is dropped, unconditionally — leftovers from crashed runs and pre-existing matches alike. The log names every drop.
- Pinning.
SELECT pgrust_pin_database('tdb_myapp_tpl__w3')exempts a database from reaping — useful to inspect a failure or hold a long provisioning window;pgrust_unpin_databasereleases it. Pins live in server memory and do not survive a restart. - Templates are never touched. Anything with
IS_TEMPLATE trueis invisible to reaping and the sweep.
The warm pool
For high-churn suites — a fresh database per test — setpgrust.ephemeral_db_pool_size = N (up to 4096). The janitor then keeps up to N pre-minted spares warm per template you mint from and satisfies mint requests by a catalog-only rename instead of a clone, with no checkpoints on the connect path — handout latency is around a millisecond.
The pool is usage-keyed: the first mint of a template registers it for pooling (that first mint is served cold), and the janitor keeps its spares topped up in the background thereafter. Several templates pool concurrently, sharing a global 4096-spare ceiling round-robin when it binds. Spares are unconnectable until handed out, are replenished in background ticks, and drain per template if that template is dropped, rebuilt, or unsealed.
Automatic mint machinery
You don’t configure any of this, but it explains the server log lines:- Simultaneous cold mints are batched — many requests share one transaction and one checkpoint pair, and directory copies fan out across worker threads.
- Templates with a relation count at or above
pgrust.ephemeral_db_wal_log_threshold(the profile sets 50) clone viaCREATE DATABASE ... STRATEGY wal_log, which requests no checkpoints at all; smaller templates keep the file-copy path, where the copy-on-write clone is what makes them fast. - After every successful mint, a one-shot prewarm worker connects to the new database and exits, off the connecting client’s critical path, so catalog pages and the relcache init file are already warm when the first real session arrives. Warm-pool spares get this at replenish time.
pgrust.ephemeral_db_prewarm = offdisables it. - The janitor periodically vacuums the shared catalogs that mint/drop churn touches (
pg_database,pg_shdepend,pg_db_role_setting), keeping mint cost flat over long runs even withautovacuum = off.
Configuration reference
All ephemeral-database settings, with their out-of-the-box defaults (the values--profile test applies are noted):
fsync = on server — dev boxes and preview environments can arm pgrust.ephemeral_db_prefix and a role allowlist without the rest of the test profile. The non-durable settings and the janitor are separate opt-ins that compose.
Harness recipes
Per-worker (the default recommendation). Each parallel test worker connects totdb_myapp_tpl__<worker-id>_<run-id>; within a worker, keep using your framework’s transaction-rollback isolation per test. Rollback composes inside an ephemeral database — this is deliberately not a rollback replacement, and rollback is faster than any clone for the tests it can serve.
Per-test. Tests that commit, use multiple connections, or are serialized by shared state get a fresh token each. Size the warm pool for your burst rate.
Per-run e2e. Browser-test tiers where one app server holds one DATABASE_URL: mint one database per run (tdb_myapp_tpl__e2e_<run-id>), pin it if provisioning happens long before the first connection, and unpin at teardown — or don’t; the janitor gets it either way.
Multiple baseline states. Seal several templates (myapp_bare, myapp_seeded) and pick per connection: tdb_myapp_bare__w1 vs tdb_myapp_seeded__w1. One server serves both.
On any crash: recreate, don’t recover. If the server or machine crashes, delete the data directory and re-run the suite setup (or restore a cached copy of the post-initdb directory). Never rely on crash recovery of a test-mode cluster.
UNLOGGED expecting extra speed — measured slower under test mode. The non-durable settings already bank the WAL savings; unlogged tables only add per-clone init-fork copying.Troubleshooting
Errors a harness can branch on:- Stock
database ... does not exist— the connecting role is not inpgrust.ephemeral_db_mint_roles, or the name genuinely doesn’t match the grammar (unauthorized roles get the stock error by design). Under--profile testevery role may mint, so check the name’s form. - Immediate FATAL
cannot mint ... the pgrust ephemeral-db janitor is not running— the janitor never registered or exited. Check thatpgrust.ephemeral_db_prefixis set (it requires a restart to change) and look in the server log for a disabled-until-restart warning. - FATAL naming the required
tdb_<template>__<token>form — an authorized role connected to a prefix-matching name without the__separator, or with an empty template or token side. timed out waiting for ephemeral database ... to be minted(60s) — the janitor is present but wedged or saturated. Check the server log for contained janitor errors and whether a huge mint burst outran the warm pool.mint request table is full— sized to be unreachable; report it as a bug.- Mint refused although the template exists — is it sealed? Run
SELECT pgrust_seal_template('<name>'); both flags (IS_TEMPLATE true,ALLOW_CONNECTIONS false) are required. - A database vanished mid-debug — the grace period expired. Pin it next time, or raise
pgrust.ephemeral_db_grace. - A pre-existing
tdb_*database vanished at server start — the startup sweep drops every prefix-matching non-template database; the log names each one. Rename databases out of the prefix (or pick a different prefix) to keep them. - Everything gone after restart — by design; ephemeral databases are disposable.
FAQ
Is test-mode Pgrust behaviorally different from production Pgrust?
Is test-mode Pgrust behaviorally different from production Pgrust?
--profile test expands to configuration, and query execution, WAL, checkpointing, and crash-handling code are identical to a production launch. What changes is the durability contract (the non-durable settings) plus an opt-in lifecycle worker that mints and drops databases under one name prefix. Isolation between minted databases is real Postgres isolation: separate tables, sequences, LISTEN/NOTIFY channels, and advisory locks, with none of the gotchas of rollback-based test isolation — commit hooks fire, deferred constraints enforce, multiple connections see each other.Why not just use tmpfs?
Why not just use tmpfs?
fsync = off, a normal filesystem’s page cache gives you the in-memory behavior for free, with graceful writeback under pressure.Do I have to use ephemeral databases to use test mode?
Do I have to use ephemeral databases to use test mode?
--profile test and still issue your own CREATE DATABASE t_42 TEMPLATE tpl / DROP DATABASE t_42 WITH (FORCE) — file_copy_method = clone makes those clones copy-on-write too. Conversely, a durable server can arm just the janitor for dev or preview environments. The mint machinery exists so your harness doesn’t have to manage any of that.Does this work in CI containers?
Does this work in CI containers?
mount privileges (a privileged container or a host-prepared mount). If you can’t get a reflink filesystem in CI, everything still works on ext4/overlayfs — clones become fast in-kernel copies, so you lose the O(1)-in-template-size property but keep the durability behavior, the memory-first behavior, and the whole ephemeral-database lifecycle.Can I use test mode with the Docker image?
Can I use test mode with the Docker image?
--profile test to the container command (see Run Pgrust with Docker for how server arguments are passed), on an image version that includes it (v0.3-beta or newer). For the copy-on-write clone path, the data volume must live on a reflink-capable filesystem on the host (APFS/XFS/btrfs).What happens to connection pools when the database is reaped?
What happens to connection pools when the database is reaped?