The ephemeral environment decision record: why the lifecycle model you chose determines your PR review cost surface and your production parity regression gap
The environment lifecycle model, the service dependency scope, and the data freshness contract are infrastructure decisions that are almost never made explicitly — they emerge from a per-PR teardown timer implemented as a CI side-effect with no secondary monitoring, a thin preview environment that runs only the changed service against mocked stubs, and a test data seed refreshed once and never touched again. Three failure patterns: the developer tooling SaaS whose teardown timer broke silently during a CI cluster upgrade, allowing 340 environments to accumulate over three months while the infrastructure bill rose from $1,400 to $18,300 per month; the B2B SaaS whose thin preview environment scope let a file upload feature pass 14 PR reviews before hitting an undocumented 10 MB attachment limit in a service the previews never ran; and the developer productivity platform whose 18-month-old static data seed made a performance optimization benchmark at 450 ms in preview and regress to 14.7 seconds in production.
A 34-person developer tooling company built a platform for engineering teams — code review analytics, deployment frequency dashboards, and DORA metric tracking. The team used Kubernetes on AWS EKS, and in month nine of operating the platform they implemented per-PR ephemeral environments: for every opened pull request, a CI job provisioned a dedicated Kubernetes namespace with the changed service, a PostgreSQL database seeded with test data, and an ingress route at a subdomain of their preview domain. The setup had taken two weeks to build and was well-received by the engineering team — reviewers could click a link in the PR and test the change in a live environment rather than reviewing screenshots or running the service locally.
The teardown mechanism was a GitHub Actions workflow triggered by the pull_request event with types [closed]. When a PR was merged or closed, the workflow ran a kubectl delete namespace preview-pr-{number} command and deprovisioned the environment. The mechanism worked correctly from month nine through month twelve — every merged or closed PR triggered the cleanup, and the team rarely had more than 15–20 concurrent preview environments active at once. The CI cost for the preview cluster was approximately $1,400 per month, which the infrastructure lead noted in the monthly cost review as "higher than expected but defensible given the review quality improvement."
In month thirteen, the team upgraded their GitHub Actions runner infrastructure from self-hosted EC2 runners to a new GitHub-native runner configuration. The migration was a two-day project — updating the runner labels, testing the new runner pool with a staging workflow, and verifying that the primary build and deploy workflows ran correctly on the new runners. The migration succeeded. The team closed the infrastructure ticket and moved on.
The upgrade had a side effect that nobody noticed at the time: the new runner configuration had a different event filter behavior for pull_request events fired from forked repositories and from merged PRs that were immediately followed by a branch deletion. In two specific cases — PRs from contributors who had forked the repository, and PRs merged via the "merge and delete branch" button — the pull_request [closed] event was not delivered to the updated workflow. The teardown job for these PRs never fired.
The accumulation was invisible. The team reviewed the monthly AWS cost dashboard, saw the EKS cluster cost increasing, and attributed it to increased team size and longer-running preview environments during a period of active feature development. At month fifteen, the infrastructure lead ran a kubectl get namespaces and counted 340 active preview namespaces. The oldest was 67 days old — a PR that had been merged on the day of the runner migration. Across 340 namespaces, each running a PostgreSQL instance, a Redis instance, and the tooling service across three replicas, the monthly cost had reached $18,300. The team had been paying for 260 zombie environments for two months.
The incident review identified three gaps. First, the teardown mechanism had no secondary guarantee — it was a single-path CI job with no fallback. Second, there was no monitoring of environment count against open PR count: the 340 active namespaces against 12 open PRs was a 28× ratio that would have been immediately visible in any reconciliation dashboard. Third, no cost anomaly alert existed for the preview environment cost center: the monthly cost increase from $1,400 to $18,300 over two months accumulated across four billing cycles before anyone ran the reconciliation count. The environment lifecycle decision record said: "Each PR creates a preview environment; the environment is torn down when the PR is closed." There was no teardown guarantee mechanism, no secondary reconciliation sweep, and no cost envelope specified.
A 41-person B2B SaaS company built a workflow automation platform for operations teams — document routing, approval chains, and integration with ERP systems. The platform was a microservices architecture with eleven services, and the engineering team had implemented preview environments to allow reviewers to test UI and API changes before merge. After a period of slow environment provisioning (full-stack previews that cloned all eleven services took eleven minutes to become ready), the infrastructure team switched to "thin" previews: each PR environment ran only the service under review, replacing the other ten services with mocked stubs maintained by the platform team. The migration reduced provisioning time from eleven minutes to 90 seconds.
The mock stubs were implemented as per-service JSON fixture files that the mock server loaded at startup. Each mock returned the responses that the calling service expected to receive based on the OpenAPI contract between the services. The mock contracts were updated when a service's API changed — the service owner was responsible for updating the mock fixture at the same time as the API change. In practice, this worked for major API changes that appeared in pull requests and code reviews. It did not work for operational configuration changes — changes to rate limits, payload constraints, authentication requirements, or behavioral flags — that were deployed directly to the staging and production services without associated code changes or PR reviews.
In month sixteen, the platform's notification service added a 10 MB attachment limit on the email notifications it sent on behalf of other services. The limit was added in response to an email provider bounce rate that had spiked after a customer used the platform's approval workflow to route documents containing large embedded images. The notification service team added the limit via a configuration change deployed directly to staging and production — no code change, no PR, no API contract update, no mock fixture update. The limit existed in the real notification service. It did not exist in the mock stub that every other service's preview environment used for notification interactions.
Over the following six weeks, the platform's file upload team built a new feature: users could attach documents to approval workflow items and route them through the existing approval chain. The feature was built across fourteen pull requests — a new storage integration, a new upload API endpoint, a new UI upload component, and integration with the approval routing service that handed off to the notification service for completion emails. Each of the fourteen PRs was reviewed in a thin preview environment. The notification service in each preview was the mock stub. The mock stub had no attachment size limit. Every reviewer who tested the upload flow — eleven engineers across fourteen reviews — received a successful notification in the preview.
The feature was deployed to production on a Tuesday morning. By Tuesday afternoon, the operations team received 23 support tickets from customers whose approval completion emails had failed to send when their attached documents exceeded 10 MB. The notification service was returning a 413 Payload Too Large error that the approval routing service had no error handler for, producing a silent failure: the approval routing service logged the error and continued, marking the workflow as complete, while the customer received no completion email and no indication that the notification had failed. Three enterprise customers escalated, including one whose legal approval workflow had a contractual 24-hour completion notification requirement.
The post-incident review produced two findings. First, the notification service's attachment limit had been added without updating the mock contract, and there was no process requiring operational configuration changes to trigger mock contract reviews. Second, the thin preview environment model had created a systematic gap between what engineers tested and what production ran: any behavioral constraint that lived in operational configuration rather than in code was invisible in every thin preview environment. The environment scope decision record said: "Thin previews run the changed service; other services are mocked from OpenAPI contracts." There was no mock fidelity maintenance process, no service behavioral audit cadence, and no classification of which cross-service interactions required real service inclusion.
A 47-person developer productivity company built a platform for software engineering teams — CI/CD pipeline analytics, build time tracking, test flakiness detection, and deployment frequency measurement. The platform's backend was a Rails application backed by PostgreSQL, and the engineering team had invested heavily in preview environments: each PR received a full-stack preview with all six services, a dedicated PostgreSQL instance, and a test data seed generated from an anonymized production snapshot. The seed had been generated in month four of the platform's operation — eleven months after the platform launched — from a production snapshot at that time. It contained 182,000 repositories, 34,000 active builds, and 8,200 pipeline configurations across the anonymized customer accounts.
The seed had been created carefully. An engineering manager had requested that the seed represent "typical production data so we can catch real bugs in preview." The infrastructure team had taken an anonymized production snapshot, run it through a data scrubbing pipeline, and loaded it into the preview seed template. From that point forward, every new preview environment was provisioned from this seed. The seed was never updated. Over the following eighteen months, the production database grew substantially: the repositories table grew from 182,000 to 1.4 million rows as the customer base expanded, the builds table grew from 34,000 to 2.1 million rows, and the pipeline_configurations table grew from 8,200 to 96,000 rows. The preview seed remained at its month-four values.
In month twenty-one, the team's platform engineering team undertook a performance project to improve the dashboard loading time for the build history view — the most-trafficked page in the platform, used daily by every engineering team to review recent build outcomes. The build history view executed a multi-table query joining the builds table, the repositories table, and the pipeline_run_steps table, with a sort on build completion time and a filter on the authenticated team's repositories. The query had been fast at launch (28 ms at P99) and had degraded gradually over production operation to 380 ms at P99 — still acceptable, but trending toward the team's 500 ms SLO threshold.
Two engineers worked on the optimization over three weeks, testing their changes in preview environments seeded with the month-four data. The optimization introduced a composite index on (team_id, completed_at, repository_id) and rewrote the query to use a covering index scan instead of a multi-table join with a filesort. In the preview environment — with 182,000 builds and 34,000 active builds in the seed — the optimized query completed in 37 ms at P99. The optimization was reviewed in four separate pull requests, each benchmarked in a preview environment with the same seed. All four reviewers saw the 37 ms result. The PRs were merged and deployed to production.
In production, the build history query ran against 2.1 million builds. The composite index was correctly created, and the covering index scan was correctly chosen by the query planner. But the team's largest customer — an enterprise account onboarded in month sixteen with 340,000 builds across 8,200 repositories — had a query result set that exposed an unoptimized sort path: the composite index covered the team_id and completed_at predicates correctly, but the repository_id filter was applied after the index scan rather than as part of it, producing an intermediate result set of 340,000 rows that required an in-memory sort before the final pagination limit was applied. At 182,000 total builds in the seed, no single team had more than 12,000 builds — the intermediate result set was small and the in-memory sort was fast. At 340,000 builds for the enterprise account's team_id, the intermediate sort took 14.2 seconds. P99 on the build history view for this customer was 14.7 seconds.
The incident was reported by the enterprise account's engineering lead at 9:15 AM on the morning after the deployment. The platform team reproduced the issue immediately by querying the production database directly with the enterprise team's team_id. The fix — adding repository_id to the composite index as a third column — reduced P99 for the enterprise account from 14.7 seconds to 31 ms. The fix was deployed within two hours of the incident report.
The retrospective identified the root cause as data volume divergence between the preview environment and production. The seed had been created at month four, when the largest customer team had 12,000 builds. By month twenty-one, the largest customer had 340,000 builds — a 28× difference. The optimization had been tested at a scale that did not represent the outlier case, and the outlier case was exactly the customer who noticed the regression first because their query was the slowest. The post-incident note said: "Our preview data is eighteen months old. We knew it was old. We didn't know it mattered this much."
Structural properties set by the ephemeral environment decision
Three structural properties are determined when a team decides — or fails to explicitly decide — how to implement ephemeral environments: what the environment lifecycle model determines about the cost accumulation surface when teardown mechanisms fail silently, what the service dependency scope determines about the integration failure gap when dependent services are replaced by mocks that do not capture real behavioral constraints, and what the data freshness contract determines about the production parity regression surface when test data volume diverges from production scale. None of these properties are labeled as decisions in the conversations that produce them. The lifecycle model emerges from a CI job that provisions environments and a corresponding CI job that tears them down — without specifying what happens when the teardown job does not fire. The service dependency scope emerges from a provisioning time optimization that replaces real services with mocks — without specifying how mock behavioral fidelity is maintained as the real services evolve. The data freshness contract emerges from a one-time data seed created at startup — without specifying when it will be refreshed or what production parity properties it must maintain over time.
Property 1: The environment lifecycle model and the cost accumulation surface. Per-PR ephemeral environments have a cost rate that is proportional to the number of live environments. When the number of live environments exceeds the number of open PRs — because environments are not being torn down when PRs close — the cost accumulates without any corresponding engineering value. The cost accumulation surface is determined entirely by the lifecycle model's teardown guarantee: a single-path teardown mechanism (a CI job triggered by a PR-close event) has no guarantee of execution if the CI system is unavailable, the event is missed, or the trigger condition changes due to a CI system upgrade. A secondary reconciliation mechanism — a scheduled job that periodically reconciles live environments against open PRs and terminates orphaned environments — provides the guarantee. Without the secondary mechanism, the teardown guarantee is as fragile as the CI system that implements it, and any CI configuration change that affects event delivery creates a gap during which environments accumulate silently. The reconciliation mechanism is not optional in a system where teardown failure has a continuous cost: each orphaned environment costs money for every hour it remains live, and the cost is invisible until the next billing review. The lifecycle decision must also specify the maximum acceptable environment lifetime — a hard upper bound after which the environment is terminated regardless of PR state, typically 7 days, which ensures that abandoned PRs and infrastructure failures never produce environments that run indefinitely. Connect this property to the observability cost governance decision record: the cost anomaly detection model for ephemeral environments is identical to the model for observability telemetry cost — a per-environment cost baseline, a total-environment-count alert (environments per open PR ratio above 2.0), and a rolling-baseline cost alert (aggregate environment cost more than 50% above the 7-day average) detect accumulation events within hours rather than at the next monthly billing cycle. The cost anomaly detection for environments is not an infrastructure team concern — it is an engineering leadership concern that belongs in the same cost governance framework as the observability platform cost, the CI/CD compute cost, and the staging environment cost.
Property 2: The service dependency scope and the integration failure gap. The integration failure gap in thin preview environments is not a function of the number of mocked services — it is a function of the behavioral fidelity of each mock relative to its real service. A mock that accurately captures the real service's current API contract, error modes, payload constraints, and authentication requirements has a small gap. A mock that was accurate when it was created and has not been updated as the real service evolved has an accumulating gap that grows with every undocumented behavioral change made to the real service. The critical insight is that the behavioral fidelity gap is asymmetric: changes made via code pull requests are visible and can trigger mock updates because they go through a review process; changes made via operational configuration — rate limits, payload size limits, feature flags, authentication requirements deployed directly to staging and production — are invisible to the mock update process because they bypass the code review workflow. The service dependency scope decision must address this asymmetry directly: either (a) include real instances of services that have significant operational configuration that is not captured in code, or (b) implement a process that requires operational configuration changes to trigger mock contract reviews. Option (b) is only viable if the team can enumerate all behavioral properties of each service that are configurable at runtime and specify a review trigger for each. For most microservices, this enumeration is practically impossible — the safer default is to include the real service in preview environments when it enforces constraints that the calling service must handle correctly. The service dependency scope decision should classify each dependent service against two criteria: (1) does it enforce constraints that are not fully represented in its OpenAPI schema? and (2) has it been updated in the last 90 days without corresponding mock contract updates? If either criterion is true, the service should be included as a real instance in preview environments rather than replaced by a mock. Connect this property to the API contract testing decision record: consumer-driven contract testing (Pact, Spring Cloud Contract) addresses part of the mock fidelity problem by encoding the consumer's expectations as executable tests that run against the real provider — when the provider changes a behavior that a consumer depends on, the contract test fails before the change is deployed; but contract testing captures API contract properties only, not operational configuration changes that are deployed outside of the code release process; the environment scope decision and the contract testing decision are complementary, not substitutes.
Property 3: The data freshness contract and the production parity regression surface. The production parity regression surface for test data is the set of behaviors whose correctness depends on data volume or data distribution properties that the test seed does not represent. The surface has three dimensions: volume (behaviors that are only incorrect at scale — N+1 queries, unoptimized sort operations, pagination edge cases), distribution (behaviors that depend on the shape of the value distribution — skewed account sizes, outlier entities with 10–100× the data of typical entities), and age (behaviors that depend on data accumulated over time — historical data queries, time-series aggregations, date-range filters that produce different result set sizes as the date range spans more historical data). A test seed that was accurate at one point in time becomes an inaccurate representation of production across all three dimensions as production evolves. The data freshness contract must specify all three: the maximum acceptable data volume gap between the seed and production (a target of 10–25% of current production row counts for the highest-volume tables), the distribution requirements (the seed must include a sample of the top 1% of entities by row count — the largest customers, the most active repositories, the highest-volume pipelines — because production parity regressions surface first against outlier-scale data), and the maximum seed age (a refresh cadence of 30 days for stable applications, 14 days for applications with more than 10% monthly table growth). The data freshness contract is not a data engineering concern — it is an engineering infrastructure concern that must be owned at the same level as the environment lifecycle model and the service dependency scope, because seed staleness has the same consequence as every other production parity gap: features that work in preview and fail in production. Connect this property to the database query optimization decision record: query optimization decisions that are benchmarked against preview data are only valid if the preview data represents production volume and distribution; a composite index optimization that reduces query time from 380 ms to 37 ms at 182,000 rows may be incomplete at 2.1 million rows; the query optimization ADR should specify the dataset size at which the optimization was benchmarked as a required field, and the data freshness contract should specify that performance-critical PR benchmarks require a seed whose row counts are within 2× of current production counts for the relevant tables. Connect to the WhyChose extractor: the ephemeral environment decisions are buried in architecture sessions where teams implemented preview environments without specifying teardown guarantees, in infrastructure optimization sessions where thin environments were adopted without documenting service scope, and in data engineering sessions where test seeds were created once and never assigned a refresh cadence; the extractor surfaces these decisions from your AI chat history before the next environment cost spike or production parity incident reveals their absence.
The ephemeral environment decision ADR: five sections
Section 1: Environment lifecycle model and teardown guarantee. Specify the complete teardown guarantee as a two-mechanism system: the primary teardown trigger and the secondary reconciliation sweep. The primary trigger specification includes the event source (PR-close webhook, pipeline completion event, or manual deprovisioning command), the CI system or event handler that executes it, the expected execution time from trigger to environment termination (typically 2–5 minutes for a Kubernetes namespace deletion), and the failure mode (what happens if the trigger event is not delivered — the environment continues running until the reconciliation sweep terminates it). The secondary reconciliation sweep specification includes the schedule (every 4–6 hours), the reconciliation logic (list all live preview environments, query the source control API for the state of each PR, terminate any environment whose PR is in a closed or merged state for more than 2 hours), the alerting condition for the sweep itself (alert if the sweep fails to complete or if it terminates more than 5 environments in a single run — the latter indicates a backlog of orphaned environments that the primary trigger has been failing to clean up), and the escalation path for persistent orphaned environments (notify the infrastructure team lead, not the on-call engineer, since orphaned environments are not an operational incident but an infrastructure configuration problem). Specify the maximum environment lifetime as a hard constraint enforced independently of both mechanisms: no ephemeral environment may run for more than 7 days; any environment older than 7 days is terminated by the reconciliation sweep regardless of the PR's state. The 7-day maximum handles abandoned PRs, PRs created as drafts and left open indefinitely, and any failure mode where both the primary trigger and the reconciliation sweep have missed an environment. Document the expected monthly cost at current team size and open-PR count: number of engineers × average open PRs per engineer × average PR lifetime in days × daily cost per environment. This figure establishes the cost envelope within which the preview environment system is expected to operate, and any deviation of more than 50% from this envelope should trigger an immediate environment count audit. Connect to the CI/CD pipeline decision record: the teardown mechanism is part of the CI/CD pipeline's environment management responsibility; the pipeline decision should specify that environment creation and environment teardown are treated as paired operations with equal reliability requirements, and that CI system upgrades, runner migrations, and event handler changes must include a preview environment count reconciliation before and after the change to verify that teardown guarantees are preserved.
Section 2: Service dependency scope and mock fidelity maintenance. Classify each service that the preview environment's primary service interacts with into one of three categories: included-real (the service runs as a real instance in the preview), mocked (the service is replaced by a mock stub), or shared-staging (the preview connects to a shared staging instance of the service). The classification for each service should be documented with the rationale, not just the decision: why is this service classified as mocked rather than included-real? The acceptable rationale for mocked is: (a) the service has no behavioral constraints that differ from its OpenAPI schema, (b) the service has no operational configuration that is deployed outside of code pull requests, and (c) the mock has been verified to match the real service's behavior within the last 30 days. If any of these three conditions is not true, the service should be reclassified as included-real or shared-staging. Specify the mock fidelity maintenance process: a quarterly review that checks each mocked service against the real service's current behavior, including operational configuration changes deployed in the prior quarter. The quarterly review should be owned by the service owner of each mocked service, not by the preview infrastructure team — the service owner has context on what has changed and is responsible for updating the mock contract when they change the service's behavior. The review should produce a signed-off artifact (a comment in the mock contract file or a checked item in a review checklist) documenting that the mock contract is current as of the review date. Any mocked service that has not been reviewed in more than 90 days should be automatically reclassified as included-real until the review is completed. Connect to the service mesh decision record: if the service mesh enforces mutual TLS and service-to-service authentication in production and staging, the mock stubs in preview environments must also implement the authentication protocol or the preview environment will not accurately test authentication failure modes; the service dependency scope decision should specify whether mocks implement the full authentication protocol or a simplified preview-only variant, and document the failure modes that are not testable in preview as a result.
Section 3: Data freshness contract and volume specification. Specify the test data seed as a decision with three explicit components: the target data volume relative to production, the required distribution properties, and the refresh cadence. The data volume target: for tables that are queried by the services running in preview, specify a minimum row count as a percentage of current production row counts (10–25% is a practical range that makes volume-sensitive regressions visible while keeping provisioning time under 5 minutes) or as an absolute minimum (whichever is larger — do not let the percentage fall below a meaningful floor as production scales). Specify the distribution requirement explicitly: the seed must include a sample of the largest entities in each entity class — the top 1% of customer accounts by row count, the top 1% of repositories by build volume, the top 1% of pipelines by execution count. The top-1% sample requirement is the most important distribution property because production parity regressions surface first against outlier-scale data: a query that is optimized for the median entity (12,000 builds) may be unoptimized for the 99th percentile entity (340,000 builds), and the 99th percentile entity is exactly the customer who will notice the regression first because their queries are the slowest. Specify the refresh cadence as a calendar rule: generate a new seed from an anonymized production snapshot every 30 days for stable applications, every 14 days for applications with more than 10% monthly row growth in any table that is queried in preview. Automate the refresh: the seed generation should run as a scheduled job that produces a new seed artifact at the specified cadence, runs a validation check (verifies row counts, verifies the top-1% sample is present, verifies that no PII has leaked through the anonymization pipeline), and updates the preview provisioning template to use the new seed. An automated staleness check in the environment provisioning step rejects seeds older than 2× the refresh cadence and pages the infrastructure team to run the refresh manually. Document the maximum acceptable seed age in the decision record alongside the reasoning: "Seeds older than 30 days at current growth rates may underrepresent production row counts by more than 25% for the builds and repositories tables; performance benchmarks in preview environments with seeds older than 30 days are unreliable for queries against these tables."
Section 4: Cost envelope and anomaly detection specification. Specify the expected monthly cost of the preview environment system as a calculated figure: number of active engineers × average open PRs per engineer (typically 1.5–2.5 for a healthy development cadence) × average PR lifetime in days (typically 2–4 days) × daily cost per environment. For a 40-person team with 2 open PRs per engineer, 3-day average PR lifetime, and $3/day per environment, the expected cost is 40 × 2 × 3 × $3 = $720/month. Document the expected cost in the decision record and update it quarterly. Specify the anomaly detection policy: (1) an alert that fires when the live environment count exceeds (current open PR count × 2) — this ratio accounts for short-lived re-opened environments and multi-service preview stacks while flagging genuine accumulation; (2) an alert that fires when the aggregate monthly environment cost is more than 50% above the calculated expected cost for the current team size; (3) a weekly report sent to the infrastructure team lead showing environment count, environment age distribution, and aggregate cost, making drift visible before it becomes a billing surprise. The anomaly detection should be implemented as a cloud cost alert (AWS Cost Explorer anomaly detection with a custom cost category for preview environments) rather than as a custom monitoring solution — cloud cost anomaly detection is reliable, requires no maintenance, and catches cost deviations regardless of their cause. The 50% above-expected threshold for the cost alert is chosen to be sensitive enough to catch environment accumulation within the first week (340 environments at $3/day accumulates at $1,020/day; the expected weekly cost at team scale would be $720 × 7 / 30 ≈ $168; the accumulation would produce a 6× anomaly visible within two days) while not being so sensitive as to fire on normal variation from sprint peaks and PR batch merges. Connect to the alerting threshold decision record: the cost anomaly alert threshold for preview environments is an alerting threshold decision that should be documented alongside the other alerting thresholds in the system — the cost alert fires as a P3 notification to the infrastructure team lead, not as an on-call page, because it represents a cost governance issue rather than a production incident.
Section 5: Production parity review and environment scope evolution. Specify a quarterly production parity review for the preview environment system: a review that assesses whether the current lifecycle model, service scope, and data freshness contract are producing preview environments that are representative enough to catch the failure modes that have occurred or are likely to occur. The review has three inputs: (1) the incident history for the quarter — any production incident caused by a failure mode that the preview environment did not catch is documented as an environment scope gap, classified by whether the gap was caused by a service not included in the preview, a mock fidelity failure, or a data volume divergence, and used to update the scope or data freshness contract; (2) the environment cost report for the quarter — any cost anomaly that occurred is reviewed to determine whether the lifecycle model's teardown guarantee needs strengthening; (3) a sample of 10 PRs from the quarter — reviewer confirmation that the preview environment was useful and that changes were caught in preview rather than in production. The quarterly review produces one or more scope updates if gaps are identified: adding a service to the included-real classification, updating the mock fidelity certification for one or more services, reducing the maximum seed age, or strengthening the teardown guarantee. The review is not a post-mortem — it is a proactive governance process that prevents the environment system from drifting into a state where it provides a false confidence: environments that look like they provide production coverage but have accumulated scope gaps that make failure modes invisible until they reach production. Connect to the observability cost governance decision record: the preview environment system has the same cost governance properties as the observability platform — a baseline cost that is justified by the engineering value it provides, a set of anomaly conditions that indicate the system is being used incorrectly, and a governance review cadence that keeps the cost within the expected envelope; the cost governance model for preview environments belongs in the same infrastructure cost review as the observability platform cost, not in a separate process owned by the infrastructure team in isolation from engineering leadership visibility.
FAQ
How do you guarantee that per-PR ephemeral environments are torn down reliably?
Guaranteeing teardown requires two independent mechanisms: a primary teardown trigger and a secondary reconciliation sweep. The primary trigger is a CI job or webhook handler that fires on PR-close or PR-merge and terminates the environment immediately. The secondary mechanism is a scheduled reconciliation job that runs every 4–6 hours, queries the source control API for the state of every open PR, and terminates any environment whose PR has been closed for more than 2 hours. The reconciliation sweep is decoupled from the CI system — it does not rely on event delivery and runs independently of CI configuration. Together they create a two-layer guarantee: environments are torn down promptly in the normal case, and orphaned environments are terminated within 6 hours in any failure case. Add a hard maximum lifetime of 7 days enforced by the reconciliation sweep regardless of PR state. Add a cost anomaly alert that fires when the aggregate environment count exceeds (open PR count × 2) — this detects accumulation within hours rather than at the next monthly billing review.
What services should be included in a preview environment, and which can be mocked?
Include as real instances any service that the PR's changed code calls directly in the primary user flow and that enforces behavioral constraints — payload size limits, rate limits, authentication requirements, schema validation — that are not fully captured in its OpenAPI schema. Mock services that provide reference data only (the calling code reads but does not write), are called only after the primary user flow succeeds, and have stable API contracts that have not been updated in the last 90 days. The key question is not "can we mock this service" but "does the mock accurately represent the service's current behavior, including operational configuration changes deployed outside of code pull requests?" A service that has had operational configuration changes in the last quarter without corresponding mock contract updates should be reclassified as included-real until the mock is verified. Review mock classifications quarterly and after any production incident caused by a behavior difference between a mock and its real service.
How do you keep preview environment test data current enough to catch production parity regressions?
Specify a data freshness contract with three components: a volume specification (target 10–25% of current production row counts for the most-queried tables), a distribution requirement (include a sample of the top 1% of entities by row count in each entity class — the largest customers, the highest-volume accounts), and a refresh cadence (every 30 days for stable applications, every 14 days for applications with more than 10% monthly row growth). Automate the refresh as a scheduled job that generates a new seed from an anonymized production snapshot, validates the output, and updates the preview provisioning template. Add an automated staleness check in the provisioning step that rejects seeds older than 2× the refresh cadence. The top-1% distribution requirement is the most critical: production parity regressions surface first against outlier-scale entities, and a seed that represents the median entity size will miss regressions that are only visible at the 99th percentile entity size — which is exactly the enterprise customer whose queries are slowest and who will report the regression first.
What is the right cost model for per-PR ephemeral environments in a team of 20–60 engineers?
Calculate the expected monthly cost: number of active engineers × average open PRs per engineer (1.5–2.5) × average PR lifetime in days (2–4) × daily cost per environment. For a 40-person team with 2 open PRs per engineer, 3-day average PR lifetime, and $3/day per environment, the expected cost is $720/month. Document this figure in the decision record and update it quarterly. Control cost with: (1) shared database instances with per-PR schemas rather than per-PR database instances — reduces RDS cost by 80–90%; (2) idle suspension — stop compute when the environment has received no HTTP traffic in 30 minutes, restart on the next request; (3) the 7-day maximum lifetime hard constraint. Implement a cost anomaly alert at 50% above the calculated expected cost for the current team size, configured in the cloud provider's cost anomaly detection service rather than as a custom monitoring solution, so that cost deviations from any cause — orphaned environments, over-provisioned instances, storage accumulation — are caught within days rather than at the monthly billing review.
Further reading
- CI/CD pipeline decision record — the pipeline design, environment promotion model, and deployment automation that determine when environments are created and what guarantees apply to their teardown; environment creation and environment teardown are paired operations in the pipeline with equal reliability requirements, and any CI configuration change that affects teardown event delivery is a pipeline reliability regression, not an infrastructure maintenance task.
- Observability cost governance decision record — the cost anomaly detection model, the cost attribution approach, and the governance review cadence that apply equally to preview environment costs as to observability telemetry costs; the preview environment cost anomaly detection is not a separate governance problem — it is the same problem of setting a per-service cost baseline, configuring a rolling-average anomaly detector, and owning the alert in an infrastructure cost review meeting.
- Database query optimization decision record — the query profiling model, the test dataset size requirement, and the query plan baseline comparison that determine whether performance optimizations benchmarked in preview environments are valid at production data volume; query optimization decisions are only reliable when the benchmark dataset represents production row counts and distribution, making the data freshness contract a prerequisite for valid query optimization reviews.
- API contract testing decision record — consumer-driven contract testing (Pact, Spring Cloud Contract) as a complement to preview environment service scope decisions; contract testing captures API schema contract properties and detects provider changes that break consumer expectations, but does not capture operational configuration changes deployed outside of code releases; the contract testing decision and the environment scope decision are complementary controls that together reduce the integration failure gap more than either does alone.
- Alerting threshold decision record — the cost anomaly alert thresholds, the environment count anomaly thresholds, and the escalation path classification for infrastructure cost issues; the environment cost alert fires as a P3 notification to the infrastructure team lead, not as an on-call page, and belongs in the same alerting threshold decision record as the observability cost alert and the CI compute cost alert.
- Open-source extractor — find the ephemeral environment decisions buried in your AI chat history: the infrastructure session where the preview system was implemented without specifying teardown guarantees, the optimization session where thin environments were adopted without documenting service scope, and the data engineering session where the test seed was created once and never assigned a refresh cadence.