all posts

Top 12 Disposable psql Platforms for Staging in 2026: Graded on Fidelity to Production

Ajay Kumar··10 min read

A staging database fails differently from a CI database. CI databases fail fast and loudly: the migration runs or it does not, the suite is green or it is red, and either way the whole thing is gone in four minutes. A staging database fails on a Tuesday, three weeks in, at 02:40, when nobody is watching.

The specific Tuesday I have in mind went like this. A long-lived staging app held two hundred and six connections open, nearly all of them idle. A backfill started an `ALTER TABLE` that needed a full rewrite of a forty-million-row table. A logical replication slot left over from a change-data-capture experiment three sprints earlier had been quietly pinning WAL since week one. And the platform's idle-suspend timer, which is the feature the team had bought the product for, had never once fired, because something was always talking to the thing. Nobody noticed any of it until the invoice in March, which is when a database named `staging-tmp-2` became a budget conversation.

None of that is a CI problem. CI wants fast and empty. Staging wants a database that behaves like production when a real application is pointed at it for days: the same extensions, the same `postgresql.conf`, the same connection ceiling, the same WAL mechanics, the same answer when someone asks for a copy of last Thursday morning. And then it wants to go away by itself when the sprint ends, which is the one thing production must never do. Those two desires are in direct tension, and every product below resolves it differently.

I build PandaStack, which has a managed Postgres product in it, so grade my entry with suspicion. My worst answer on this axis is a big one and it is near the end: you do not get a superuser and you do not get a shell, deliberately, and there is an incident behind that.

The short version, before the list. If staging exists to test something that lives in `postgresql.conf` or in an extension nobody has allowlisted, no managed product will fully satisfy you — rent a machine. If staging exists to hold a realistic copy of production data for a sprint, every serious product here can do that and the differences are in how long the copy takes and whether it becomes a second permanent line item. And if staging must cost nothing between sprints, you are shopping for a suspend timer, in which case go and find out what resets it, because the answer is usually your own dashboards.

Six tests, chosen before the list

  • Privilege and configuration. Do you get superuser, or a managed subset with a vendor-shaped name? Concretely: can you run `ALTER SYSTEM SET shared_buffers`, add a library to `shared_preload_libraries`, and restart into it? Prod-fidelity bugs live in planner behaviour and memory settings, and those are configuration, not SQL.
  • Extensions. Not "does it have pgvector" — can you install an extension the vendor has not allowlisted, including one you compiled yourself five minutes ago? There are exactly three honest answers: a long allowlist, a short allowlist, and `make install`.
  • Two hundred idle connections, overnight. A staging app with a connection pool is the normal case and it is also the case nobody tests. Each idle Postgres backend is an OS process with its own private memory. A pooler changes that arithmetic but imports transaction-pooling semantics your ORM may not survive. And a pool that never fully disconnects is the single most common reason scale-to-zero never happens.
  • A realistic dataset. Load eighty gibibytes and watch whether the storage layer behaves like a disk or like a billing meter. Separated-storage architectures are a joy right up until staging does a full table rewrite and you learn that you pay per byte written, per byte read, and for the branch somebody forgot in April.
  • A copy at an instant, handed to a teammate. Branching and point-in-time restore are the same feature wearing different clothes. What matters: how long it takes, whether it touches the source, whether the copy gets its own credentials and its own backup chain, and whether the copy is now a second thing that bills forever.
  • Long DDL and replication slots. A forty-minute `ALTER TABLE` has to survive whatever the platform does to compute while it runs. An abandoned logical slot must not silently fill the disk. These two are where "it is real Postgres" is either load-bearing or a marketing sentence.
Everything below about other people's products is deliberately qualitative: shape, fit, and one honest drawback. No latencies, no prices and no limits I did not measure myself. This category reprices and re-architects constantly and at least three entries have materially changed what they are in the last two years. Verify every specific against the vendor's current documentation before you commit — and that includes the PandaStack row.

The twelve, in cohorts

Branch-first serverless: Neon, Xata, Nile

Neon separates storage from compute and makes branching the headline. On the copy-at-an-instant test it is the strongest shape in the list: a branch is a copy-on-write operation at the storage layer rather than a dump and a restore, so handing a teammate a copy of this morning is cheap in both time and bytes. Scale-to-zero is native rather than bolted on. The fidelity caveats are the mirror image: there is a privileged role but it is not the server's superuser, extensions are an allowlist rather than a compiler, and configuration exposure is a subset. If staging exists to test your application against realistic data, that trade is excellent. If staging exists to test something at the storage layer, remember that the storage layer is the part that is not stock Postgres.

Xata has been re-architected toward Postgres-native with branching plus data anonymisation applied when you copy. That second half matters more than people expect, because the thing that actually blocks staging-from-production is almost never bytes — it is personally identifiable information and whoever owns your data-protection impact assessment. A product that anonymises on the copy path is solving the real blocker. This is also the entry whose shape has changed most, so verify the current privilege and extension matrix against their docs rather than against anything, including this sentence.

Nile reshapes Postgres around tenants. If your product is B2B multi-tenant, a staging database that can materialise one customer's tenant is a far better fit than a generic full copy, and the isolation model is the product rather than an add-on. It is not the pick when staging means "all of production, behaving normally", and it is not where I would go for raw configuration fidelity.

The Postgres companies: Crunchy Bridge, Aiven, Timescale Cloud

Crunchy Bridge is the closest thing in the managed aisle to renting a Postgres machine from people who send patches upstream. There is less magic in the way, which on a fidelity axis is the entire feature, and the list of things it withholds is short. Verify the current privilege matrix, because "broad admin access" and "superuser" are not synonyms anywhere in this category. What it is not is disposable: it is priced and shaped like a server you keep, so the disappearance test is yours to solve with a teardown job.

Aiven has the strongest configuration-as-API story here. A large number of server parameters are exposed as service settings you can set programmatically, which is exactly the right answer if your fidelity problem is parameters rather than privileges — and for most teams it is. It also forks a service to a timestamp, which is a real copy-at-an-instant primitive. No superuser, and extensions are an allowlist. If your staging gap is "prod has `work_mem` at 64 MB and staging does not", this cohort closes it.

Timescale Cloud earns its slot on one condition: if your queries depend on hypertables, chunk exclusion and compression, then staging must have hypertables, chunk exclusion and compression, and testing on vanilla Postgres will produce confident wrong conclusions about your query plans. Forking a service exists. It is not a general-purpose disposable Postgres and does not try to be.

The hyperscalers: RDS and Aurora Serverless v2, Cloud SQL

RDS has the best configuration-control answer in the managed half of this list, and it is unfashionable to say so. Parameter groups are real: you can change most of `postgresql.conf`, including `shared_preload_libraries`, and reboot into it, which means a staging instance can be made genuinely parameter-identical to a production instance. The ceiling is that `rds_superuser` is not superuser and extensions are an allowlist, so an extension that is not on the list is simply not available, full stop. Snapshots and point-in-time restore give you copies, and the restore is a new instance rather than an in-place rewind. Aurora Serverless v2 adds capacity scaling and the question of whether it reaches zero, and how long resumption then takes, is precisely the thing to verify in your region today rather than from a blog post. The honest failure mode in this cohort is organisational: the instance you forgot is a real line item, and the thing teams actually get wrong is the storage and the snapshots that outlive the instance they were taken from.

Cloud SQL is the same shape with a smaller knob set — database flags are a curated subset of parameters, and `cloudsqlsuperuser` is, again, not superuser. Its clone and point-in-time features are a genuinely good copy-at-an-instant story and land you in a new instance. Same disposability problem, same answer: a teardown job you own, not a hope.

Platform-shaped: Supabase

Supabase is full Postgres with a long built-in extension list and a product assembled around it — auth, storage, realtime, row-level-security conventions. For staging this is either exactly right or far too much, and which one depends on a single question: does your application use the parts that are not Postgres? If it does, those parts are the ones you cannot reproduce anywhere else, and staging anywhere else is staging a different application. Preview branches tied to a git branch exist and fit the per-pull-request pattern well. The role you get is generous by managed standards but still is not the server's superuser, so the configuration test lands where the rest of the cohort lands.

Your own Postgres: Docker or Testcontainers, and a VM

A `postgres:16` container gives you full superuser, your exact `postgresql.conf`, any extension you can bake into an image, and zero idle cost, because when the process dies the database never existed. It fails exactly one of the six tests and it fails it completely: a teammate cannot connect to it, and neither can your staging app at 02:40 on a Tuesday. For CI this is the correct answer and I argue for it at length in Top 11 Ephemeral Postgres Platforms for CI Pipelines: Time to First Connection, and Whether It Is Real Postgres. For staging it is a local fixture wearing a staging costume, and the moment someone needs a URL to share, the costume comes off.

Postgres on a plain VM is the undefeated fidelity champion. Superuser, `make install`, every parameter, every kernel tunable, `pg_basebackup` to anywhere you like, and no vendor's opinion between you and the server. You also own TLS, backups, the correctness of your point-in-time recovery, major-version upgrades, the disk filling up at 02:40, and the invoice, which has never heard of your sprint. Most of the products above exist because somebody added up what that ownership cost in engineer-hours and decided differently. That arithmetic is specific to your team, and if you have one person who actually enjoys this, the VM is a defensible answer.

The microVM answer: PandaStack managed databases

Each managed database here is its own Firecracker microVM with a durable volume rather than an ephemeral rootfs, built from the `postgres-16` template, with 1 GiB of RAM and 8 vCPU baked into the snapshot. Create takes 30 to 90 seconds. The API answers 202 with a status of `provisioning` and you poll `GET /v1/databases/{id}` until it reports `running` with credentials, because "provisioned" and "accepting a connection and willing to run DDL" are different events and only the second one is useful to a migration tool. The RAM tier is chosen at create with `size` — `1g` by default, or `4g` or `16g` — and cannot change afterwards, because Firecracker cannot change guest memory at snapshot restore. The supported resize is to clone with a different `size`, which hands you a new database id.

The isolation is a separate kernel per database, which is the strongest boundary in this list, and the blast radius is honestly one VM: a `VACUUM FULL`, an out-of-memory kill or a runaway recursive CTE is one tenant's problem and not a shared cluster's problem. That is the part I would buy.

Now the configuration test, where I lose points. The role you connect as is `pandastack`, created `NOSUPERUSER` and `NOCREATEROLE` with `CREATEDB` and a connection limit of 200. `postgresql.conf` is a property of the template, not a knob: `shared_buffers = 256MB`, `work_mem = 8MB`, `max_connections = 200`, `wal_level = logical`, `max_replication_slots = 10`, `max_wal_senders = 10`, and `shared_preload_libraries` set to `pg_stat_statements,auto_explain`. You can create the trusted extensions that are present in the image as the owner of your database — `pgcrypto`, `uuid-ossp`, `pg_trgm`, `hstore`, `ltree`, `unaccent` and `vector` are installed already, and `postgresql-contrib` is in the image — and you cannot add anything that needs a line in `shared_preload_libraries`, because you cannot edit the file and you cannot restart the server.

One detail I will volunteer because it is the sort of thing a fidelity probe catches and a feature grid never mentions: `shared_buffers` is 256 MB on every tier. The 16 GiB tier has sixteen gibibytes of guest memory and a 256 MB buffer pool. The host page cache absorbs a lot of that gap in practice, but it is not the same engine as a production server tuned to a quarter of its RAM, and if your staging work is plan-shape work you should know that before you draw conclusions from a timing.

And you cannot get a shell. That is not an oversight, it is a scar. `GET /v1/databases/{id}` returns the sandbox id underneath the database, and for a while nothing stopped a customer from calling `POST /v1/sandboxes/{that_id}/exec` with it — which someone did, in August, to run a cryptominer at roughly 380 percent CPU across three database VMs until both host agents were saturated. The guest-control routes — `/exec`, `/fs`, `/fork`, `/snapshots` — now return 403 on any sandbox that backs a managed database, enforced independently at the control plane and again at the agent, because the agent is reachable by anything holding a node token. The consequence for an honest customer is the one above: configuration is a template property. If you need it to be a knob, the right product is a plain sandbox where you run your own Postgres and have root, not the managed one.

On the copy test it does well. `POST /v1/databases/{id}/clone` builds a new database id from the archive, optionally with a `target_time` in RFC3339 that must be at least two minutes in the past, because the archive trails live writes, and optionally with a different `size`. The source is never touched, the clone gets fresh credentials and its own independent backup chain, and because it rebuilds from the archive it can land on any healthy host. There is also a branch, which reflinks the running parent's live volume during a brief pause on the parent's own host: seconds instead of a WAL replay, at the cost of being pinned to that host. `POST /v1/databases/{id}/reset-credentials` rotates the password and the broker token synchronously and disconnects everyone holding the old ones, which is the correct behaviour for a staging database that was shared in a Slack thread in July.

On disappearance it does what the category promises: idle databases auto-suspend and wake on the next connection, and `always_on` turns that off for the ones that must not. Connections are `postgres://pandastack:<password>@<id>.db.pandastack.ai:5432/pandastack` with TLS required, routed by SNI so the hostname is the route and the UUID in it is part of the credential. There is a pooled URL on port 6432 that goes through the VM's own PgBouncer in transaction mode, an optional IP allow list and per-database connection rate limit in front of it, and an HTTP query broker for edge functions that cannot hold a socket open.

The rough edges, stated plainly: no read replicas. No multi-availability-zone failover story beyond rebuilding on a healthy host from the archive, which is what `POST /v1/databases/{id}/failover` does — minutes, with a recovery point measured against the last archived WAL rather than seconds with zero loss. And a running database is pinned to a host. For a staging database that trade is usually fine; for production, read Backups you haven't restored aren't backups and decide on purpose rather than by default.

Twelve disposable psql options graded on staging fidelity: whether you control the server's configuration, what extensions you may install, how you take a copy at an instant, and what the thing costs while nobody is using it. Qualitative by design — verify every row against current vendor documentation, including the last one.
PlatformSuperuser / config controlExtensionsCopy a point in timeIdle cost
NeonPrivileged role, not superuser; config subsetAllowlistBranch, copy-on-write, fastScales to zero natively
XataPostgres-native; verify privilege matrixVerify current listBranch, with anonymisationVerify current model
NileTenant-shaped rather than server-shapedVerify current listTenant-scoped copiesVerify current model
Crunchy BridgeClosest to admin on a real serverBroad; verify the listPITR into a new serverBilled like a server
AivenNo superuser; many params as service settingsAllowlistFork a service to a timestampBilled while it exists
Timescale CloudNo superuser; time-series tuning exposedAllowlist plus TimescaleDBFork a service to a timestampBilled while it exists
RDS / Aurora Serverless v2rds_superuser; parameter groups are real controlAllowlist, incl. preload libsSnapshot and PITR to a new instanceCapacity floor; verify zero
Cloud SQLcloudsqlsuperuser; flags are a curated subsetAllowlistClone and PITR to a new instanceBilled unless stopped
SupabaseGenerous role; the server is not yoursLong built-in listPreview branches, git-tiedProject-shaped; verify
Docker / TestcontainersFull superuser, your own conf fileAnything you bake in the imageVolume snapshot or pg_dumpZero; it stops existing
Postgres on a VMSuperuser, every parameter, every tunablemake install, anything at allpg_basebackup plus WAL, you own itThe VM bills forever
PandaStack managedNOSUPERUSER role; conf baked in the templateTrusted contrib plus pgvectorclone with target_time, or branchAuto-suspends, wakes on connect

Two hundred idle connections, overnight

This is the test that separates a staging database from a CI database, and almost nobody runs it before buying. The arithmetic first. A Postgres connection is an operating-system process with private memory, and `work_mem` is charged per sort or hash node in a plan rather than per query, so a handful of concurrent analytical queries can each allocate it several times over. Two hundred connections against a 1 GiB guest with `shared_buffers` at 256 MB is not a configuration anyone would choose on purpose; it is a configuration your staging app arrives with, because its pool size was copied from production and production has sixteen times the RAM.

A pooler fixes the arithmetic and imports a different problem. PandaStack's pooled URL is PgBouncer in transaction mode with a default pool size of 25 and a client ceiling of 500, and `server_reset_query` set to `DISCARD ALL`. In transaction mode, server-side prepared statements, session-level `SET`, advisory locks held across statements and `LISTEN`/`NOTIFY` do not behave the way your application assumes, because the server connection underneath you changes between transactions. Several popular ORMs and drivers use server-side prepared statements by default, so the correct sequence is: turn them off, or use the direct URL and accept the connection cost. Discovering this on staging is the entire point of staging; discovering it in production because staging used the direct URL and production used the pooled one is the exact failure this test exists to prevent.

Then the part that surprises people, and which I learned by shipping it wrong. Any idle detector that asks "is anybody connected" or "is WAL moving" is defeated by something that holds a connection and ticks. A pooler's keepalive counts. A monitoring agent running `SELECT 1` every ten seconds counts. A walsender draining a logical replication slot counts — we had small databases that should have slept staying awake for exactly that reason. If you are buying a suspend timer, the only question worth asking is what resets it, and the answer is frequently your own observability stack, which nobody thinks of as traffic.

If staging never suspends, the timer is probably not broken. Before filing a bug, run `SELECT state, count(*) FROM pg_stat_activity GROUP BY 1` and look at `pg_replication_slots`. The second query is also how you discover why the volume is full: an inactive logical slot pins WAL indefinitely, and on a small tier that becomes "the database stopped accepting writes" long before anyone suspects a slot from an experiment three sprints ago.

The forty-minute ALTER TABLE, and the slot nobody dropped

A full-rewrite `ALTER TABLE` is a single transaction holding an `ACCESS EXCLUSIVE` lock while it writes an entire new heap and all the WAL that implies. Four things kill it, and all four are platform behaviours rather than Postgres behaviours: a compute scaling or migration event that restarts the backend; a suspend timer that counts "no new queries" rather than "no open transaction"; an idle timeout in a proxy or load balancer sitting between your client and the server; and a disk filling with the WAL the rewrite itself produced. Test this deliberately, with `statement_timeout` set to zero and a table big enough to take half an hour, on the platform you are evaluating, before a release night tests it for you.

Replication slots are the companion footgun. `wal_level = logical` is a fidelity feature — it is what lets you test change-data-capture, logical replication and a Debezium-shaped pipeline against staging at all — and it is also a loaded gun pointed at your volume. An inactive slot retains WAL forever by design, because that is the promise a slot makes. On a small-tier database with ten slots of headroom, one forgotten slot from a CDC experiment will fill the durable volume, and the symptom presents as write failures rather than as a slot problem. `pg_drop_replication_slot` is one line, and it belongs in whatever script tears your staging environment down.

A disposable staging database, end to end

# A disposable staging database, in four calls. Nothing here is magic;
# the only subtlety is that "provisioning" is not "ready".
API=https://api.pandastack.ai
AUTH="Authorization: Bearer $PANDASTACK_API_KEY"

# 1. Create. Returns 202 with {"id": "...", "status": "provisioning"}.
#    size is the RAM tier: "1g" (default), "4g", "16g". Baked at create.
DB=$(curl -sS -X POST "$API/v1/databases" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"label":"staging-sprint-41","size":"4g"}' | jq -r .id)

# 2. Poll until Postgres accepts a connection and will run DDL (30-90s).
#    A migration tool that connects one second early does not retry.
until [ "$(curl -sS "$API/v1/databases/$DB" -H "$AUTH" | jq -r .status)" \
  = "running" ]; do sleep 3; done

# 3. Connect. TLS is required and the hostname is the route (SNI).
INFO=$(curl -sS "$API/v1/databases/$DB" -H "$AUTH")
psql "$(jq -r .connection_url <<<"$INFO")?sslmode=require" \
  -c 'select current_setting($$server_version$$);'

# 4. A copy of 09:30 this morning, as a brand-new database id.
#    target_time must be at least 2 minutes in the past, because the
#    archive trails live writes. The source is never touched.
curl -sS -X POST "$API/v1/databases/$DB/clone" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"label":"staging-at-0930","target_time":"2026-10-11T09:30:00Z"}'

The probe that tells you what you actually bought

Run this against staging and against production, diff the output, and commit the diff next to your migrations. It takes under a minute, it works on every platform in the table because it is all catalog queries, and it answers the configuration and extension tests empirically rather than from a feature grid.

-- The fidelity probe. Run it against staging AND against production,
-- diff the two outputs, and commit the diff. Every staging-only bug you
-- will ever have is somewhere in that diff.

-- 1. Is the engine configured like prod, or like a demo?
select current_setting('server_version')             as version,
       current_setting('shared_buffers')             as shared_buffers,
       current_setting('work_mem')                   as work_mem,
       current_setting('max_connections')            as max_conn,
       current_setting('wal_level')                  as wal_level,
       current_setting('shared_preload_libraries')   as preloaded;

-- 2. Am I privileged, or privileged-adjacent? rolsuper is the whole
--    argument. Everything else is a consolation prize.
select rolname, rolsuper, rolcreatedb, rolcreaterole, rolreplication,
       rolconnlimit
  from pg_roles
 where rolname = current_user;

-- 3. What can I install without superuser? trusted = a database owner
--    may create it; superuser = only a superuser may.
select name, version, superuser, trusted
  from pg_available_extension_versions
 order by trusted desc, name
 limit 40;

-- 4. Who is holding a connection, and is anything pinning WAL? The
--    second query is also how you find out why the disk is full.
select state, count(*) from pg_stat_activity group by 1 order by 2 desc;

select slot_name, slot_type, active,
       pg_size_pretty(
         pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as wal_pinned
  from pg_replication_slots;

Standing up the whole staging stack

The database on its own is not a staging environment. This is the shape I actually use: one database, one app built from a git branch, the connection strings injected as secrets rather than baked into the repo, and a teardown that lives in a job rather than in somebody's memory.

import os
import pandastack

ps = pandastack.Client(api_key=os.environ["PANDASTACK_API_KEY"])

# A staging database for this sprint. 4 GiB because the dataset is real.
# The tier is baked into the snapshot at create time; the only supported
# resize later is clone(size="16g"), which hands you a new id.
db = ps.databases.create(label="staging-sprint-41", size="4g")
print(db["status"], db["host"])

# If staging data must look like production, do not restore a dump.
# Clone production at an instant: new id, fresh credentials, its own
# backup chain, source untouched. target_time >= 2 minutes in the past.
# db = ps.databases.clone(
#     os.environ["PROD_DB_ID"],
#     label="staging-sprint-41",
#     target_time="2026-10-11T09:30:00Z",
#     size="4g",
# )

# Point the staging app at it. Two URLs on purpose: the direct one is a
# real Postgres session, the pooled one is PgBouncer in transaction mode
# (right for short bursty queries, wrong for anything that SETs state).
app = ps.apps.create(
    "shop-staging",
    git_url="https://github.com/acme/shop",
    git_branch="develop",
    env={"NODE_ENV": "staging"},
)
ps.apps.set_env(app["id"], "DATABASE_URL", db["connection_url"],
                secret=True)
ps.apps.set_env(app["id"], "DATABASE_POOL_URL",
                db["pooled_connection_url"], secret=True)

dep = ps.apps.deploy(app["id"])
for line in ps.apps.deploy_logs(app["id"], dep["id"]):
    print(line)
print(ps.apps.get(app["id"])["url"])

# Teardown belongs in the sprint-close job, not in somebody's memory.
# The invoice has no idea your sprint ended.
# ps.apps.delete(app["id"])
# ps.databases.delete(db["id"])

Where I would not use PandaStack for this

If staging exists to test configuration, do not buy my managed Postgres. You cannot change `postgresql.conf`, you cannot add to `shared_preload_libraries`, and you cannot compile an extension, because there is no shell and that is enforced on purpose in two layers. Use a plain sandbox with root and run your own Postgres in it, or rent a VM. If you need read replicas, or a failover measured in seconds with zero data loss, this is not that product — recovery is a rebuild on a healthy host from the archive, and it is minutes. If your dataset is multiple terabytes, a database pinned to one host's durable volume has a disk-shaped ceiling and a hyperscaler is the adult choice.

Two more. There are no GPUs anywhere on this platform and no GPU passthrough, so if staging has to exercise a model serving path on real hardware, it cannot do that here. And the guest kernel is 5.10, which matters more than it sounds: if what you are staging depends on newer kernel behaviour — recent io_uring semantics, newer cgroup or eBPF features — staging will give you a confident answer about a kernel you are not running in production. Finally, if the honest answer to "who owns staging" is "nobody", no suspend timer will save you from the March invoice. That is solved by an owner, a naming convention and a reaper, and no vendor can sell it to you.

How to choose, in five steps

  1. Write down which of the six tests your staging database genuinely has to pass. Most teams need two of the six and shop as though they need all six, which is how they end up paying for branching they never use.
  2. If your list includes changing `shared_preload_libraries` or installing an unpackaged extension, leave the managed aisle entirely. Rent a machine, or run Postgres inside a sandbox where you have root.
  3. If your list is "a copy of production at an instant, with the personal data removed", shop for branching plus anonymisation — and then test the anonymisation, which is the half that will be wrong.
  4. If your list is "it must cost nothing between sprints", pick a suspend timer and then immediately audit everything that holds a connection: poolers, dashboards, a CDC slot, a health check someone added in a hurry.
  5. Whatever you pick, run the fidelity probe against staging and production on day one and commit the diff. Then schedule the teardown in the same pull request that creates the environment, because the version of you that remembers to delete things does not exist in March.

Frequently asked questions

Do I actually need superuser on a staging database, or is a managed subset enough?

It depends entirely on what you are staging, and the dividing line is sharper than most people expect. Superuser buys you four concrete things: `ALTER SYSTEM` and therefore control of the server's configuration, the ability to put a library into `shared_preload_libraries` and restart into it, installation of untrusted extensions (anything whose control file is not marked trusted, which includes most things that hook into the planner or the executor), and server-side filesystem reach through facilities like `COPY PROGRAM` and server-side `COPY TO FILE`. A managed subset gives you everything else: all ordinary DDL, ownership of your own database, trusted contrib extensions as the database owner, and reads of `pg_stat_statements` if the vendor preloaded it. The heuristic: if the thing you are testing is a query, a migration, an index choice or application behaviour, a managed subset is enough and the privilege question is a distraction. If it lives in configuration, in an unpackaged extension, or in replication plumbing needing more than a replication role, you need the real thing. On PandaStack managed Postgres you get the subset: the role is created `NOSUPERUSER NOCREATEROLE` with `CREATEDB`, `postgresql.conf` is baked into the template, and there is no shell in the VM because guest-control routes are blocked on any sandbox backing a managed database. If you need the real thing from us, use a plain sandbox and run Postgres yourself with root.

Will two hundred idle connections from my staging app's pool break a small managed Postgres?

Usually not break, frequently degrade, and almost always in a way that is misdiagnosed. Each Postgres connection is a process with its own private memory, and `work_mem` is allocated per sort or hash node in a plan rather than once per query, so the memory ceiling is not two hundred times a small number but two hundred times a number that depends on your worst plan. On a 1 GiB-class instance with `shared_buffers` at 256 MB, two hundred mostly-idle connections consume process memory and leave less page cache for the data you read, and staging then feels inexplicably slower than production on the same query. Two practical fixes. First, size the staging pool for the staging instance instead of copying production's pool size, which is the single highest-value change and costs one config line. Second, use a pooler, but understand what you are buying: PandaStack's pooled URL is PgBouncer in transaction mode, and in transaction mode server-side prepared statements, session-level `SET`, cross-statement advisory locks and `LISTEN` do not survive between transactions because the backend underneath you changes. Several common drivers use server-side prepared statements by default, so you must disable them or use the direct URL. And one non-obvious consequence: a pool that always holds at least one live server connection, or a monitoring query on a ten-second interval, will keep an auto-suspending database awake indefinitely. That is not a bug in the timer, it is traffic that nobody counted as traffic.

What is the difference between a branch, a clone and a point-in-time restore?

They answer the same question with three different mechanisms, and the mechanism determines the cost and the constraints. A branch is copy-on-write: the storage layer records that a new database shares the parent's blocks and diverges only where written. Fast, cheap in bytes until the two sides drift, and tied to the storage that provides it. A clone or restore rebuilds from a backup: fetch a base backup, replay WAL forward. Slower and bandwidth-heavy, but fully independent and able to live anywhere. Point-in-time restore is that same replay with an instruction to stop at a specific instant, which is why there is always a lower bound on how recent your target can be — the archive trails live writes. On PandaStack both exist. `clone` rebuilds from the archive, takes an optional RFC3339 `target_time` at least two minutes in the past and an optional different `size` tier, never touches the source, and can land on any healthy host. `branch` reflinks the running parent's live volume during a brief pause on the parent's own host, which takes seconds rather than a replay, and is therefore pinned to that host. Both produce a new database id with fresh credentials and its own independent backup chain. One thing worth internalising across every product in this list: a copy is a new billable object with its own lifecycle, and the forgotten copy is the most common way this category gets expensive.

Can I run a long ALTER TABLE or logical replication on a disposable staging database?

You can, and you should test precisely that before you trust the environment, because this is where disposable products diverge most from durable ones. For long DDL, the risk is never Postgres — a full-rewrite `ALTER TABLE` is just one long transaction holding an `ACCESS EXCLUSIVE` lock while it writes a new heap plus WAL. The risk is the platform: a scaling or migration event that restarts your backend mid-statement, a suspend timer that counts the absence of new queries rather than the absence of an open transaction, a proxy with its own idle timeout between your client and the server, and a volume that fills with the WAL the rewrite generated. Run the real thing once, with `statement_timeout` set to zero, on a table large enough to take half an hour, and watch what the platform does to you. For logical replication, check two properties: whether `wal_level` is actually `logical` and whether you can create a slot at all, since some managed products hand out a replication role and some do not. On PandaStack the template ships `wal_level = logical` with ten replication slots and ten WAL senders, so change-data-capture pipelines can be staged against it. The caveat is the one that bites everyone: an inactive slot retains WAL forever, by design, and on a small tier that fills the durable volume and presents as the database refusing writes rather than as a slot problem. Put `pg_drop_replication_slot` in your teardown script next to the delete call.

How do I stop disposable staging databases from showing up on next quarter's invoice?

Accept first that no feature solves this, because the failure is organisational and the tooling only changes how expensive the failure is. That said, four things measurably help. One, create the teardown in the same change that creates the environment. A pull request that provisions a staging database and does not also schedule its deletion is an incomplete change, and reviewing for that is cheaper than auditing later. Two, label everything with an owner and a date at creation time — on PandaStack that is the `label` field, and a convention like owner-ticket-date turns a mystery line item into a two-minute conversation instead of a forensic exercise. Three, prefer products where a copy is cheap to re-make: a copy you can rebuild quickly is a copy you will delete, while one that took eleven minutes to seed is one everyone hoards. Four, do not rely on a suspend timer as your cost control. Suspension helps — PandaStack auto-suspends idle databases and wakes them on the next connection, with `always_on` to opt out — but it is defeated by anything holding a connection, usually your own monitoring. Audit what touches staging on a schedule. Then, when you have done all four, run a reaper you own: list the databases, compare against the environments your infrastructure code declares, and delete the orphans on a cadence. The reaper is twenty lines and it is the only one of the five that actually works unattended.

Keep reading

Related posts

  • Top 11 Ephemeral Postgres Platforms for CI Pipelines: Time to First Connection, and Whether It Is Real Postgres

    Your CI job does not care about QPS. It cares about two things nobody benchmarks: how long until a connection is actually accepted, and whether the thing accepting it is real Postgres or something wire-compatible. Eleven options graded on those, plus isolation, leak cost and whether a seeded dataset can be reused without re-seeding.

  • 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 9 Temporary PostgreSQL Database Platforms (2026)

    A temporary Postgres is produced in one of four ways — a process on the runner, a schema inside somebody else's server, a copy-on-write branch, or a server of its own. The factory decides everything else, including whether your migration rehearsal means anything.

  • Top 10 Throwaway psql Platforms for Engineers (2026)

    Everyone benchmarks create latency. Nobody benchmarks the part engineers actually wait on, which is the seed. A database you can create in two seconds and must then load 40 GB into is a twenty-minute database -- so grade these ten on the provisioning verb instead.

  • Top 6 Burner Postgres Platforms for Pull Requests (2026)

    A scratch database for a test run dies with the process that made it. A pull-request database has to survive nine days of review, serve a preview app and a human with psql, be found again by the next CI run, and then be deleted by a job nobody owns. Six platforms, graded on the fourth one.

More in Ephemeral databases · See Ephemeral Postgres databases on PandaStack

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.