“It worked in staging” is one of the most common — and most avoidable — categories of deployment failure. A large share of it traces back to a single root cause: staging and production weren’t actually running the same thing, because each environment built its own copy of “the same” code, separately.
The problem with rebuilding per environment
If a CI/CD pipeline, discussed in What Is CI/CD?, rebuilds an application separately for test, staging, and production — even from the identical source commit — there’s no guarantee the result is actually identical each time. Dependency resolution can pull a slightly different transitive package version between builds if versions aren’t fully pinned; build tool versions can differ subtly between environments; timing-dependent build steps can behave inconsistently. Each of these is individually rare, but across enough builds, environments, and dependencies, “rare” adds up to a real, recurring source of exactly the kind of bug that only shows up in one environment and not another.
The alternative: build once, promote unchanged
An immutable build artifact — a compiled binary, a container image, a packaged bundle — is built exactly once, from a specific source commit, and that exact artifact, unchanged, is what moves through every subsequent stage: tested in a test environment, promoted to staging, and ultimately promoted to production. Nothing is rebuilt at any later stage. The twelve-factor app methodology, discussed further in The Twelve-Factor App, names this directly as a core principle: strictly separating the build stage (converting code into an executable bundle) from the release stage (combining that bundle with environment-specific configuration) and the run stage (actually executing it) — and once a build exists, it should never be modified, only combined with different configuration for different environments.
This guarantees something rebuilding-per-environment can’t: what was actually tested in staging is byte-for-byte the same thing that runs in production, not merely “the same source code, rebuilt separately.” Any test result from an earlier environment applies directly and exactly to what ships, because it’s literally the same artifact.
What changes between environments, if not the build
If the artifact itself doesn’t change, what does differ between test, staging, and production is external configuration — database connection strings, feature flag states discussed in Feature Flags Explained, API keys, and other environment-specific values, injected at deploy or runtime rather than baked into the artifact itself. Keeping this strict separation — build artifacts free of environment-specific values, configuration supplied externally — is also a security requirement, not just an architectural one, since baking credentials directly into a build artifact means they travel with it into every environment and every copy, a mistake covered further in Secrets Management in CI/CD Pipelines.
Versioning and traceability
A practical consequence of immutable artifacts is that each one can be given a unique, traceable identifier — a version number, a content hash, a specific container image tag — and that identifier stays meaningful throughout the artifact’s life, since the artifact itself never changes underneath it. This makes it possible to answer, with certainty, exactly which artifact is running in a given environment at a given time, which matters directly during an incident: knowing precisely what changed, and being able to roll back to a specific known-good prior artifact, discussed in the context of deployment safety in Blue-Green and Canary Deployments, depends on artifacts actually being immutable and individually identifiable in the first place.
Key takeaway
Building an artifact exactly once and promoting that unchanged artifact through every environment — rather than rebuilding separately at each stage — closes off a real and common category of environment-specific bugs, and makes test results from earlier environments actually apply to what ships. The environment-specific behavior that’s still needed comes from external configuration supplied at deploy or runtime, not from rebuilding the artifact itself differently for each destination.
This article explains general CI/CD and build practices; specific tooling varies. See our disclaimer.