Preview Environments
A running instance per change, created from the pull request and destroyed with it — excellent evidence about wiring and product behaviour, no evidence at all about scale.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
What does giving every pull request its own running environment actually buy, and what does it quietly cost?
Reviewing a change as a diff tells you about the code and nothing about the running system, so integration and product problems are found after merge, in a queue behind everyone else's changes.
Put a preview URL on every pull request. Reviewers click it, see the change working, and approve with confidence.
The confidence extends further than the evidence. A preview environment has a trivial dataset, no concurrency and stubbed dependencies, so a green preview says nothing about the properties that cause outages.
- The confidence extends further than the evidence. A preview environment has a trivial dataset, no concurrency and stubbed dependencies, so a green preview says nothing about the properties that cause outages.
- Previews need credentials. If they get production credentials for convenience, every pull request — including one from a fork or a compromised dependency in the build — becomes a path to production data (Secrets in CI).
- They accumulate. Without enforced teardown, a team ends up running dozens of forgotten stacks and paying for all of them.
- They frequently share a backing database or a third-party sandbox, so one preview's test data pollutes another's, and a destructive test breaks everybody.
- Creating one gets slower as the system grows, until the preview arrives after the review is finished and nobody uses it.
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- A preview environment is an ephemeral instance keyed to a change rather than to a stage: the pipeline builds the branch, provisions an isolated stack, seeds it, and publishes a URL that is torn down when the pull request closes.
- What it genuinely preserves is code paths, integration wiring, configuration plumbing, and the product experience a human can look at. That is a real and underrated set — it catches the class of problem that a diff cannot show.
- What it does not preserve is everything expensive: data volume, concurrency, real dependencies, cost behaviour. It is the leftmost useful instrument, not a small production.
- The isolation boundary is the design decision that determines whether previews are safe. Fully isolated stacks are expensive and safe; shared backing services are cheap and let one preview affect another.
The lifecycle, and where each step fails
Preview environments are the most fully automatable thing in this module, which is why the failures cluster in the steps people leave manual: credentials and teardown.
- 1Build the branch
Produce an artifact from the pull request head, identically to the main build path.
fails by A separate preview build path that diverges from the real one, so the preview tests something you will never ship (Build Once, Deploy Many).
evidence The preview reports the same commit and build identifier as a normal build.
- 2Provision
Create an isolated stack — compute, its own schema or database, its own queue prefix.
fails by Reusing shared backing services, so one preview's migration breaks another's tests.
evidence Two previews running destructive tests simultaneously do not affect each other.
- 3Issue credentials
Mint short-lived, preview-scoped credentials at deploy time.
fails by Reusing a staging or production credential because it was already in the pipeline (Roles vs Static Keys).
evidence The preview's identity cannot read production data, demonstrated by trying it.
- 4Seed
Load synthetic data that exercises the interesting shapes.
fails by Loading a production extract for realism, creating an exposure in the least-controlled environment you run.
evidence The seed dataset is generated by a script in the repository and contains no real identifiers.
- 5Publish
Post the URL on the pull request, behind authentication.
fails by A public URL, indexed and reachable, exposing an unreleased feature.
evidence An unauthenticated request to the preview URL is refused.
- 6Destroy
Tear down on close and on time-to-live expiry, whichever comes first.
fails by Teardown only on close, so abandoned branches leave stacks running indefinitely.
evidence Live preview count returns to near zero overnight.
What a green preview is and is not evidence for
This is the same instrument framing as What an Environment Is For, applied to the leftmost useful environment. The left column is genuinely valuable — these are failures a diff review cannot catch — and the right column is the reason a preview must not be treated as a gate for risky change classes.
| Question | Does a preview answer it? | Why |
|---|---|---|
| Does the feature work end to end? | Yes, for the happy path | Real code, real wiring, a human clicking it |
| Is the new config key plumbed through? | Yes | The service boots with the full config schema (Validate at Startup, Fail Clearly) |
| Does the new dependency get called correctly? | Partly | Against a stub or sandbox, not the real dependency's bad day |
| Does the migration run? | Yes, mechanically | On a trivial dataset — says nothing about duration or locks |
| Will the migration lock the table? | No | Lock duration is a function of row count (Expand, Migrate, Contract) |
| Does it hold up under concurrency? | No | One user, no contention, no races |
| What does it cost per request? | No | No meaningful traffic to measure against (Cost Per Request) |
| Does it degrade gracefully when the dependency fails? | Only if deliberately tested | Requires fault injection; stubs are up by default |
The credential boundary
A preview environment is deployed from a pull request, and a pull request is untrusted input — potentially from a fork, potentially containing a build step someone else wrote. Anything the preview pipeline can reach, that input can reach.
This is why the credential design matters more here than in any other environment, and why "it is only a preview" is the sentence that precedes the incident.
How to do it properly
Most important first.
- Scope preview credentials to the preview environment only, issued at deploy time through workload identity rather than stored as long-lived keys (Workload Identity).
- Seed with synthetic data by default. A preview environment is the least-controlled environment you run; it is the worst place for a production extract (Production Data in Lower Environments).
- Enforce teardown on pull request close and on an absolute time-to-live, so an abandoned branch cannot leave a stack running for a month.
- Give each preview its own schema or database when a full instance is too expensive, so tests cannot collide.
- Treat third-party integrations explicitly: point at a sandbox, a recorded fixture, or a stub, and make it obvious in the UI which one is in use.
- Keep creation fast enough that the preview beats the reviewer. Past a few minutes people review the diff and ignore the link.
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
Contained by isolation, when isolation is real. Where previews share a database, a third-party sandbox or a credential, the effective scope jumps to every other preview — and if the credential reaches production data, to everyone.
What can go wrong
- A preview URL that is publicly reachable and indexed, exposing an unreleased feature or a debug endpoint.
- Shared database, so two previews running migrations at once corrupt each other and both look like flaky tests (Flaky Tests).
- Teardown that runs on pull request close only, so branches abandoned without closing accumulate silently.
- A preview that talks to production dependencies for convenience — sending real emails, charging real cards, writing real analytics events.
- Preview infrastructure that itself drifts from production, so it tests a wiring shape that does not exist anywhere else (Environment Drift).
- "The preview looked fine, so it is safe to ship." The preview covered code paths and wiring. It had no data, no concurrency, and stubs where your dependencies are (Why Local Success Predicts So Little).
- "Previews replace staging." They replace some of what staging was used for — product review and integration wiring. They do not replace rehearsal against realistic data, which staging may or may not have been providing either.
- "It is just a test environment, the credentials do not matter." A preview environment is reachable from a pull request, which is the least trusted input in your system (CI Security).
- "We should keep the preview around after merge for reference." That is how you get a long-lived, unowned, drifting environment. Keep the pull request; delete the stack.
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- Preview creation time is measured, and the median is short enough that reviewers use the link.
- A count of live previews exists on a dashboard and returns to near zero when the team is not working.
- A credential audit shows preview environments hold no credential that grants access to production data (Least Privilege in Production).
- Reviewers cite behaviour they saw in the preview, not just the diff.
- A preview environment is disposable by construction: the rollback is deletion, and it should be cheap enough to do on a whim.
- What does not roll back is anything a preview wrote to a shared system — emails sent, webhooks delivered, rows written to a shared database, events pushed into an analytics pipeline. Those side effects need to be blocked at the boundary, not undone.
- Automate creation, seeding, URL publication, teardown on close, and expiry by time-to-live. All of it is mechanical and none of it should require a person.
- Automate the "no production credentials" check as policy rather than convention (Policy as Code).
- Do not automate the conclusion. A preview passing is not an approval; a human still decides whether the change's risk class is covered by this kind of evidence (Parity That Is Worth Paying For).
- Full isolation per preview multiplies infrastructure spend by the number of open pull requests; shared backing services cut the cost and reintroduce interference.
- Seeding realistic-looking synthetic data is real ongoing work, and stale seed data makes previews progressively less useful.
- The faster you make creation, the more you cut — and each cut moves the preview further from the wiring it was supposed to verify.
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- PLATFORM-SPECIFICFrontend and stateless-service previews are close to solved — several hosting platforms create one per pull request by default. Previews for a system with a large stateful backend are a bespoke engineering effort, because the hard part is producing an isolated dataset quickly, not running the code (Stateless vs Stateful Services).
- SCALE-SPECIFICWith a handful of open pull requests, per-preview isolation is cheap and obviously worth it. At hundreds of concurrent branches, isolation cost dominates and teams move to shared backing services with per-preview schemas — which brings back the interference this lesson warns about, now as a deliberate trade.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.
- — Testing & Reliability Engineering — which test tiers belong on a preview environment and which are wasted there.