Non-production environments routinely cost 3–5x what production costs, and per-PR preview environments are the fastest-growing line item inside that multiple. The pitch is always "unlimited preview environments" — a live URL for every pull request — but at 50 concurrent PRs, the naive version of that promise burns roughly $2,850 a month in compute and databases, most of it while nobody is looking at any of them. Here is the before/after math, with the assumptions stated so you can re-run it for your own fleet:
| Setup (50 live PRs, typical web app + worker + Postgres) | Compute/mo | Database/mo | Total | vs naive |
|---|---|---|---|---|
| A. Always-on, full database each, no cleanup | $1,900 | $950 | $2,850 | — |
| B. + auto-sleep on compute only | $565 | $950 | $1,520 | −47% |
| C. + scale-to-zero database branches | $565 | $285 | $850 | −70% |
| D. + auto-destroy on merge/close | $435 | $220 | $650 | −77% |
Assumptions: 0.02/preview-hour of database compute, previews genuinely used 50 of 168 hours per week, and — without auto-destroy — roughly 30% extra stale environments from merged or abandoned PRs nobody cleaned up. The rest of this post earns each row: where the naive bill comes from, why auto-sleep alone stalls at half the savings, why the database line (not compute) decides whether you clear 70%, and the three defaults a self-hosted per-PR namespace pattern must enforce before "unlimited previews" becomes the fleet's biggest orphaned-resource bill.
The naive bill, line by line
Take a typical team that just wired "deploy every PR" into CI and called it done. Each preview runs an app container, a background worker or sidecar, and its own Postgres — say 0.02 per hour for a small managed database, which tracks the entry-level managed-Postgres pricing bands in 2026. That is $0.06 per preview-hour, running 24/7, because nothing ever told it to stop.
Fifty concurrent PRs is not a big team. It is a dozen engineers with a few open PRs each plus CI churn. At 730 hours a month, 50 always-on previews cost 2,850 a month, or about $34,000 a year, for environments whose median viewer count on any given night is zero.
Note the shape of the bill, because it determines everything downstream: two-thirds compute, one-third database. Any savings lever that only touches compute caps out around two-thirds of the total — and in practice much lower, because compute is the half that sleeps easily while databases, by default, do not.
Auto-sleep: the hours math and where vendors' 60–70% comes from
Auto-sleep (scale-to-zero after idle, wake on traffic) exploits the simplest fact about preview environments: they are used during work hours and dead the rest of the time. A team working roughly 10 hours a day, five days a week genuinely exercises its previews about 50 of the 168 hours in a week — 30% active, 70% asleep. Apply that to the compute line and 565. That is row B, and it is where most teams stop and declare victory at −47%.
The vendor claims of 60–70% savings assume the database line sleeps too. Atmosly's environment-scheduler guidance puts scheduled shutdown of non-prod at 40–60% savings, and Quali's Torque case study describes converting 300-plus always-on environments to ephemeral ones for an over-50% cut in the annual cloud bill worth $1.2M in margin. None of these numbers come from compute alone. They come from treating the whole environment — app, worker, and data tier — as something that can go to zero.
Which makes the sensitivity analysis the most important paragraph in this section. The 70%-asleep figure assumes a single-timezone team with short-lived PRs. Stretch either variable and the savings melt: a globally distributed team with PRs under active review 80 hours a week is at 48% active, and row C's total rises from 1,355 — savings fall from 70% to roughly 52%. Long-lived feature PRs that stay warm for weeks do the same damage from the other direction. Auto-sleep savings are a function of your team's actual active hours, not a sticker on the vendor's box. Measure yours (request logs per preview namespace make this a ten-minute query) before promising finance a number.
There is also a latency price, and honesty requires stating it: a cold preview takes seconds to tens of seconds to wake — container pull and start, plus database resume. For a human clicking a PR link, that is a shrug. For CI-driven end-to-end suites that hit the preview URL the instant it is "ready," it is a flake factory unless readiness gates wait for actual traffic-serving state rather than pod-running state. Sleep is free money with one integration tax: your readiness signal must mean warm, not merely scheduled.
Auto-destroy: killing the 30% orphan drag
Auto-sleep shrinks the hourly burn; auto-destroy shrinks the environment count. They are independent levers and the naive setup has neither. Every merged PR whose environment survives the merge is a small perpetuity: at 44 a month to keep a dead preview warm, or about $13 a month asleep with a sleeping database — still real money when dozens accumulate per quarter, and still consuming IP space, DNS entries, branch databases, and backup retention that all have their own ceilings.
The fix is destroy-on-close wired to the forge event, not to a human remembering: PR merged or closed → delete the namespace, the database branch, the DNS record, and the build cache, in one controller action. Qovery's day-2 guidance for ephemeral environments makes the point bluntly — centralized automation must destroy the temporary namespace the moment the PR merges, because the CronJob-and-runbook alternative is how teams end up hand-auditing namespaces quarterly. The Kubernetes-native vocabulary for this is sleep (scale to zero, keep the namespace) versus auto-delete (remove the namespace after N hours of inactivity), and you want both: sleep for live PRs outside work hours, delete for closed ones, plus a TTL backstop (72 hours of zero traffic is the common default) for PRs that are technically open but practically abandoned.
Row D prices the discipline: removing the 15 stale environment-equivalents takes the fleet from 65 back to the 50 live PRs, and the monthly total from 650. Auto-destroy alone, without any sleeping, would take the naive 2,190 — a 23% cut for one webhook handler. That is the cheapest money in the entire table, and the lever most commonly missing, because every preview-environment demo shows creation and almost none shows deletion.
The database question: full, schema, or branch
Rows B and C differ by exactly one decision — whether the database sleeps — and $670 a month hangs on it. But "the database" is doing a lot of quiet work in that sentence, because there are three materially different things teams mean by a per-preview database, and the cost/risk tradeoff between them dwarfs the compute discussion:
| Strategy | How it works | Cost at 50 PRs | Isolation risk |
|---|---|---|---|
| Full isolation | Dedicated database server (or container + volume) per preview | Highest: full $/hour per PR, and it cannot sleep unless the engine supports it | None: noisy neighbors and schema experiments cannot escape |
| Schema isolation | One shared server, one schema or database-name per preview | Low: one server's hourly cost amortized across all PRs | Medium: a runaway query or migration can starve siblings; cleanup is DROP SCHEMA, which had better be automated |
| Branch-based (copy-on-write) | Serverless Postgres with instant branches; compute scales to zero per branch | Lowest at idle: near-zero when asleep, storage-only for dormant branches | Low: separate branch endpoints with production-like data, but cold-start latency on first query after suspend |
The branch-based row is why the economics changed in the last two years. Neon's model is the reference implementation: branching a database is an instant copy-on-write operation rather than a dump-and-restore, idle branch compute auto-suspends after about five minutes of inactivity and scales to zero cost, and the first connection back pays a cold start of roughly half a second to two seconds. Paired with a Vercel-style integration, every preview deployment gets a matching database branch automatically and the branch dies with the PR. Pricing followed the architecture down — per-branch compute that idles at zero means fifty dormant preview databases cost storage pennies instead of fifty server-hours, which is precisely the gap between rows B and C.
Schema isolation remains the rational choice for stateful edge cases branch-based platforms handle poorly (extensions, exotic Postgres forks, multi-database transactions), and full isolation remains the choice when a preview must survive the shared server's maintenance window. But both are now deliberate, minority choices with a known monthly price — full isolation at this scale costs roughly the entire row-B database line by itself — rather than the accidental default. The failure mode to avoid is defaulting into full isolation per preview because "each environment gets its own database" sounded architecturally clean in the design doc, then discovering it is the single largest line in the preview budget and the only one that cannot sleep.
Defaults a self-hosted per-PR pattern must enforce
Everything above ports directly to a self-hosted platform running one Kubernetes namespace per PR on its own machines — owned hardware changes who you pay, not whether waste exists. Idle previews on Hetzner boxes still consume RAM that could bin-pack tenant workloads, still hold PVCs against finite block storage, and still accumulate DNS and branch-database sprawl. Three defaults, enforced by the platform rather than suggested in documentation:
- Destroy-on-merge, wired to the forge event. Namespace, database branch, DNS record, and build cache deleted by controller action when the PR closes — plus a 72-hour zero-traffic TTL backstop for zombie PRs. This is the 23%-for-one-webhook row-D money.
- Idle-scale-to-zero with a warm-aware readiness gate. Sleep live previews after N idle minutes (start with 30; tighten once you trust wake latency), and make "ready" mean serving traffic, not merely scheduled, so sleeping previews stop flaking CI suites.
- A deliberate database-per-preview strategy, chosen once. Branch-based with scale-to-zero as the default; schema isolation where the workload genuinely needs it; full isolation only with a named owner and an expiry. Never full-isolation-by-default — it is the $670/month decision hiding inside a YAML template.
Add per-namespace cost labels from day one (pr, owner, created-at), because the team that can attribute preview spend per PR is the team that notices the $44/month dead preview in week one instead of during the annual cloud-bill postmortem. Non-production environments costing multiples of production is not a law of nature; it is what happens when creation is automated and destruction is a ticket in someone's backlog.
Preview environments belong in the demo; the cleanup policy belongs in the platform. Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with per-PR environments that clean up after themselves. Star the repo on GitHub or deploy your first app today.



