Your tests pass against a build with debug assertions, development dependencies, verbose errors and a permissive configuration. You then ship a different build, compiled with different flags, containing a different dependency set, behaving differently when something goes wrong.
The gap between those two artefacts is where a specific class of problem lives: things that are safe in the build you tested and not in the one you shipped, or present in the shipped one and never exercised.
This post is what differs and what to check. For whoever owns the release pipeline.
What is actually different
Dependencies. Development and test dependencies are excluded from a production build, which is correct. The failure is the reverse: a development-only package that ends up in the production dependency list because somebody installed it without the right flag. Now a test helper, a mock server or a debugging utility ships, and those are rarely written with hostile input in mind.
Error handling. Development returns stack traces and query text; production is supposed to return a generic message. If the switch is a single environment variable, you have one flag between an internal error message and a detailed disclosure, and nothing tests the production branch.
Debug endpoints and tooling. Profilers, hot reload, in-browser developer panels and admin utilities guarded by a build flag. The guard is usually correct and it is a condition nobody verifies after release.
Configuration defaults. Permissive settings that make development pleasant: relaxed cross-origin policy, disabled certificate validation against a local service, a seeded credential, a shortened token lifetime for convenience.
Optimisation and hardening flags. Compiled languages ship with different flags in release builds, and the security-relevant ones (stack protection, position independence, fortification) are sometimes attached to a build type nobody checked.
Source maps. Shipped to production they hand over readable source, including comments and internal names, which is a reconnaissance gift and occasionally reveals credentials in code that was never expected to be read.
The general rule
Test the artefact you will ship, not a rebuild of the same source.
That means the pipeline produces one artefact, that artefact is what runs in staging, and it is the same bytes promoted to production. If staging is rebuilt from source, you have tested a second artefact that resembles the first and you have no guarantee about the difference.
This is the same argument as pinning by digest rather than by tag, applied one step earlier.
Check what shipped
The useful checks run against the built artefact rather than the repository:
# Container: is a shell, package manager or compiler present?
docker run --rm --entrypoint sh your-image:tag -c \
'command -v sh bash apk apt-get pip npm gcc 2>/dev/null'
# Container: what is actually installed versus what your manifest declares?
docker run --rm --entrypoint sh your-image:tag -c \
'command -v dpkg >/dev/null && dpkg -l | wc -l'
# Web build: did source maps ship?
curl -s -o /dev/null -w "%{http_code}\n" https://app.example.com/static/main.js.map
# Web build: is the production error path actually generic?
curl -s https://app.example.com/api/does-not-exist | head -c 300
# Node: are dev dependencies present in the production install?
docker run --rm your-image:tag npm ls --omit=dev --all 2>&1 | grep -i "extraneous" | head
The source map check takes five seconds and finds something surprisingly often, because bundlers emit them by default and the deployment copies everything in the output directory.
Test the production configuration, at least once
The specific gap worth closing: a test suite that only ever runs in development configuration never exercises the code path that runs for your users.
Run a subset of your tests against the production build with production settings. Not everything, because some of it requires the permissive configuration to work at all, but enough to assert the things that differ: that errors are generic, that the debug routes return 404, that the permissive cross-origin rule is absent.
A handful of assertions, run against the real artefact, catches the flag that was flipped the wrong way.
Make the differences enumerable
The reason this class survives is that nobody can list how the two builds differ. The list lives across a build configuration, a dependency manifest, an environment file and a container definition, and no single person holds it.
Write it down once: every setting that differs between development and production, with the value in each and why. It is a page, it is the artefact a reviewer needs, and producing it usually finds two things that should not differ at all.
The concession
Some differences are necessary and good. Development should have verbose errors and hot reload, production should not, and building one artefact that behaves identically in both would be worse for everyone.
The argument is not for eliminating the differences but for knowing them and testing the production side of each. The failures here are not caused by having a production configuration, they are caused by it being the configuration nobody looks at, because looking at it is inconvenient precisely when it matters.
The implication
Everything you verified was verified against something slightly different from what your users received.
Run the source map check and the error-message check against your live site. Two commands, and they tell you whether the gap between tested and shipped is a gap you know about.