Top 7 Disposable Postgres Platforms for Developers (2026)
Somewhere in your organisation there is a staging database that has been temporary since 2019. Its hostname is something like staging-db-new-2, which tells you exactly where staging-db-new went. Its password is in a wiki page, a Terraform variable and one person's shell history. And at some point a service started reading from it in production, by accident, and nobody has been brave enough to find out which one.
That database was disposable once. Someone created it in an afternoon to try something out. The reason it is not disposable any more has nothing to do with how it was created and everything to do with the fact that no system owns its destruction. That is the actual definition worth working from: a database is disposable when the delete path belongs to something that cannot forget. Creating databases is easy. Destroying them reliably is the engineering.
So this is not a vendor list. It is the seven shapes a throwaway PostgreSQL can take, and three of them are not products you buy — they are a flag you pass to initdb, a SQL statement on a cluster you already run, and a container you already have. The shapes matter more than the logos, because almost every bad decision in this space is someone buying one shape while needing another.
Four questions, asked in this order
Grade every shape below on these and the comparison gets short. Most roundups ask the first one and stop, which is why they recommend things that cannot test a migration.
- How long until you hold a DSN that accepts a connection? Not "how long until the API returns" — those are different events, and the gap between them is where asynchronous-create bugs live.
- Where is the isolation boundary: a process, a container, or a kernel? This decides what "I dropped the wrong table" can reach, and whether you are allowed to install an extension that crashes backends.
- Can it branch data, or only schema? A copy with real cardinality answers "will this migration finish inside the deploy timeout". A copy with an empty table answers nothing, extremely quickly.
- What is the honest failure mode, and who pays for it? Every shape has one. The interesting question is whether it fails as a wrong test result, a leaked credential, or a line item.
Question two is the one people answer wrong because the word "isolated" is doing three jobs. A separate database inside your existing cluster is isolated from your other databases' tables and nothing else: it shares the postmaster, shared_buffers, the connection limit and the superuser. A container is isolated from your filesystem and shares the host kernel. A microVM has its own kernel, so "what else could notice if this backend segfaults" has a boring answer. These are not three points on a spectrum of safety; they are three different sets of things that can go wrong.
Question three is the one that decides what you are allowed to test. A migration that adds a NOT NULL column with a default completes instantly against an empty table and holds a lock for an uncomfortable number of minutes against a real one. If your disposable database starts empty, your migration test is a syntax check wearing a lab coat.
The seven shapes
1. A container on your machine or in your test process
docker run postgres:16, or the same thing driven by Testcontainers so the lifecycle is tied to your test process rather than to your memory. This is the correct default and it belongs first, not last: a platform decision made before you have tried this is a platform decision made too early. You get the real binary at the tag you pinned, a random host port, and in the Testcontainers case a reaper sidecar that cleans up containers your process abandoned when it crashed.
Time to DSN: as long as Postgres takes to run initdb and its startup checks, which is the floor for every shape that starts a fresh cluster. Isolation: a container, so a shared kernel and your own filesystem view. Branches data: no, it starts empty, unless you restore a dump into it and pay that cost on every run.
The failure mode is not performance, and it is not emptiness — it is that this shape teaches your code the wrong thing about the network. There is no server certificate in the official image, so every connection is plaintext and sslmode=require fails outright. Which means the TLS path your production DSN uses is exercised for the first time in production. Then there is the port: -p 5432:5432 publishes on all interfaces, Docker's own iptables chain is consulted before the firewall front-end you configured, and most test fixtures set POSTGRES_HOST_AUTH_METHOD=trust because arguing with a password in a test fixture is nobody's idea of a Tuesday. A trust-authenticated Postgres, reachable from the hotel wifi, containing last week's anonymisation output. Bind it to 127.0.0.1 explicitly and move on.
2. initdb into a tmpfs, or pg_tmp
The same idea with the container removed and the durability thrown away on purpose. initdb into a directory on a tmpfs, set fsync = off and listen_addresses = '', start it on a unix socket, and let a trap delete the whole thing. pg_tmp from the ephemeralpg family does this for you and reaps the cluster after an idle timeout; the embedded-postgres libraries do it from inside your test runtime. The script in the next section is the do-it-yourself version, because it is about fifteen lines and then you understand it.
Time to DSN: the fastest of any shape that gives you a fresh cluster, because fsync is a lie and the data directory never touches a disk. Isolation: a process and a filesystem permission. Nothing is listening on a port, which is a genuinely stronger position than the container shape — there is no network surface to get wrong — but the cluster runs as your user, so a malicious extension or a sufficiently creative COPY PROGRAM has your shell's credentials, not a sandbox's.
Failure mode: your dataset has to fit in RAM, and the storage limit is enforced by the OOM killer rather than by a disk-full error, which is a much less pleasant way to find out. fsync = off is a correctness crime everywhere except here, where it is a feature — but it means a crash leaves a corrupt cluster, so never, ever let this configuration leak into a template that something long-lived gets built from. And the version is whatever your package manager felt like; pin it against production or your throwaway will cheerfully accept SQL that production rejects.
3. CREATE DATABASE ... TEMPLATE on a cluster you already have
The shape nobody writes a landing page for, and the one that wins most test suites outright. Seed one database properly — schema, reference data, a realistic row count — then clone it per worker with CREATE DATABASE worker_3 TEMPLATE template_app. You pay for the seed once instead of once per worker, and seeding is the expensive step in nearly every suite I have profiled.
Two mechanics decide whether this is fast or disappointing. First, the source template must have no other sessions connected to it, which is why seeding happens before the workers start rather than during. Second, since Postgres 15 the default copy strategy is WAL_LOG, which writes the entire new database through the write-ahead log; STRATEGY = FILE_COPY takes a checkpoint and copies the directory instead and is dramatically cheaper for a large template you are about to destroy. If you tried template databases on an older major version and remember them as fast, that is the strategy you were getting by default.
Time to DSN: whatever a directory copy costs, with no cluster start at all. Branches data: yes, real data, as real as whatever you put in the template. Isolation: effectively none in the senses that matter. Same postmaster, same shared_buffers, same superuser, and critically the same max_connections — which defaults to 100, so eight workers each holding a pool of twenty is not an arithmetic problem, it is an outage in your test suite. Raise it, or pool, or use fewer workers.
4. Storage-level branching (Neon, and the architecture everyone is copying)
Separate the compute from a multi-version storage layer and a branch becomes a pointer at a moment in time rather than a copy of anything. This is the reason "database branching" is a phrase engineers use at all, and if your mental model of disposable Postgres is "instant and full of real data", this is the architecture that put it there.
It answers question three better than anything else on this board: a branch carries real data with real cardinality, created in roughly the time it takes to write a pointer. What it cannot do is give you a boundary, because the whole trick is a shared storage layer and a shared control plane. For a test suite running your own code that is a perfectly good trade. For running somebody else's extension, or testing a change that could wedge a backend, it is the wrong shape and no amount of product polish changes that.
Things to establish from their current docs rather than from a blog post: how a branch's compute is sized and what it costs while nobody is querying it, how quickly a branch suspends and resumes, the limits on branch count and on total storage across branches, and what happens to a branch when its parent moves on. Failure mode: branch sprawl, discussed below, plus the general hazard of a shape so cheap that nobody builds a destroy path for it.
5. Platform branching (Supabase-shaped): a branch of the environment, not the database
The same branching idea wired into a whole platform, so what you get is closer to an environment than to a bare database: the Postgres, plus the auth, the storage, the functions, and migrations applied from your repository by the platform's own workflow. If your application already stands on that platform, this is exactly right and the integration is the product. If you only wanted a database for forty minutes, you have bought a lot of surface area to get one.
What to pin down before you depend on it: precisely what a branch includes beyond the database, what it costs while idle, how migrations are applied to a branch versus to the parent, and — the one that bites CI — how you create and destroy a branch programmatically rather than through a pull-request integration, because your pipeline will eventually need to do it without a pull request existing. Verify all of it against current docs; platform branching has been moving quickly.
6. Managed branch-and-deploy-request (PlanetScale-shaped, Xata-shaped): read the engine name
The workflow everything else is imitating: branch the schema, open a deploy request, get a reviewable diff and a safety check, merge it into production without a blocking lock. It is genuinely excellent engineering and it solves a problem the other six shapes do not even address, which is the governance of the schema change itself rather than the provisioning of somewhere to try it.
The instruction attached to this shape is narrow and important: read the engine name the way you would read a prenup. The branch-and-deploy-request workflow grew up around MySQL and Vitess, and the Postgres offerings in this space are newer and not necessarily identical in semantics. Xata rebuilt itself around Postgres and sells branches too. So confirm, in current documentation, which engine you would actually be running, whether the Postgres product has the same branch semantics as the MySQL one, and whether a branch carries data or only schema. If you are here because of something you read about branching a couple of years ago, that article is now describing a different product.
7. A microVM per database (PandaStack managed Postgres)
Full disclosure: this one is mine. A managed database here is a dedicated Firecracker microVM — guest kernel 5.10, Ubuntu 24.04 — running PostgreSQL 16 with a durable volume attached. Not a schema in a shared cluster, not a role, not a tenant inside a storage engine. The isolation boundary is a kernel boundary, which makes question two boring in the good way: CREATE EXTENSION whatever you like, crash as many backends as you want, test the change that would have been unforgivable on a shared cluster.
The native connection is TLS-required at postgres://...@<id>.db.pandastack.ai:5432/pandastack, with a pooled URL through the guest's pgbouncer for workloads that open many short-lived connections, and an HTTP query broker for callers that cannot hold a socket. RAM comes in tiers — 1g, 4g, 16g — and point-in-time clones replay from the database's own archive into a brand-new id, leaving the source untouched. Tiers, clones, credential rotation and the broker are all documented on the managed Postgres page.
Now the honest part. A create takes 30 to 90 seconds, because it provisions a machine and bootstraps a cluster, and that is the slowest create on this board by a wide margin. If your workflow wants a database per test case, this is the wrong shape and shape 3 is the right one. The RAM tier is fixed at create time because a snapshot-restored Firecracker VM cannot change its guest memory, so the supported resize is a clone into a different tier — a new database produced from the backup stream, not an ALTER on the running one. And one database means one VM, so the capacity unit you are really buying is memory, not rows.
The seven shapes, side by side
| Shape | Time to a usable DSN | Isolation boundary | Branches real data? | The honest failure mode |
|---|---|---|---|---|
| 1. Container / Testcontainers | A cluster start | Container, shared kernel | No — starts empty | No TLS and a port published wider than you think |
| 2. initdb in tmpfs / pg_tmp | Fastest fresh cluster | A process, as your own user | No — starts empty | Dataset must fit in RAM; the limit is the OOM killer |
| 3. CREATE DATABASE ... TEMPLATE | A directory copy | None worth the word — same cluster | Yes, whatever is in the template | Shared max_connections; the cluster becomes a pet |
| 4. Storage-level branch (Neon-shaped) | Pointer-fast, per their docs | Shared storage + control plane | Yes — the reference implementation | Sprawl: cheap per branch, unbounded in count |
| 5. Platform branch (Supabase-shaped) | Environment-shaped, per their docs | Platform tenancy | Yes, plus the rest of the environment | You adopt a platform to rent a database |
| 6. Managed branch + deploy request | Per their docs | Managed tenancy | Check: data or schema only | Confirm which engine you would actually run |
| 7. MicroVM per database (PandaStack) | 30-90s, measured | Its own kernel | Yes, via clone / point-in-time clone | Slowest create here; RAM tier fixed at create |
Shapes 2 and 3, in one script you can run today
This is the version I reach for before I reach for anything with a dashboard. One cluster in RAM, seeded once, then a database per parallel worker cloned from that seed. It needs no account, no network and no cleanup job, because the trap handler is the cleanup job.
#!/usr/bin/env bash
# Shapes 2 and 3 in one script: one throwaway cluster in RAM, seeded once,
# then one database per parallel test worker cloned from that seed.
set -euo pipefail
PGDATA=$(mktemp -d /dev/shm/pgtmp.XXXXXX) # /dev/shm is a tmpfs on most Linux
SOCK=$(mktemp -d) # unix socket dir: no TCP listener
trap 'pg_ctl -D "$PGDATA" -m immediate stop >/dev/null 2>&1 || true; rm -rf "$PGDATA" "$SOCK"' EXIT
initdb -D "$PGDATA" -U postgres --auth=trust >/dev/null
# Durability is a liability here. Nothing in this cluster is allowed to
# outlive the script, so every guarantee you pay for at runtime is waste.
cat >>"$PGDATA/postgresql.conf" <<'CONF'
fsync = off
full_page_writes = off
synchronous_commit = off
listen_addresses = ''
max_connections = 200
CONF
pg_ctl -D "$PGDATA" -o "-k $SOCK" -w start >/dev/null
export PGHOST="$SOCK" PGUSER=postgres
# Pay for schema + seed exactly once, into a template database.
createdb template_app
psql -q -d template_app -f schema.sql
psql -q -d template_app -f seed.sql
# One database per worker. CREATE DATABASE ... TEMPLATE refuses to run while
# any other session is connected to the source, which is why the seeding above
# happens before the workers start and never while they are running.
#
# Postgres 15+ defaults to STRATEGY = WAL_LOG, which pushes the whole copy
# through the write-ahead log. FILE_COPY checkpoints and copies the directory
# instead — far cheaper for a large template you intend to destroy anyway.
for i in $(seq 1 8); do
psql -q -d postgres -c \
"CREATE DATABASE worker_$i TEMPLATE template_app STRATEGY = FILE_COPY;"
done
echo "worker 3 DSN: postgresql:///worker_3?host=$SOCK"
# No password, no TLS, no listening socket, no survivors.Three details in there are the whole point. listen_addresses = '' means there is no TCP socket to misconfigure. The seeding happens strictly before the loop because TEMPLATE refuses a source with other sessions attached. And max_connections is raised explicitly, because the default of 100 divided by eight workers with ORM pools is the arithmetic that produces the flakiest failure in testing: a suite that passes alone and fails in parallel, intermittently, in a way that looks exactly like a race condition in your code.
Shape 7, in practice: create, rehearse, clone, destroy
The microVM shape earns its 30-to-90 seconds in exactly one workflow: you want a database you are genuinely allowed to destroy, containing real data, on which you can time a migration and then throw the entire machine away. Per pull request, not per test.
import os
from pandastack import Client
# Auth is PANDASTACK_API_KEY. (PANDASTACK_TOKEN was removed in SDK 0.3.0.)
c = Client()
# Shape 7: a dedicated Firecracker microVM running PostgreSQL 16, with a
# durable volume attached. create() blocks until Postgres answers, and that
# honestly takes 30-90 seconds: a VM is provisioned and a cluster is
# bootstrapped. Set your CI step timeout accordingly, and create one of these
# per pull request, never per test case.
db = c.databases.create(size="4g", label=f"pr-{os.environ['PR_NUMBER']}-migration-rehearsal")
print(db["connection_url"]) # postgres://...@<id>.db.pandastack.ai:5432/pandastack
print(db["pooled_connection_url"]) # same creds via the guest's pgbouncer (txn pooling)
# TLS is required on the native port, so this is the one shape in this post
# where your sslmode=require code path is actually exercised before production.
try:
# ... run migrations against db["connection_url"], time them, assert on
# lock duration, then throw the whole machine away.
pass
finally:
c.databases.delete(db["id"]) # irreversible, which is the entire point
# Point-in-time clone: a NEW database id, replayed from the source's archive.
# target_time must be at least 2 minutes in the past, because the WAL has to
# physically reach the archive before anyone can replay to a moment inside it.
forensic = c.databases.clone(
db["id"],
label="what-did-the-backfill-eat",
target_time="2026-10-06T09:00:00Z",
)
# And this is the supported "resize": clone into a different RAM tier. A
# snapshot-restored microVM cannot change its guest RAM, so the tier is fixed
# at create time and growing one means producing a new database from the
# backup stream. The source is never touched.
bigger = c.databases.clone(db["id"], label="same-data-16g", size="16g")The two-minute floor on target_time is not a product limitation, it is physics with a shipping address: the write-ahead log has to reach the archive before anything can replay to a moment inside it. Every archive-based point-in-time system has some version of that floor, and a vendor who does not mention theirs has one anyway.
Which makes the highest-value use of this shape nothing to do with CI. Someone ran a backfill, it set a large number of rows to the wrong value, and the question in the incident channel is what those rows looked like before. Clone to five minutes before the deploy, point a read-only session at the clone, diff. That is a question that used to require a restore ticket and an afternoon.
A branch is cheap until there are four hundred of them
Every shape in this post leaks, and the cheaper the shape, the worse the leak, because nobody budgets a destroy path for something that costs nothing. Four hundred open branches is not a technical failure; it is a billing event with a three-week fuse, and the thing that turns up first is usually not the cost but a limit — branch count, storage total, connection total — hit by an unrelated deploy on a Friday.
The structural problem is that teardown is almost always written inside the job that needs cleaning up after. That code does not run when the job is cancelled, when the runner is reclaimed mid-step, or when the network eats the DELETE and hands you a response indistinguishable from success. So: trap your teardown on every exit path and then assume it failed anyway.
import time
from pandastack import Client
# The teardown in your CI job runs inside the job it is cleaning up after, so
# it does not run when the job is cancelled, when the runner is reclaimed, or
# when the network eats the DELETE. Assume leakage and sweep on a schedule.
c = Client()
CUTOFF = time.time() - 6 * 3600
for db in c.databases.list():
label = db.get("label", "")
if not label.startswith("pr-"):
continue # not ours to judge
if db.get("always_on"):
continue # someone opted this out on purpose
if db.get("created_at", 0) > CUTOFF:
continue # still plausibly in use
print("reaping", db["id"], label, db.get("status"), db.get("size"))
c.databases.delete(db["id"])
# Two rules make this safe enough to run unattended: the label encodes who
# created it and why, and the reaper only ever deletes labels it owns. A
# reaper with a broad rule is how a "temporary" database becomes a pet — the
# first time it eats something real, somebody turns it off forever.The same twenty lines write themselves against pg_database for shape 3, against a branch list for shapes 4 to 6, and against docker ps for shape 1. What matters is not the language but two rules: the label says who created the thing and why, and the reaper only ever deletes labels it owns. A reaper with a broad rule eats something real exactly once, and then somebody disables it permanently, and then you have a 2019 staging database again — this time with four hundred siblings.
How a disposable database becomes a pet
The progression is always the same and it never involves a decision. Someone creates a throwaway to test a migration. The throwaway works, so a second person points a script at it rather than creating their own. Now it has two users, which means deleting it requires asking, which means it survives the week. Then it gets a DNS name, because typing the host is annoying. Then a service reads from it during an incident, as a workaround, and the workaround outlives everyone's memory of the incident. By the time anyone asks whether it is still needed, the only honest answer is "find out by turning it off", which nobody will authorise.
The defence is not discipline, it is making the destroy path belong to a system instead of a person. A trap handler in the script. An absolute TTL set at creation time and enforced by the platform rather than by your orchestrator, so a crashed pipeline cannot leak one. A hostname that is unmistakably disposable — a random id is better than a readable name here, precisely because nobody bookmarks it. And a hard rule that credentials for a disposable database are generated at creation and never written down; if the DSN is in a wiki page, you have already lost.
If you cannot name the system that will delete this database without a human remembering, it is not a disposable database. It is a production database with a humble name.
What to actually pick
There is no best shape here, because the trades are structural rather than a matter of execution quality. Instant plus real data requires a shared storage layer. A kernel boundary requires provisioning a machine. Zero network surface requires giving up the network. No vendor collapses those, and the ones that claim to have usually moved the cost somewhere you will find later.
- Unit and most integration tests: shape 3, with shape 2 underneath it. A seeded template plus a database per worker beats every managed option on latency and costs nothing. Start here and let a real limitation push you off it.
- Local development and pinned-version reproducibility: shape 1. Accept that it starts empty and bind the port to localhost.
- Per-pull-request environments where reviewers need realistic data: shapes 4 or 5, graded on idle cost and on whether you can create and destroy one without a pull request existing.
- Governed schema changes against production: shape 6, after you have confirmed which engine you are running.
- Migration rehearsals, extension testing, anything where a crashed backend or a rogue extension must not reach another tenant, and forensic point-in-time questions: shape 7, and budget the 30 to 90 seconds.
One last sequencing note, because it is the mistake I see most often. Teams shop for a platform before they have made one correct disposable database by hand. Build the seeded template first. Find out whether your suite's real problem is provisioning latency, data realism, connection limits or isolation — they look identical from the outside and have four different solutions. Then buy the shape that fixes the one you actually have.
Frequently asked questions
What is the fastest way to get a disposable Postgres right now, with no account?
initdb into a tmpfs and start it on a unix socket, then clone per-worker databases out of a seeded template. That is shapes 2 and 3 stacked, it is about fifteen lines of bash, and it beats every hosted option on time-to-DSN because there is no network between you and the cluster and no durability being paid for. Concretely: initdb into a directory under /dev/shm, append fsync = off, full_page_writes = off, synchronous_commit = off and listen_addresses = '' to postgresql.conf, start with pg_ctl pointing -k at a socket directory, and trap the stop-and-delete on script exit. Seed one database called something like template_app, then CREATE DATABASE worker_1 TEMPLATE template_app STRATEGY = FILE_COPY for each parallel worker. Two cautions. Raise max_connections above the default of 100 before you run eight workers with connection pools, or you will spend an afternoon debugging what looks like a race condition in your application and is actually the postmaster refusing connections. And pin the Postgres major version to match production, because a throwaway cluster from your package manager will happily accept syntax that production rejects, which converts a disposable database from a testing tool into a source of false confidence.
Is seeding enough, or does a disposable database need real data?
It depends entirely on what you are testing, and conflating the two is how migration incidents happen. Seeded fixtures are deterministic, live in version control, review like code and contain exactly the data shapes you thought of. That is perfect for unit and integration tests, and it is why the template-database shape wins so much work. What seeded data cannot do is tell you how long a lock will be held. A migration adding a NOT NULL column with a default completes instantly against fifty seeded rows and holds an exclusive lock for minutes against forty million real ones, and the deploy timeout does not care which of those you tested. Nor will your fixtures ever contain the row with a NULL in the column your migration assumes is populated, because you wrote the fixtures from the same mental model you wrote the migration from. So: seed for correctness tests, clone real data for anything about duration or lock behaviour, and keep that clone short-lived and access-controlled. And if you are considering a pipeline that anonymises production into automatically-created environments, price the whole thing honestly — you are building a system that copies customer data on a trigger, and the realistic failure is not an attacker but a well-meant psql select left in a CI log that every repository reader can see.
How do I stop disposable databases from leaking?
Accept that your in-job teardown will not run, and build a second mechanism that does not depend on it. Teardown written inside a CI job does not execute when the job is cancelled, when the runner is reclaimed mid-step, or when a network partition makes a failed DELETE look exactly like a successful one. So trap the teardown on every exit path, and then run a scheduled reaper that sweeps by label and age regardless. Two rules keep a reaper safe enough to leave unattended: every disposable database carries a label encoding who created it and why — the pull request number is ideal, because it is reconstructable from outside the job — and the reaper deletes only labels matching its own prefix, never anything it cannot explain. Prefer absolute TTLs over idle TTLs, because idle detection is a heuristic and "nobody has connected recently" is also true of a database that is about to be needed. Best of all, use a platform that enforces the TTL on its own side rather than from your orchestrator, so a crashed pipeline physically cannot leak one. On our platform that reaping is platform-side for exactly this reason, and a database can opt out explicitly with always_on when someone genuinely means it to persist — an explicit opt-out is far healthier than a reaper everyone is scared to enable.
Why does a microVM database take 30 to 90 seconds when a storage branch is nearly instant?
Because they are doing different amounts of work and buying you different boundaries. A storage-level branch writes a pointer into a multi-version storage layer that is already running — the compute may need to start, but no cluster is being created, because the data is reached through a layer shared with every other branch and tenant. A microVM database provisions a machine with its own kernel, attaches a durable volume, bootstraps a PostgreSQL cluster, generates credentials and waits for the server to answer. There is no shared storage layer to point at, which is precisely the property you are paying for: when a backend crashes or an extension misbehaves in there, the list of other things that could notice is empty. Our sandboxes restore from a baked snapshot in about 179 milliseconds at p50, so the 30 to 90 seconds is not VM overhead — it is cluster bootstrap and the readiness check, and we would rather block until Postgres actually answers than hand you a connection string that is not yet a database. The practical consequence is a granularity rule: create one of these per pull request or per migration rehearsal, never per test case. For per-test granularity, use a template database on a cluster that is already running, which is a directory copy and costs essentially nothing.
Why can't I resize a managed database in place?
Because Firecracker cannot change a VM's guest memory when restoring from a snapshot, and every one of these databases is restored from a snapshot. The RAM tier is baked into the template the VM restores from, so it is fixed at create time: 1g, 4g or 16g. That is a real constraint and I would rather state it than hide it behind a vague error message. The supported path is to clone into a different tier — clone(db_id, size="16g") produces a brand-new database id, replayed from the source's own archive, with fresh credentials and its own independent backup stream, while the source keeps running and is never touched. Practically that is a better-behaved operation than an in-place resize anyway: you can verify the new database before you repoint anything, and if the bigger tier turns out not to help you delete it and nothing has changed. The same mechanism does double duty for point-in-time restores — pass target_time instead of size, at least two minutes in the past so the write-ahead log has reached the archive — and for branching a database for an experiment. One operation, three uses, which is usually a sign the primitive is the right shape. Plan the tier at create time if you can; cloning a large database replays WAL and takes minutes, not seconds.
Keep reading
- Top 8 throwaway Postgres platforms — The vendor-shaped sibling: eight products graded on which of three storage mechanisms they actually implement.
- Throwaway Postgres for testing — Goes deeper on the isolation unit inside a test suite — transaction rollback, schema per test, database per worker.
- Best Postgres branching platforms — Shapes 4 to 6 on their own, with the branch-versus-clone distinction taken apart properly.
- Ephemeral databases on PandaStack — What shape 7 looks like as a product: a microVM per database, point-in-time clones, platform-side reaping.
Related posts
- Top 5 Scratch Postgres Platforms for CI (2026)
Before you shop for a disposable-Postgres product: a tuned postgres:16 container plus CREATE DATABASE ... TEMPLATE beats every platform on the board for unit and most integration tests. Five options for the cases where it genuinely doesn't, including the one where my own platform is the slowest thing here.
- Top 8 Ephemeral Development Environment Platforms (2026)
Feature grids do not decide this. Two numbers do: how fast a fresh environment can exist, and what happens to it when nobody is looking. Eight platforms graded on both, with the substrate each one actually isolates with and the catch I would want before the purchase order.
- Top 7 Ephemeral Development Environment Platforms in 2026
An environment is ephemeral when it is created from a definition, nobody is sad when it dies, and the 400th costs the same as the 4th. Most "cloud dev environments" fail at least one of those tests.
- A staging environment per branch, database included
Preview URLs are standard now. Preview databases mostly aren't, so every branch still points at the one shared staging database — which is why your migrations still break each other.
- Ephemeral Databases for AI Agents
Give an agent a run_sql tool and it will use it — including the DELETE it emits while debugging its own step. The fix isn't a better statement filter; it's a database that exists for one task and is deleted when the task ends.
More in Ephemeral databases · See Ephemeral Postgres databases on PandaStack
49ms p50 cold start. Fork, snapshot, and scale to zero.