solutions

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.

1 VM
per database
warm
cache on the first query
$0
compute while idle
postgres 16 — branch + point-in-time restore
definition

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, long-lived and shared staging databases compared
ephemeral databaselong-lived databaseshared staging database
Lifetimeone job: created at the start, deleted at the endas long as the application it serveslong-lived, refreshed now and then
Who writes to itone test run, one PR, or one agent taskyour applicationevery developer, CI job and branch at once
Schemaexactly the branch under testwhatever is deployedwhichever migration ran last
Destructive migrationstry them; the copy is thrown awayonly after review, with a backuprisky: a mistake blocks everyone
Test isolationno other writers, so no cross-test interferencenot a place to run teststests see each other's rows
Cleanupone DELETE callnever; it is the system of recordrarely; 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.

options

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.

Ways to get an ephemeral PostgreSQL database
optionwhere it runsstarting datacleanup
pg_tmp (ephemeralpg)a temporary local Postgres on your machine or CI runnerempty; you load schema and fixturesgarbage-collected after a timeout set with -w (60 seconds by default)
Testcontainersa Postgres container next to your tests; needs local Docker or Testcontainers Cloudthe image you pick; empty unless you load datacontainers are removed after the run, even if the test process is killed
Neon branchNeon's managed Postgresa copy-on-write clone of the parent's data by default; schema-only is an optiondelete it, or give it an expiration date
Supabase preview brancha separate Supabase environment per branch, via the GitHub integration or the dashboardno data by default; a seed file or the Include data option adds itpreview branches are deleted when the PR is merged or closed
PandaStack brancha new PostgreSQL 16 microVM on the parent's hosta copy-on-write copy of a running database, cache warm by defaultexplicit DELETE; idle branches auto-suspend
PandaStack clonea new microVM on any healthy hostthe source's backups, optionally stopped at a point in timeexplicit DELETE
PandaStack empty databasea new microVMan empty PostgreSQL 16explicit 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.

on pandastack

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.

bash — branch, use, delete
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"
testing and ci

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.
branch goldenmigrate + testdelete (always)
yaml — .github/workflows/integration-tests.yml
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"
          fi
yaml — .github/workflows/sweep-ci-databases.yml
name: 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"
              done
pull requests

A 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.

ai agents

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.

typescript — npm i @pandastack/sdk
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

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.

python — pip install pandastack
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)
cleanup and cost

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.

the engine

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

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.
faq

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.