Skip to content
AhmadKhidir

POST

The lies we tell about staging

Jul 20266 MIN READ

#devops#environments#reliability#ci-cd

Every team believes its staging environment is a faithful copy of production. The staging server runs the same code, connects to the same shape of database, and behaves the same way. That belief lasts until the first staging pass. Then the code works in staging and breaks in production, or works in production and breaks in staging, and the team discovers that the two environments were never actually alike, and the gap was invisible because nobody was measuring it.

The incident where "it worked in staging" is not caused by the code. It is caused by the environment being a lie. The fix is not more testing. It is making the environment honest, which is harder than it sounds, because the forces pushing staging away from production are structural: cost, data, time, and attention. The teams that close the gap are the teams that treat staging as a system to maintain, not a checkbox to check.

The four divergences

The first lie is the data lie. Production has real data: millions of rows, weird edge cases, users who have been around since 2019 with corrupt records that only exist because of a bug from 2020. Staging has a fixture, or a backup from a year ago, or a script that generates fake users. The application behaves differently with real data, and the difference shows up as a production incident with the phrase "we did not have that in staging".

The second lie is the scale lie. Staging runs on a smaller instance, or a shared cluster, or a laptop. The query that is fast on a laptop with a thousand rows is slow on production with a billion, and the index that was missing in staging is missing in production too, because nothing ever ran the query against a production shaped dataset. Scale is not a nice to have. It is a different runtime.

The third lie is the configuration lie. Staging uses different feature flags, different credentials, different third party sandboxes, different secrets. The configuration drift is usually invisible, because the application works with either set, until the day a flag behaves differently and the code path that was never exercised in staging is the one that breaks.

The fourth lie is the traffic lie. Staging has no users, no load, no queues backing up, no retries colliding. The failure modes that come from concurrency and load are absent by definition, because there is nobody on the other end. The team that tests concurrency in staging has to simulate the users, and most teams do not simulate the users.

The honest purposes

The uncomfortable part is that some of these divergences are the point. Staging should not be a full copy of production, because a full copy costs as much as production, and it would be a second production system that nobody watches. The purpose of staging is to catch the failures that can be caught cheaply: integration issues, migration problems, configuration mistakes, the things that a realistic environment with realistic data would surface before the deploy.

The divergence that is never acceptable is the one the team does not know about. Staging can be smaller than production, if the team knows the size difference and accounts for it. Staging can have a subset of data, if the team knows the subset is not representative and tests the gaps elsewhere. The lie is not the difference. The lie is believing there is no difference.

The environment that fights back

The most productive staging environments are the ones that are not static. They get reset regularly, with fresh data from production, so the environment does not drift into a state that has never existed anywhere. They run the same deploy pipeline as production, so the difference between the environments is the data and the scale, not the process. They are the same code, the same configuration source, the same image, the same migrations. The team that deploys to staging with the exact same automation it uses for production has removed an entire class of "works here, breaks there".

The other direction is the staging environment that is also the load test environment. The same cluster, the same data, the same configuration, and then the load test runs against it, and the results mean something, because they measure the real thing instead of a scaled down imitation. The teams that do this discover their capacity problems in a controlled way, which is the entire point of having an environment that is not production.

The smoke test is the minimum

If a team does nothing else, it should smoke test the deploy itself. The classic failure is the migration that works in staging and breaks in production, and the classic cause is that the environments differ in the one dimension that matters for migrations: the data. The smoke test that runs after the deploy, against the production data shape, catching the missing index and the slow query before the users do, is the cheapest reliability investment a team can make.

The smoke test has a cousin that is even cheaper: the environment comparison. Every so often, the team runs the same checks against staging and production and looks at the differences. The exercise reveals the drift: the feature flag that is set differently, the schema that has diverged, the config that was patched in production and never backported. The teams that do this regularly stop being surprised by the gap, because they measure it.

The truth about the environment

The honest way to think about staging is that it is a tool with a specific job: catching the failures that a realistic environment can catch. It is not a mirror. It is not a promise. It is a system with known limits, and the teams that succeed are the ones that know the limits and compensate for them.

The compensation looks like this. For the data gap, use production shaped data, even if it is a subset, and test the edge cases separately. For the scale gap, do load testing against something that resembles production, and do not claim that a laptop test proved capacity. For the configuration gap, source the configuration the same way in both environments, and diff them regularly. For the traffic gap, simulate the concurrency, because nobody else is going to.

The environment as an admission

Here is the uncomfortable summary. Every staging incident is an admission that the environment lied, and the team believed it. The fix is not to trust staging more. It is to know exactly what staging is and is not, to keep it honest with the same automation that makes production honest, and to treat the differences as known risks instead of invisible ones.

Production will always be the environment where the truth comes out. The question is whether it comes out as a surprise, during an incident, with users watching, or as a measured difference, caught in a smoke test, while there is still time. The staging environment that is honest about its limits is the one that gives you the second option. The one that pretends to be production gives you the first, and it always does, eventually.