ProductHow it worksPricingBlogDocsLoginFind Your First Bug
Three parallel test runs writing to one shared database, with one run's leftover row causing another run's assertion to fail
TestingTest Data IsolationFlaky Tests

The Shared-State Problem in Test Environments (and How to Kill It)

Tom Piaggio
Tom PiaggioCo-Founder at Autonoma
The shared-state problem is when parallel tests or preview environments write to one shared database, so one run's data corrupts another's and failures become non-deterministic.

The problem often appears after a test suite starts running concurrently. A test creates an account, another test assumes the account does not exist, and only one of them can be right. Run either test by itself and it passes. Run both at the same time and the result depends on timing rather than the behavior under test.

This is why shared-state failures are so expensive to diagnose. The test code can be unchanged, the application code can be unchanged, and a retry can pass. The hidden input is data written by another actor that the test did not declare or control.

The same failure reaches beyond a single test suite when several pull requests are developed in parallel. Each preview can look healthy on its own while still reading a database, cache, or sandbox state that another PR changed. For validation before merge, a preview needs a declared and private starting state, not a shared baseline that cleanup may eventually restore.

That is why, at Autonoma, we built PreviewKit to operate the per-PR preview lifecycle with a routed environment and database boundary. It does not replace the application's isolation design, but it gives each pull request a place to validate that design before merge.

What is the shared-state problem?

Shared state is any mutable resource that independent test runs can read or change without a boundary between them. The shared database problem in testing is the most common form: two CI jobs, browser tests, or preview environments use the same rows and each assumes a baseline the other can alter.

For example, one end-to-end test signs up test@example.com and expects success. Another begins by checking that the address is unused. Neither assertion is wrong in isolation. Together, they are order-dependent. If the first write completes before the second check, the second test fails even though the application may be working correctly.

Cleanup helps remove data after a run, but cleanup is not isolation. It can be delayed, interrupted, or incomplete, and it cannot stop two active runs from observing each other's writes. Isolation makes the starting state private before a collision can happen.

Run ACreates test userWrites firstRun BExpects no userAssertion failsApplicationSame endpointSame baselineShared databasetest@example.comRow already exists

Both tests touch the same application path, but their expectations conflict because the database is a shared baseline rather than a private test fixture.

Why does shared state make tests non-deterministic?

A deterministic test has a known starting condition and an outcome that follows from the code and declared fixture. A shared-state test has another variable: which run wrote first, which cleanup finished, or which process changed a row between the setup and the assertion. The same test can pass alone, fail in a full suite, and pass on retry without any code change.

Race conditions are one form of this failure. If one test reads before another commits, it observes one state. If it reads after the commit, it observes another. Leftover rows create a similar effect over a longer interval. Both problems make a failure look intermittent when the missing isolation boundary is the stable cause.

How to avoid shared state in test environments

There are three remedies. They differ by where the boundary sits and which part of the system can enforce it.

Per-run isolation

For a unit or integration test that controls its database connection, run the test inside a transaction and roll it back after the assertion. The test can create or update rows, but the writes disappear with the transaction. The next run returns to the same baseline.

This works only when the test owns the transaction context. A browser test against a deployed application normally does not own the server's database connection, so it cannot safely wrap every server-side write in its own transaction. In that setting, transaction rollback is not a complete end-to-end isolation boundary.

Tenant scoping

Tenant scoping keeps one physical database but gives each run a tenant, workspace, or account identifier. The run seeds rows for that identifier and every relevant query, worker, cache key, and side effect must remain in that scope. Parallel runs can share storage without seeing each other's tenant rows.

This approach is useful when the application already enforces tenant boundaries consistently. Its weakness is the same boundary: an unscoped query, global table, background job, or cache key can expose shared state again. Tenant scoping also does not isolate schema changes, because every tenant still uses the same database schema.

That is the gap between separating rows and validating a pull request independently. Tenant scoping helps only when every relevant query, worker, cache key, and side effect remains inside the boundary.

Per-preview databases

Per-preview databases give each preview environment, pull request, or job its own database instance or branch. The boundary sits below the application, so another run's rows are absent rather than filtered out. This is often the clearest option for deployed end-to-end tests, particularly when a change includes migrations or the application cannot guarantee complete tenant scoping.

The trade-off is lifecycle work. Provisioning must create the database, apply the expected schema, provide safe test data, connect the environment, and tear it down when the run ends. A separate database isolates data and schema, but any shared cache, object store, external sandbox account, or feature-flag configuration still needs its own boundary.

How Autonoma removes shared state from PR validation

The failure pattern here is not only a bad test fixture. It is an operational gap: every PR needs an environment, routing, secrets, services, and database state that another concurrent change cannot mutate. Without that boundary, a passing preview can still be passing against someone else's state.

Our managed preview environments provision, route, and tear down per-PR previews, including full-stack service replication, secrets propagation, environment routing, and database isolation. End-to-end testing runs in that same environment: Planner handles the database state setup needed by a test, and Diffs Agent maintains test cases as each PR changes. PreviewKit is our managed Layer 1 preview-environment product, while teams still need to isolate mutable dependencies such as external sandbox accounts and shared caches.

Transaction rollbackTest connectionWrites roll backConnection boundaryTenant scopingShared databaseTenant A rowsTenant B rowsApplication boundarySeparate databasesPreview A databasePreview B databaseData and schemaEnvironment boundary

Rollback isolates one controlled connection, tenant scoping separates rows through application rules, and a separate database gives a deployed environment its own data and schema boundary.

How do you choose the right remedy?

Choose the smallest boundary that the test can actually enforce. Per-run isolation is efficient for tests that own their connection. Tenant scoping works when isolation is a reliable application invariant. Per-preview databases are the stronger choice when a deployed environment needs its own data and schema. Teams often use more than one: transactions for fast lower-level tests and tenant or database boundaries for deployed flows.

Validate the boundary by running two jobs concurrently with deliberately different data. Each job should read only its own state, and teardown should remove only the resources that job created. This check should include mutable dependencies beyond the database, because an isolated database does not protect a shared email inbox or cache key.

A managed preview-environment platform can take on the provisioning and teardown work when operating isolated databases internally costs more than maintaining the platform. For preview-specific context, see do Vercel previews share the same database and database branching vs tenant isolation.

Make Isolation a Property of the Pull Request

Modern engineering teams need to validate several changes before merge, not serialize work around one shared test environment. The goal is not simply to eliminate a flaky failure; it is to ensure that a PR can prove its behavior against the state it declares, while another PR does the same without either changing the other's result.

PreviewKit makes the environment part of that implementation by operating the per-PR preview lifecycle and its database boundary. The remaining design work still matters: identify shared caches, sandbox accounts, and data fixtures that need their own scope. Once those boundaries are explicit, parallel development and end-to-end validation can reinforce each other rather than create another source of uncertainty.

Frequently Asked Questions

It is the failure mode where independent test runs can read or change the same mutable state. Their outcomes then depend on timing, run order, or data left behind by another run.

No. Cleanup can restore a shared baseline after a run, but it does not prevent concurrent runs from observing or changing each other's state. Isolation creates the boundary before the collision happens.

A separate database is often the clearer choice for deployed end-to-end tests, schema changes, or applications that cannot enforce tenant scope consistently. Tenant scoping can work when the application already applies that boundary to every relevant query and service.

Related articles

Two test runs writing to the same table, one scoped to tenant A and one to tenant B, with a tenant_id boundary keeping each test's rows from being visible to the other

How to Isolate Test Data Per Tenant

How to isolate test data per tenant in a multi-tenant SaaS app: tenant-scoped queries, seed factories, a fresh tenant per test, and clean teardown.

Stacked bar chart showing the annual cost of flaky tests split between CI minutes wasted on reruns and engineer-hours chasing flakes, totaling over one hundred thousand dollars for a twenty-engineer team

The Real Cost of Flaky Tests: $120,000 Per Year

Flaky tests cost a 20-engineer team roughly $120,000 per year in wasted CI minutes and engineer-hours. Here is the full worked dollar model, dated 2026.

Engineer debugging a flaky test in a CI/CD pipeline, showing a red build and a systematic root cause analysis workflow for fixing test flakiness

Fix Any Flaky Test in 30 Minutes With This Debugging Playbook

How to fix flaky tests for good. This debugging playbook maps every flaky test symptom to its root cause and gives you working code to resolve it in 30 minutes.

Visualization of engineering velocity being drained by flaky test reruns in CI/CD pipelines showing the hidden cost of test flakiness on development teams

Flaky Tests Consume 20% of CI Time. Here's the Math.

Flaky tests cost a 50-person team $200-400K/year in wasted CI compute and developer time. The full cost breakdown for your next budget meeting.