Skip to main content
Technical Systems

Why Some Systems Can Only Be Tested in Production

Your staging environment is a comforting lie

Some distributed systems can only be meaningfully tested in production. Scale-dependent behavior, timing races, and emergent failures make staging environments structurally insufficient.

Why Some Systems Can Only Be Tested in Production

Testing is often described as the process that tells us whether software works. Run enough unit tests, integration tests, performance tests, and staging deployments, and eventually the system should be ready for the real world.

That model works well until the real world becomes part of the system.

A production application does not run against a clean database with predictable traffic and carefully controlled dependencies. It runs against years of accumulated state, real users behaving in unexpected ways, queues that occasionally build backlogs, external services changing independently, infrastructure operating under genuine load, and dozens of components interacting on timelines nobody completely controls, which is why your SLA is only as good as your dependencies.

Some behaviour exists only under those conditions.

That does not mean testing before production is ineffective, nor does it justify treating customers as test subjects for unfinished software. It means testing provides evidence under particular conditions, and eventually production contains conditions that cannot be reproduced completely anywhere else.

The engineering problem is therefore larger than testing software before deployment. Mature systems also need ways to discover production-only behaviour gradually, observe it clearly, limit its blast radius, and recover when reality violates an assumption.

Every Test Is Really a Set of Assumptions

A test does not prove that software will behave correctly under every possible condition. It shows that the software behaved as expected under the conditions represented by that test.

A unit test may establish that a function produces the correct result for a defined set of inputs. An integration test can show that several components communicate correctly. A performance test can demonstrate behaviour under a particular workload, while staging can verify that a deployment works in an environment designed to resemble production.

Each one replaces uncertainty with evidence.

Each also depends on assumptions.

Unit tests

Does the code behave correctly in isolation?

Integration tests

Do components behave correctly together?

Performance tests

Does the system behave under modeled load?

Staging

Does the application behave in a production-like environment?

Production

Does reality behave like our models predicted?

The final question is different from the others because production is not merely another test environment with more servers.

Production contains history, scale, timing, external change, and human behaviour that test environments deliberately simplify.

A staging database may contain representative customers. It does not necessarily contain eight years of customer history, partially migrated records, unusual combinations of old and new data, abandoned workflows, and every edge case created by previous versions of the software.

A load test may generate the expected number of requests per second. It may not reproduce the exact mixture of interactive requests, scheduled jobs, retries, cache misses, deployment activity, third-party latency, and queue backlogs occurring at 9:17 on a Monday morning.

Those differences matter because complex failures often depend on combinations rather than individual components.

Testing is therefore better understood as evidence gathering. The closer the tested conditions are to the conditions the software eventually encounters, the stronger the evidence becomes.

Production contains the last set of conditions because it contains reality itself.

Production Is Different, Not Merely Larger

It is tempting to reduce production-only behaviour to scale.

A staging environment handles a thousand requests, production handles a million, so perhaps the solution is simply to make the load test bigger.

Scale matters, but it is only part of the difference.

Production accumulates state.

Databases grow. Caches fill and evict. Queues build backlogs and later recover. Long-running workflows overlap with deployments. Old records created by previous application versions continue interacting with new code.

Production also accumulates independent actors.

Users behave differently from synthetic clients. Background jobs execute on their own schedules. Other teams deploy services without coordinating every change with you. Cloud infrastructure is replaced or reconfigured. External providers introduce new limits or experience regional degradation.

These interactions create conditions that do not necessarily appear when the components are tested individually.

Consider a service with a perfectly reasonable retry policy. If a downstream database request times out, it retries.

The database also works correctly. So does the connection pool.

Under normal conditions, the system behaves exactly as intended.

Now increase latency.

Database slows

requests time out

clients retry

database receives more work

connection pool fills

requests wait longer

more requests time out

more retries

No individual component needed to contain a conventional software bug.

The failure emerged from their interaction.

The retry mechanism attempted to improve reliability. The connection pool correctly limited resources. The database continued processing queries. Yet together, under the right timing and load, they created a feedback loop that made the incident worse.

This is emergent behaviour: system-level behaviour that cannot be understood simply by inspecting one component in isolation.

The larger and more distributed the architecture becomes, the more opportunities there are for these interactions.

Time Creates States That Staging May Never Reach

Software diagrams tend to make operations look immediate.

A producer sends a message to a queue, the consumer receives it, and processing continues. A service calls another service, receives a response, and returns to the user.

Production introduces waiting.

Suppose Service A publishes messages faster than Service B can process them for twenty minutes. The queue begins accumulating a backlog.

That backlog changes the system.

Messages that normally arrive seconds after they were created are now thirty minutes old when processed. Data referenced by the message may have changed in the meantime. A user may have cancelled the operation. A deployment may have changed the consumer’s behaviour between the time the event was produced and the time it was processed.

The queue has not merely become slower.

Delay has created new states.

Retries add more timing effects. A request can fail, retry while the original operation is still completing, and create duplicate work. Several clients can retry simultaneously after a dependency recovers, producing a sudden traffic spike precisely when that dependency is least able to handle one.

Caches introduce another clock.

An entry that usually remains hot may expire during an unusual traffic pattern, causing thousands of requests to fall through to the database at once. A cache designed to protect the database can briefly become the trigger for database overload.

Background jobs introduce their own timing.

A nightly process that runs comfortably for two hours may eventually take five hours as the dataset grows. One day it overlaps with morning customer traffic for the first time, and resource contention appears that no isolated test ever reproduced.

The implementation may be unchanged.

Time changed the system around it.

This is why production timing problems are so difficult to model perfectly. You can deliberately test queue backlogs, retry storms, cache expiration, and overlapping jobs, and doing so is valuable.

You still have to know which combinations to test.

Production eventually discovers combinations nobody predicted.

Production State Has a History

Test environments usually have one enormous advantage over production: engineers can control their starting state.

A test database can be recreated. Fixtures can load known records. Caches can be cleared. Queues can start empty. Configuration can be reset to a documented baseline.

Production does not restart from a clean conceptual state every morning.

It remembers.

A customer record created six years ago may have survived five schema migrations. Another record may have been imported from a legacy platform and contain fields no current workflow produces. A long-running account may have permissions accumulated under several generations of authorization logic.

Partial migrations create another source of history. Perhaps 99.9% of records were transformed correctly, while a small population retained an old state because a migration job failed and was restarted.

The new application can pass every test against clean modern data and still encounter one of those records months later.

This explains a familiar class of production bug: the problem affects one customer.

The code path is exercised millions of times successfully. One account consistently fails because its historical state is different.

The software is not necessarily executing different instructions.

It is receiving an input state that the testing process never reproduced.

Data volume can change behaviour too.

A query that performs well against ten thousand rows may behave very differently against hundreds of millions. Index selectivity changes. Query planners make different choices. Maintenance operations take longer. Cache effectiveness changes.

Again, simply copying more test records is not always enough.

The structure of years of production history matters alongside the amount of data.

Testing can reproduce code relatively easily.

Reproducing history is much harder.

External Systems Evolve Without Your Permission

Distributed applications also depend on systems that belong to somebody else.

Payment providers, identity services, messaging platforms, shipping APIs, cloud storage, government systems, banking services, and business partners all evolve independently.

Testing usually handles this through mocks, test doubles, sandboxes, or vendor-provided staging environments.

Those tools are necessary because running every automated test against real production providers would be impractical and often dangerous.

They are still models.

A mock returns the responses you programmed it to return. A sandbox behaves according to the subset of production behaviour the provider chose to reproduce. Neither can guarantee that the live service will always behave exactly the same way.

The production provider may suddenly take five seconds to respond instead of 200 milliseconds. It may begin rate-limiting a traffic pattern that the sandbox never reaches. A regional incident may produce intermittent failures rather than clean downtime.

The provider can also change independently.

Your code may not have been deployed for weeks, yet production behaviour changes because an external dependency released a new version, modified configuration, rotated certificates, introduced a new validation rule, or experienced an incident.

This produces an important operational reality:

Your system can change even when your code does not.

Integration testing can establish that your assumptions were correct at a particular moment, while contract testing can make some of those cross-service assumptions explicit.

It cannot freeze the systems outside your control.

Production therefore remains the place where those assumptions meet the actual dependency.

Scale Can Change the Kind of Behaviour You See

Some production behaviour genuinely does require scale.

A cache that comfortably holds the working set in staging may begin evicting frequently used entries in production. A connection pool that never saturates under test traffic may become a bottleneck when thousands of requests arrive concurrently.

Database behaviour can change as data grows.

A query that scans a small table efficiently may become expensive once the table contains millions of rows. The database optimizer may choose a different execution plan because the statistical shape of the data has changed.

Queues also behave differently when arrival rates approach or exceed processing capacity.

A consumer that processes 100 messages per second appears healthy while producers generate 80. Increase production to 105 messages per second and the system does not merely become 25% busier.

It becomes unstable over time.

The backlog grows by five messages every second. After an hour, 18,000 messages are waiting. After several hours, the delay itself may start affecting business behaviour.

Thresholds like these matter because distributed systems are often nonlinear.

A system can appear healthy at 70% resource utilization and deteriorate rapidly near saturation. Latency rises, timeouts appear, retries create additional work, and queues grow.

The transition from healthy to unhealthy may occur over a surprisingly small increase in load.

Performance testing can and should search for these thresholds.

Production still contains a traffic mix that is difficult to reproduce exactly. Real users do not generate perfectly smooth load, and real incidents rarely wait until every dependency is otherwise healthy.

Scale interacts with timing, state, and failure.

That interaction is the difficult part.

Staging Is Valuable Because It Is Not Production

None of this makes staging pointless.

A good staging environment catches enormous numbers of problems before customers ever see them. It can verify deployment artifacts, database migrations, configuration, service integrations, infrastructure changes, and application workflows in conditions far closer to production than a developer’s machine.

The mistake is expecting staging to provide proof.

To become completely identical to production, staging would need production’s data, traffic, users, dependency behaviour, history, capacity constraints, configuration drift, and ongoing independent changes.

At that point, it would effectively be another production environment.

The practical value of staging comes from its simplifications.

It is safer because traffic is controlled. It is easier to reset because the data is not authoritative. Engineers can deliberately break things because customers are not depending on the environment.

Those properties make staging useful precisely because it is not production.

A mature testing strategy therefore asks a more realistic question:

Which risks can we eliminate before deployment, and which uncertainties can only be reduced after deployment?

Unit tests can eliminate many implementation mistakes cheaply.

Integration and contract tests can catch incorrect assumptions between components.

Performance tests can expose capacity limits.

Staging can catch deployment and configuration problems.

Production then reveals the remaining assumptions that depend on real traffic, real state, real dependencies, and real time.

Production testing is not a substitute for the earlier layers.

It is the next layer.

Production Testing Should Mean Controlled Exposure

Saying that some behaviour can only be tested in production can sound reckless if “testing in production” means deploying an unverified change to everyone and waiting for complaints.

That is not the useful model.

The safer model is progressive exposure.

Software passes the tests that can be run before deployment. It reaches production, but deployment does not automatically mean that every user receives the new behaviour.

Instead, exposure increases as evidence accumulates.

Build

Automated tests

Staging

Deploy

Internal users

Small production cohort

Observe

Increase traffic

Full release

This turns production from a binary destination into another stage of validation.

The change is already running in the real environment, which means it can encounter real infrastructure and real operational conditions. The number of users exposed to its consequences can still remain small.

If the evidence looks healthy, exposure expands.

If it does not, the rollout stops.

This is the principle behind techniques such as feature flags, canary releases, and shadow traffic. They differ in implementation, but all accept the same premise: some uncertainty remains at deployment time, so control how much of the system is exposed while that uncertainty is reduced.

Feature Flags Separate Deployment From Release

Traditional deployment often combines two events.

The new software is installed, and users immediately begin depending on its new behaviour.

Feature flags separate them.

A new implementation can be deployed to production while remaining disabled for most users. Engineers may first enable it for internal accounts, then a small customer cohort, and eventually the broader user population.

That distinction is powerful because the code can encounter the real production environment before the organization fully commits to the new behaviour.

Suppose a new search implementation has passed every automated and staging test.

After deployment, internal users reveal that production queries have characteristics the test dataset never represented. The feature can remain disabled for customers while engineers investigate.

The deployment succeeded.

The release did not need to proceed.

Feature flags are not free. They introduce configuration, branching behaviour, cleanup work, and another source of operational state.

Used deliberately, however, they give engineers something extremely valuable: reversibility.

A surprising production observation no longer automatically requires a full application rollback.

Exposure itself can be changed.

Canary Releases Compare Expectations With Reality

Canary releases use a similar idea at the deployment level.

Instead of routing all production traffic to a new version, a small percentage goes to the new release while the existing version continues serving the majority.

That creates a real-world comparison.

Perhaps 5% of requests use the canary while 95% remain on the established version. Engineers can compare error rates, latency, resource consumption, and relevant business outcomes between them.

If the new version consumes twice as much memory under real traffic, the canary can reveal it before every instance is affected.

If one production-only request pattern triggers errors, the blast radius remains relatively small.

The value of a canary is not simply caution.

It is measurement.

The release process is acknowledging that staging produced strong evidence but not complete knowledge. Production provides additional evidence, and the rollout expands only when that evidence supports the change.

This is a much more realistic model of software confidence.

The system is not declared safe because every pre-production test passed.

It becomes increasingly trusted because observations continue to match expectations.

Shadow Traffic Can Test Reality Without Owning the Result

Some changes are too sensitive to let an experimental implementation control the user-visible result immediately.

A payment decision, recommendation system, search migration, fraud model, or critical business-rule engine may need exposure to real production inputs before engineers trust it.

Shadow traffic provides one way to do that.

A real request is sent to the existing production system as normal. A copy is also sent to the new implementation, but the new system’s response is not returned to the user.

                    Real Request

                ┌────────┴────────┐
                ▼                 ▼
          Current System      New System
                │                 │
                ▼                 ▼
        User gets result     Record result


                              Compare

The new system gets to experience real production inputs, timing, dependencies, and data while the established system remains authoritative.

Engineers can then compare results.

Did both systems make the same decision? Was the new implementation slower? Did it fail for data shapes that never appeared in staging? Does it create unexpected downstream load?

Shadowing does not work safely for every operation.

If the shadow request sends an email, charges a card, modifies inventory, or performs another external side effect, simply duplicating traffic could be dangerous. The shadow path needs to prevent or carefully isolate those effects.

Where that can be done, shadow traffic provides something ordinary staging cannot: production inputs without production authority.

Observability Turns Production Behaviour Into Evidence

Progressive delivery is useful only if engineers can tell whether the new behaviour is healthy.

That makes observability part of the testing strategy.

A canary deployment that receives 5% of traffic provides little safety if nobody can distinguish its error rate from the established version. A feature flag provides limited protection if failures cannot be correlated with the users or requests that received the feature.

Production validation therefore needs measurable expectations.

Engineers may compare latency distributions, error rates, resource consumption, queue depth, dependency failures, or business outcomes. The correct signals depend on what the change is expected to affect.

A new checkout implementation should not be judged solely by CPU usage.

It may be technically healthy while conversion drops because a workflow is confusing or a payment edge case fails.

Likewise, a database migration can preserve successful HTTP responses while quietly increasing query latency and connection pressure.

The point is not to monitor everything indiscriminately.

It is to define what evidence would increase or decrease confidence in the change.

This is where Service Level Objectives and similar reliability measures can become useful rollout boundaries. If the canary causes latency or errors to exceed an acceptable level, exposure should stop rather than continuing because the deployment technically succeeded.

Testing in production without observability is guessing.

Testing in production with controlled exposure and meaningful measurements becomes an engineering process.

Production Failures Should Leave the System Better Understood

If production contains behaviour that cannot be predicted completely, failures will occasionally reveal assumptions nobody knew were wrong.

The important question is what happens next.

Suppose an outage reveals that three independently deployed services share a connection pool nobody realized was a common bottleneck. Fixing the immediate incident matters, but the larger lesson is architectural: those services had a shared failure domain that was not visible.

A retry storm may reveal that several layers each retry the same operation.

A queue backlog may reveal that consumers depend on messages arriving far sooner than the architecture actually guarantees.

A one-customer bug may reveal a historical data state that migration testing never represented.

Each incident adds information to the organization’s model of the system.

That information can then become a new test, alert, load scenario, architectural constraint, runbook, or deployment check.

This creates a useful feedback loop:

Assumption

Testing

Deployment

Production evidence

Unexpected behaviour

New understanding

Better tests and architecture

The purpose is not to celebrate production failures.

It is to avoid wasting them.

If the same class of surprise repeatedly reaches production without changing the testing strategy or architecture, the organization is collecting incidents rather than learning from them.

Good Systems Are Designed for Safe Discovery

Accepting that production contains unavoidable uncertainty changes what good architecture looks like.

Reversibility becomes more important.

A change that can be disabled quickly is safer to explore than one requiring a lengthy rollback. A database migration that supports old and new application versions simultaneously is easier to roll out progressively than one requiring every service to switch at exactly the same moment, especially when modernizing systems where eventually is not acceptable forces the team to preserve business invariants during change.

Isolation matters too.

If a new feature can exhaust resources used by the entire platform, a 1% rollout may still create a large incident. Canary traffic only limits risk when the resources and failure modes are sufficiently contained.

Observability needs to be designed alongside the feature rather than added after something fails. Engineers need to know which version handled a request, which feature flags were active, which dependencies were called, and whether new behaviour changed resource consumption, so correlation and trace IDs become rollout evidence rather than debugging ornament.

Recovery also matters more than the assumption that rollback will always restore the previous world.

Once software has sent messages, changed external state, modified data, or triggered downstream workflows, rolling back the binary does not necessarily undo what already happened.

Safe production learning therefore depends on several capabilities working together:

small exposure, visible behaviour, contained failures, clear decision thresholds, and reliable recovery.

Those capabilities do not make production predictable, but restartability helps keep discovery from turning every partial failure into manual archaeology.

They make unpredictability manageable.

Testing Never Really Ends

Software does not become static when it reaches production.

Traffic changes. Data accumulates. Customers discover new workflows. Dependencies release new versions. Infrastructure changes underneath the application. Yesterday’s safe capacity margin can become tomorrow’s bottleneck.

That means testing cannot really be treated as a phase that ends before deployment.

Some forms of testing belong before production because they are faster, safer, cheaper, and easier to automate there. A basic correctness bug should be caught by a unit or integration test rather than discovered through a canary.

Other questions only become fully meaningful after deployment.

How does this version behave against years of production data? What happens under the actual traffic distribution? Does the new implementation interact safely with real dependencies? Does the feature change customer behaviour in a way synthetic tests could not predict?

Those are not excuses for inadequate pre-production testing.

They are different questions.

The strongest engineering process asks each question in the cheapest and safest environment capable of answering it, then continues gathering evidence as the software moves closer to reality.

By the time a change reaches production, uncertainty should already be much smaller than when development began.

It will rarely be zero.

That is why some systems cannot be completely tested without production. Distributed software eventually depends on combinations of real scale, historical state, timing, independent services, external change, and human behaviour that no staging environment can reproduce perfectly. The answer is not to trust production blindly or abandon pre-production testing. It is to treat deployment as controlled learning: expose changes gradually, observe real behaviour, contain failures, preserve reversibility, and turn every unexpected result into better evidence for the next release. Testing does not prove that reality will cooperate. It gives us enough evidence to meet reality with progressively greater confidence.