How to check environment parity
Parity is a diff of two fingerprints, not a feeling. Print the Node and npm versions, the lockfile hash and every installed dependency version on each environment, then diff the two files. Here two environments whose package.json parses to the same object disagreed on Node, on npm and on semver 7.6.0 against 7.8.5.
Why check this
This page covers runtime and dependency parity: which Node and npm are running, which package versions are on disk, and whether both sides came from the same lockfile. The key by key comparison of .env and config files is a separate check, and it is linked below.
Run this before a staging sign-off is treated as evidence about production, and after any deploy that reinstalled dependencies rather than shipping a built artefact. The failure it prevents is the sign-off that proves nothing: staging passes on one minor version of a dependency, production runs another, and the defect belongs to a version nobody compared.
Prerequisites
- A way to run one command inside each environment. A deploy hook, an SSH session or a debug endpoint that shells out all work.
- Node.js and npm in both. The fingerprint below is 20 lines of Node with no dependencies, so it runs anywhere the application runs:
import { createHash } from 'node:crypto';
import { readFileSync, existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
const out = [];
out.push('runtime.node ' + process.version);
out.push('runtime.npm ' + execSync('npm -v').toString().trim());
out.push('runtime.platform ' + process.platform + '-' + process.arch);
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
out.push('engines.node ' + (pkg.engines?.node ?? '(absent)'));
if (existsSync('package-lock.json')) {
const raw = readFileSync('package-lock.json');
out.push('lock.version ' + JSON.parse(raw).lockfileVersion);
out.push('lock.sha256 ' + createHash('sha256').update(raw).digest('hex').slice(0, 16));
} else {
out.push('lock.sha256 (absent)');
}
for (const name of Object.keys(pkg.dependencies ?? {}).sort()) {
const p = 'node_modules/' + name + '/package.json';
const v = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')).version : 'NOT INSTALLED';
out.push('dep.' + name.padEnd(16) + v);
}
console.log(out.join('\n'));
- The two environments in the output below are two directories on one machine, one installed through Node 20.19.6 and one through Node 22.23.2. There is no remote host here, so the run demonstrates the method and the shape of a real mismatch, not a real deployment.
Steps
- Step 1.
Fingerprint the first environment. Run it from the application root, where
package.jsonandnode_modulesare.node env-fingerprint.mjs > staging.txt && cat staging.txtruntime.node v20.19.6 runtime.npm 10.8.2 runtime.platform win32-x64 engines.node >=20 lock.version 3 lock.sha256 3024fcdccba61f63 dep.ms 2.1.3 dep.semver 7.6.0dep.lines read the version from insidenode_modules, so they report what is installed rather than what is declared. - Step 2.
Fingerprint the second environment with the same file.
node env-fingerprint.mjs > production.txt && cat production.txtruntime.node v22.23.2 runtime.npm 10.9.8 runtime.platform win32-x64 engines.node >=20 lock.version 3 lock.sha256 dab0c151fa0c4cea dep.ms 2.1.3 dep.semver 7.8.5engines.nodereads>=20on both. The declared requirement is satisfied in both places and says nothing about parity. - Step 3.
Compare the two files.
diff staging.txt production.txt1,2c1,2 < runtime.node v20.19.6 < runtime.npm 10.8.2 --- > runtime.node v22.23.2 > runtime.npm 10.9.8 6c6 < lock.sha256 3024fcdccba61f63 --- > lock.sha256 dab0c151fa0c4cea 8c8 < dep.semver 7.6.0 --- > dep.semver 7.8.5Four lines in three groups: a runtime difference, a lockfile difference, and one dependency two minors apart.
msand the platform match, so those rows are absent. - Step 4.
Rule out the explanation everyone reaches for first, that the two sides declare different dependencies.
diff <(node -p "JSON.stringify(require('../env-staging/package.json'))") <(node -p "JSON.stringify(require('./package.json'))") && echo "package.json is the same object in both"package.json is the same object in bothCompare the parsed objects rather than the files. npm rewrites whitespace in the manifest when it installs, so a byte comparison reports a difference that means nothing.
- Step 5.
Reinstall the second environment from the first one's lockfile, then fingerprint it again and diff.
npm ci --silent && node env-fingerprint.mjs > rebuilt.txt && diff staging.txt rebuilt.txt1,2c1,2 < runtime.node v20.19.6 < runtime.npm 10.8.2 --- > runtime.node v22.23.2 > runtime.npm 10.9.8Every dependency line now matches, and the lockfile hash with them. What is left is the runtime, which a lockfile cannot fix and a version manager has to.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| runtime.node differs | Two Node versions | Pin both from one file and read How to check node version of a project. |
| lock.sha256 differs | The two sides installed from different lockfiles | Ship the lockfile with the deploy and install with npm ci. |
| A dep. line differs, lockfile hash matches | Something wrote to node_modules after the install | Reinstall from scratch. A patched module in place is a defect that travels. |
| NOT INSTALLED on a dep. line | Declared and absent | The install failed or was partial. Nothing else on this page is trustworthy until it is fixed. |
| Only runtime.platform differs | Different OS or architecture | Expect native modules to be rebuilt, and treat performance numbers from one side as not comparable. |
Common mistakes
What to check next
- How to check config drift between environments: the other half of parity, key by key across
.envand config files. - How to check if package lock is in sync: whether the lockfile a deploy installed from matches the manifest at all.
- How to check node version of a project: the four places a runtime version is declared inside one repository.
- How to check installed npm package version: a closer look at one of the
dep.lines that came back different. - How to check if a npm package is installed globally: a tool present on one environment and not the other, which no fingerprint of dependencies reports.
- Test environment checklist: the wider set of checks before an environment is trusted for sign-off.
FAQ
How do I check environment parity between staging and production?
Run one fingerprint script in each, redirect to a file, and diff the two files. Steps 1 to 3 do that. Keep the output sorted and one fact per line, so the diff names the difference instead of moving whole blocks around.
Is this the same as comparing environment variables?
No. This page compares runtimes and installed packages. Key by key comparison of configuration belongs to the config drift check linked above. Running both gives the full answer, and they fail for different reasons.
Why compare the lockfile hash rather than the lockfile?
A lockfile is thousands of lines and a diff of two of them is unreadable. The first 16 hex characters of a sha256 answer the only question a parity check has: same file or not. Diff the lockfiles themselves once the hashes disagree.
Does npm ci guarantee parity?
It guarantees the dependency tree, and nothing above it. After npm ci from the same lockfile, every dep. line matched here and the Node and npm rows still differed. Runtime parity needs a pinned version, not an install command.
What else belongs in a fingerprint?
The container image digest, the OS release, the timezone, the locale and the commit hash of the deployed code. Add one line each, keep the format stable, and store the output with the release so a later comparison has something to compare against.
Verified
Verified by Maks Vernynode 22.23.2 and 20.19.6npm 10.9.8 and 10.8.2GNU diffutils 3.10
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate9 minpublished updated Maks Verny