Ephemeral Postgres databases:
branch, test, throw away.
An ephemeral database is a real PostgreSQL 16 server that exists for one job (a test run, a pull request, an agent task) and is deleted when the job ends. On PandaStack each one is its own Firecracker microVM: branch a running database with its cache already warm, clone any moment in its backup window, and pay nothing for compute while it sleeps.
What is an ephemeral database?
An ephemeral database is a short-lived, isolated database created on demand for a single task (a CI run, a pull request, an AI agent session) and deleted when that task is done. Nothing else writes to it, so tests only see their own rows, destructive migrations are safe to try, and cleanup is one call.
| ephemeral database | long-lived database | shared staging database | |
|---|---|---|---|
| Lifetime | one job: created at the start, deleted at the end | as long as the application it serves | long-lived, refreshed now and then |
| Who writes to it | one test run, one PR, or one agent task | your application | every developer, CI job and branch at once |
| Schema | exactly the branch under test | whatever is deployed | whichever migration ran last |
| Destructive migrations | try them; the copy is thrown away | only after review, with a backup | risky: a mistake blocks everyone |
| Test isolation | no other writers, so no cross-test interference | not a place to run tests | tests see each other's rows |
| Cleanup | one DELETE call | never; it is the system of record | rarely; data accumulates |
Not to be confused with the ephemeral configuration database in Junos OS, which is a configuration store on Juniper network devices. On this page, an ephemeral database is a PostgreSQL server whose lifetime you control.
Ways to get an ephemeral Postgres
The approaches differ in where the server runs and where its starting data comes from. Each non-PandaStack row is described from that project's own documentation, linked under the table.
| option | where it runs | starting data | cleanup |
|---|---|---|---|
| pg_tmp (ephemeralpg) | a temporary local Postgres on your machine or CI runner | empty; you load schema and fixtures | garbage-collected after a timeout set with -w (60 seconds by default) |
| Testcontainers | a Postgres container next to your tests; needs local Docker or Testcontainers Cloud | the image you pick; empty unless you load data | containers are removed after the run, even if the test process is killed |
| Neon branch | Neon's managed Postgres | a copy-on-write clone of the parent's data by default; schema-only is an option | delete it, or give it an expiration date |
| Supabase preview branch | a separate Supabase environment per branch, via the GitHub integration or the dashboard | no data by default; a seed file or the Include data option adds it | preview branches are deleted when the PR is merged or closed |
| PandaStack branch | a new PostgreSQL 16 microVM on the parent's host | a copy-on-write copy of a running database, cache warm by default | explicit DELETE; idle branches auto-suspend |
| PandaStack clone | a new microVM on any healthy host | the source's backups, optionally stopped at a point in time | explicit DELETE |
| PandaStack empty database | a new microVM | an empty PostgreSQL 16 | explicit DELETE |
Reach for a local tool when a test needs an empty database on one machine. Reach for a branch when it needs production-shaped data, runs in hosted CI without a container runtime, or has to be handed to someone else (a reviewer, an agent) as a URL. What sets a PandaStack branch apart is the unit of isolation: each one is a separate microVM with its own kernel, its own postgres process and its own credentials.
Three ways to get one on PandaStack
Each returns a new database id with its own postgres:// URL, freshly generated credentials and its own backup history. They differ in where the data comes from and how long you wait.
Branch a running database
POST /v1/databases/{id}/branchcopies a running database on the parent's host with no backup round-trip. Branches are warm by default: the parent is memory-forked, so tables it had cached are served from memory on the first query. The parent pauses while its memory is copied, so the pause grows with its tier (1, 4 or 16 GiB), and then it keeps serving. Pass {"warm": false} for a disk-only branch with a much shorter pause.
Clone any point in time
POST /v1/databases/{id}/clonebuilds a new database from the source's base backup and WAL on any healthy host, and works on running, hibernated and failed sources. target_time stops replay at an instant at least two minutes old and inside your retention window (7 days on Free, 30 on Pro, 90 on Team); size lands the copy on a 1, 4 or 16 GiB tier. Large databases take minutes.
Start from empty
POST /v1/databases provisions a blank PostgreSQL 16 in its own microVM, typically in 60 to 180 seconds end to end. Use it for schema-only suites where isolation matters more than speed. For an empty database on your own machine, a local tool such as pg_tmp starts faster.
API=https://api.pandastack.ai/v1/databases
AUTH="Authorization: Bearer $PANDASTACK_API_KEY"
# Branch a running database into a NEW database id (HTTP 202)
BRANCH_ID=$(curl -fsS -X POST "$API/$GOLDEN_DB_ID/branch" \
-H "$AUTH" -H "Content-Type: application/json" \
-d '{"label": "ci-1842"}' | jq -r .id)
# Poll until running; only then does GET return credentials
for _ in $(seq 1 90); do
STATUS=$(curl -fsS -H "$AUTH" "$API/$BRANCH_ID" | jq -r .status)
[ "$STATUS" = running ] && break
[ "$STATUS" = failed ] && { echo "branch failed" >&2; exit 1; }
sleep 2
done
# The branch has its OWN credentials. Never reuse the parent's.
DATABASE_URL="$(curl -fsS -H "$AUTH" "$API/$BRANCH_ID" \
| jq -r .connection_url)?sslmode=require"
# ... migrate and test against $DATABASE_URL ...
# One call removes the VM, the volume and its backups
curl -fsS -X DELETE -H "$AUTH" "$API/$BRANCH_ID"Ephemeral databases for testing and CI
Keep one golden database (migrated, seeded, masked) and give every CI run its own branch of it. Tests run against production-shaped data, parallel jobs never see each other's writes, and nothing waits on a seed script.
One branch per run, one DELETE per run
- Branch once per run, or once per parallel worker with labels like
ci-<run>-<worker>, so one shard can't corrupt another's rows. - Reset by branching again, not by truncating tables or re-running seed scripts.
- Delete in an
if: always()step so failed runs clean up too, and schedule a sweep for anything a cancelled run left behind. Managed databases never expire on their own. - The golden database auto-suspends after 15 idle minutes unless you set
always_on, and a branch needs a running parent, so the workflow wakes it first.
name: integration-tests
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
env:
API: https://api.pandastack.ai/v1/databases
PANDASTACK_API_KEY: ${{ secrets.PANDASTACK_API_KEY }}
GOLDEN_DB_ID: ${{ vars.GOLDEN_DB_ID }}
LABEL: ci-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@v4
- name: Branch the golden database
run: |
set -euo pipefail
AUTH="Authorization: Bearer $PANDASTACK_API_KEY"
status() { curl -fsS -H "$AUTH" "$API/$1" | jq -r .status; }
wait_running() {
for _ in $(seq 1 90); do
s=$(status "$1")
[ "$s" = running ] && return 0
[ "$s" = failed ] && break
sleep 2
done
echo "database $1 not running (last status: $s)" >&2
return 1
}
# A branch forks a RUNNING parent. Wake the golden
# database if it auto-suspended since the last run.
if [ "$(status "$GOLDEN_DB_ID")" = hibernated ]; then
curl -fsS -X POST -H "$AUTH" "$API/$GOLDEN_DB_ID/wake" > /dev/null
fi
wait_running "$GOLDEN_DB_ID"
BRANCH_ID=$(curl -fsS -X POST -H "$AUTH" \
-H "Content-Type: application/json" \
-d "{\"label\": \"$LABEL\"}" \
"$API/$GOLDEN_DB_ID/branch" | jq -r .id)
echo "BRANCH_ID=$BRANCH_ID" >> "$GITHUB_ENV"
wait_running "$BRANCH_ID"
DB=$(curl -fsS -H "$AUTH" "$API/$BRANCH_ID")
echo "::add-mask::$(jq -r .password <<< "$DB")"
echo "DATABASE_URL=$(jq -r .connection_url <<< "$DB")?sslmode=require" >> "$GITHUB_ENV"
- name: Migrate and test
run: npm ci && npm run migrate && npm test # your commands
- name: Delete the branch
if: always()
run: |
if [ -n "${BRANCH_ID:-}" ]; then
curl -sS -X DELETE \
-H "Authorization: Bearer $PANDASTACK_API_KEY" "$API/$BRANCH_ID"
finame: sweep-ci-databases
on:
schedule:
- cron: "0 */6 * * *"
jobs:
sweep:
runs-on: ubuntu-latest
steps:
- name: Delete ci-* databases older than 6 hours
env:
PANDASTACK_API_KEY: ${{ secrets.PANDASTACK_API_KEY }}
run: |
set -euo pipefail
API=https://api.pandastack.ai/v1/databases
AUTH="Authorization: Bearer $PANDASTACK_API_KEY"
CUTOFF=$(( $(date +%s) - 6 * 3600 ))
curl -fsS -H "$AUTH" "$API" \
| jq -r --argjson cutoff "$CUTOFF" '.items[]
| select((.label // "") | startswith("ci-"))
| select((.created_at // 0) > 0 and .created_at < $cutoff)
| .id' \
| while read -r id; do
curl -sS -X DELETE -H "$AUTH" "$API/$id"
doneA database per pull request
When the database should live as long as the review rather than one CI run, branch once when the pull request opens, reuse the branch on every push, and delete it when the pull request closes. Our guide to a database per pull request has the complete GitHub Actions workflow, including deterministic pr-<number> labels, re-runs that reuse the branch instead of duplicating it, and the four ways it goes wrong. Add preview environments and reviewers get the running app on top of the branch.
Disposable Postgres for AI agents
An agent that writes SQL needs a database it is allowed to break. Branch one per task, hand the agent that branch's credentials (never production's), and delete it when the task ends.
Its own microVM
A DROP TABLE, a runaway query or a bad migration stays inside the branch's VM. Writes on the branch never reach the parent.
HTTP when TCP isn't available
Every database also exposes a query broker over HTTPS, authenticated with its own broker_token: query, exec and transaction endpoints, with row and timeout limits you can set per request.
Rotate in one call
reset-credentials replaces the postgres password and the broker token together and disconnects every client still using the old ones.
import { Client } from "@pandastack/sdk";
const client = new Client(); // reads PANDASTACK_API_KEY
const API = "https://api.pandastack.ai/v1/databases";
// 1. Branch the golden database for this task. Branch has no SDK
// method yet, so this one call is plain REST (202 + a new id).
const res = await fetch(`${API}/${goldenDbId}/branch`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PANDASTACK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ label: `agent-${taskId}` }),
});
if (!res.ok) throw new Error(`branch failed: ${res.status} ${await res.text()}`);
const { id } = await res.json();
// 2. Wait for it, then hand the agent THIS database, never production.
const db = await client.databases.waitUntilReady(id);
try {
await runAgentTask({ databaseUrl: db.connection_url! }); // your agent loop
} finally {
await client.databases.delete(id); // 3. throw it away
}More on ephemeral databases for AI agents, testing LLM-generated migrations before they reach production, and sandboxes for AI agents.
Test data: seed, clone, or mask once
Where the golden database's rows come from decides what your tests can catch.
Mask once, branch many times
Fixture seeds give you a clean, known dataset that drifts away from production's shape. A straight clone of production gives you the real shape and the real personal data with it. The pattern that holds up: clone production into a golden database, run your masking or anonymisation against that copy once, then branch the masked copy for every run and refresh it on a schedule with a new clone.
Point-in-time clones make “production as of Monday 09:00” reproducible. If production runs elsewhere, restore a pg_dump into the golden database instead. PandaStack does not rewrite your data: masking is your SQL or your tool, and it runs once rather than once per test.
Go deeper with realistic test data for ephemeral databases and cloning production for testing, safely.
import pandastack
client = pandastack.Client() # reads PANDASTACK_API_KEY
# Production as it was at 09:00, on a bigger tier, as a NEW
# database. The source is not modified. Blocks until running.
golden = client.databases.clone(
prod_db_id,
label="golden-2026-09-22",
target_time="2026-09-22T09:00:00Z",
size="4g",
)
print(golden["id"]) # becomes GOLDEN_DB_ID in CI
# Run your masking SQL against golden["connection_url"] here, once.
# Retire last week's golden copy: VM, volume and backups go together.
client.databases.delete(old_golden_id)Lifecycle, cleanup, and cost
Managed databases never expire on their own: the TTL reaper that recycles ordinary sandboxes skips them, and only an explicit DELETE destroys one. That is deliberate, because a test database vanishing mid-run is worse than a leak, so every workflow above deletes in an always-run step and backs that up with a sweep.
Delete is final and complete
DELETE /v1/databases/{id} removes the VM, the data volume and the backup archives together. Deleting a branch leaves its parent untouched.
Forgotten branches go quiet
After 15 minutes with no connections a database auto-suspends and bills storage only; the next connection wakes it in a few seconds. A forgotten branch is cheap, not free.
One rate card
$0.054 per active vCPU-hour and $0.0162 per working-set GiB-hour, metered per second while awake, plus $0.15 per GiB-month for storage beyond your plan's quota (Free 1 GiB, Pro 100 GiB, Team 2.5 TiB). Egress is not billed.
Worked example: a 1 GiB database that keeps one vCPU busy and all of its memory resident for a 10-minute test run bills at most about $0.012, which is ($0.054 + $0.0162) ÷ 6. Most runs bill less, because idle CPU meters near zero. Each branch has its own volume, which counts against your plan's storage quota like any other database. See pricing.
Real PostgreSQL 16, not a lookalike
Tests that pass against an emulation can still fail against production. Every ephemeral database here is the same PostgreSQL 16 as a long-lived one.
Extensions included
pgvector, pg_stat_statements, pg_trgm, ltree, hstore, uuid-ossp, pgcrypto and unaccent come pre-installed. Any driver or ORM connects with the postgres:// URL.
TLS, routed by SNI
TLS is required (sslmode=require), and the proxy routes each connection to your database's VM by its hostname.
Direct and pooled URLs
A direct URL on port 5432, and a PgBouncer transaction-pool URL on 6432 that accepts up to 500 client connections. Give the pooled one to parallel test workers; keep LISTEN/NOTIFY and advisory locks on the direct one.
Allow list and rate limit
An optional per-database IP allow list and new-connection rate limit sit on top of TLS and the password.
Full details are on the managed Postgres page.
Limits, and when you don't need this
What a branch can't do yet, and the cases where a local tool is the better answer.
- A branch forks a running parent on the parent's host. Wake a suspended parent first; branching a stopped one returns 409. When the parent isn't running, use clone, which rebuilds from backups on any host.
- Every branch lands on its parent's host, and each warm branch pauses the parent while its memory is copied, longer on bigger tiers. Keep the golden database away from live traffic, pass
{"warm": false}when a short pause matters more than a warm cache, and use clones to spread a large fan-out across hosts. - Branching is a REST call or the Branch button in the dashboard. The Python and TypeScript SDKs (0.9.0) cover create, clone, wait-until-ready and delete, but not branch yet.
- An empty database takes typically 60 to 180 seconds to create. For unit tests, an in-process fake, pg_tmp or Testcontainers is faster and runs on hardware you already have.
- Databases are in public beta. The data volume is host-local, backed by continuous WAL archiving and failover to another host. Read replicas, storage autoscaling and cross-host durability are on the roadmap.
- Memory is fixed per tier (1, 4 or 16 GiB, each with 8 vCPU). To change tier, clone into a new size.
- Branch and clone need a multi-node deployment. On a single-node self-hosted install, both endpoints return 501.
Frequently asked questions
What is an ephemeral database? +
An ephemeral database is a short-lived, isolated database created on demand for one task (a CI run, a pull request, an AI agent session) and deleted when the task ends. On PandaStack it is a full PostgreSQL 16 server in its own Firecracker microVM, created by branching or cloning an existing database or by provisioning an empty one.
How is an ephemeral database different from an in-memory database? +
An in-memory database such as SQLite's :memory: mode runs inside your test process and disappears with it, but it is not the engine you run in production. An ephemeral Postgres is the real server, with the same SQL dialect, extensions, locking and migrations; it just has a short life. Use in-memory fakes for fast unit tests and an ephemeral Postgres wherever behaviour has to match production.
How fast can I get an ephemeral Postgres database? +
A branch copies a running database on the parent's host with no backup round-trip; the parent pauses briefly (longer on the 4 and 16 GiB tiers; warm:false keeps it short), and you poll until the branch reports running. A clone rebuilds from backups and takes minutes for large databases. An empty database typically takes 60 to 180 seconds end to end. For an empty database on your own machine, a local tool such as pg_tmp or Testcontainers is quicker.
How do I create a database per pull request in GitHub Actions? +
Branch a golden or staging database with POST /v1/databases/{id}/branch, export the branch's connection_url as DATABASE_URL, run migrations and tests, and delete the branch in a step that runs even when the tests fail. For a database that lives as long as the pull request, use a deterministic label such as pr-<number>, reuse the branch on every push, and delete it on the closed event.
Can I use production data in an ephemeral database safely? +
Clone production into a golden database, run your masking or anonymisation against that copy once, and branch the masked copy for tests. Every branch and clone gets its own credentials and its own microVM, and the source is not modified. PandaStack does not mask data for you; that step is your own SQL or tool.
How are ephemeral databases cleaned up? +
Explicitly. Managed databases never expire on their own, and DELETE /v1/databases/{id} removes the VM, the data volume and the backup archives. Delete in a CI step that always runs and add a scheduled sweep for anything a cancelled job leaves behind. Until then, an idle database auto-suspends after 15 minutes without connections and bills storage only.
Is it real Postgres? +
Yes. It is PostgreSQL 16 with pgvector, pg_trgm, pgcrypto and other common extensions pre-installed, reachable with a normal postgres:// URL over TLS. Any driver or ORM works, and a PgBouncer URL that accepts up to 500 client connections is included for workloads that open many short-lived connections.
Should I use an ephemeral database or Testcontainers? +
Testcontainers runs Postgres in a container next to your tests and needs a Docker-compatible runtime, either local Docker or Testcontainers Cloud. It suits local and unit-level work well. A PandaStack branch runs on separate infrastructure, can start from a production-shaped dataset with a warm cache, and needs no container runtime in CI. Many teams use both.
How does this compare with Neon branching? +
Neon documents its branches as copy-on-write clones that include the parent's data by default, with an optional expiration date for automatic deletion. A PandaStack branch is also copy-on-write, but it is a separate PostgreSQL 16 microVM with its own kernel, it starts with the parent's cache warm by default, and you delete it explicitly.
Can AI agents use ephemeral databases safely? +
Give each task its own branch and hand the agent that branch's credentials or its HTTP query-broker token, never production's. The branch is a separate microVM, so a DROP TABLE or a runaway query stays inside it, and reset-credentials rotates the password and the broker token in one call.
What does an ephemeral database cost per CI run? +
Databases bill $0.054 per active vCPU-hour and $0.0162 per working-set GiB-hour, per second while awake, plus $0.15 per GiB-month for storage beyond your plan's quota. A 1 GiB database that keeps one vCPU busy and all of its memory resident for a 10-minute run bills at most about $0.012, and most runs bill less because idle CPU meters near zero. Network egress is not billed.
Ship on the millisecond cloud.
Free tier with $5.40/mo usage credit. No card. Apache-2.0.