Fixing npm EBADENGINE 'Unsupported Engine' Warnings
npm warns EBADENGINE Unsupported engine because an installed package requires a Node or npm version different from yours. This page shows what triggers the warning, when it is safe to ignore, and how to make engine constraints deterministic.
Exact symptoms and error messages
npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE package: 'some-lib@3.0.0',
npm warn EBADENGINE required: { node: '>=20' },
npm warn EBADENGINE current: { node: 'v18.19.0' }
npm warn EBADENGINE }
Root cause analysis
A package's engines field declares the runtime it supports. npm compares it to your active Node/npm and warns (or errors, under engine-strict) on a mismatch. The warning is advisory by default, but it signals real drift between developer and CI environments — the same determinism problem the packageManager field and lockfile strategies address, and it ties back to Dependency Resolution Explained.
EBADENGINE is npm comparing an installed package's declared engines against the runtime you are actually using. By default it is a warning, because a minor mismatch is often harmless — but it is a real signal that a dependency was authored for a runtime you are not running, and the incompatibility it warns about can be anything from a missing built-in API to different default behavior. The warning is advisory; the drift it reveals is not.
The deeper issue is environment inconsistency. If developers run Node 18, CI runs Node 20, and a dependency requires Node 20, the warning appears on some machines and not others, and a genuine incompatibility can hide until it hits the one environment that matters. Pinning the runtime everywhere converts 'works on my machine' into a single, enforced version, which is the same determinism goal behind the packageManager field and frozen lockfile installs.
EBADENGINE is npm comparing an installed package's declared engines against the runtime you are actually running. By default it is a warning, because a minor mismatch is often harmless — but it is a real signal that a dependency was authored for a runtime you are not on, and the incompatibility it warns about can be anything from a missing built-in API to different default behavior. The warning is advisory; the drift it reveals is not, which is why it is worth resolving rather than suppressing.
The deeper issue is environment inconsistency. If developers run Node 18, CI runs Node 20, and a dependency requires Node 20, the warning appears on some machines and not others, and a genuine incompatibility can hide until it reaches the one environment that matters. Pinning the runtime everywhere converts 'works on my machine' into a single, enforced version, which is the same determinism goal behind the packageManager field and frozen lockfile installs.
Resolution and configuration patch
Pin the runtime so every environment matches, and decide whether to enforce:
// package.json
{
"engines": { "node": ">=20.0.0" },
"packageManager": "pnpm@10.4.1"
}
# .npmrc — turn the warning into a hard error if you require the engine
engine-strict=true
Add an .nvmrc (or Volta pin) so local shells select the matching Node.
Pin the runtime so every environment matches, and decide whether to enforce:
// package.json
{ "engines": { "node": ">=20.0.0" }, "packageManager": "pnpm@10.4.1" }
# .npmrc — turn the warning into a hard error where the engine is load-bearing
engine-strict=true
Add an .nvmrc (or a Volta pin) so local shells select the matching Node automatically, and set the same version in CI. When all environments agree, EBADENGINE either disappears or becomes a deliberate, universal signal rather than a per-machine surprise.
CLI validation and debug commands
# Confirm your active runtime against the requirement
node --version && npm --version
# See which dependency demands the newer engine
npm ls some-lib
# Fail fast on mismatch
npm install --engine-strict
Prevention and CI guardrails
- Pin Node with
enginesplus an.nvmrc/Volta so shells match CI. - Pin the package manager via
packageManagerand Corepack. - Set
engine-strict=truewhen a wrong runtime would genuinely break the build. - Fail CI early with a Node-version check step rather than tolerating the warning.
- Pin Node with
enginesplus an.nvmrc/Volta so shells match CI. - Pin the package manager via
packageManagerand Corepack. - Set
engine-strict=truewhere a wrong runtime would genuinely break the build. - Fail CI early with a Node-version check rather than tolerating the warning.
Pinning the runtime across every environment
The durable fix is to make one Node version authoritative and select it everywhere the code runs. Declare the floor in engines, pin the exact version for local shells with an .nvmrc or Volta, and set the same version in CI. When all three agree, EBADENGINE either disappears or becomes a deliberate, universal signal rather than a per-machine surprise.
{
"engines": { "node": ">=20.0.0" },
"volta": { "node": "20.11.1" }
}
With Volta (or an .nvmrc plus nvm use), a developer's shell selects the pinned Node automatically on entering the project, and the CI job installs the same version. The dependency's engines requirement is then satisfied identically everywhere, so the warning reflects a real decision about your supported runtime rather than an accident of who ran the install.
Deciding whether to enforce with engine-strict
By default EBADENGINE is advisory, which is usually right — you do not want a trivial mismatch to block every install. But when a wrong runtime would genuinely break your build or ship broken behavior, promote the warning to an error so the mismatch cannot slip through.
# .npmrc
engine-strict=true
With engine-strict, an unsupported engine fails the install outright. Reserve it for cases where the engine requirement is load-bearing — a dependency that uses a Node API added in a specific version, say. For everything else, a CI step that checks node --version against your declared floor gives you an early, explicit failure without making local installs brittle. The decision is really about how costly a wrong runtime is: enforce when the cost is a broken build, warn when it is merely untidy.
Pinning the runtime across every environment
The durable fix is to make one Node version authoritative and select it everywhere the code runs. Declare the supported floor in engines, pin the exact version for local shells with an .nvmrc or Volta, and set the same version in CI. When all three agree, EBADENGINE either disappears — because the runtime satisfies every dependency's requirement — or becomes a deliberate signal that you are on an unsupported runtime, rather than a warning that appears for some developers and not others.
The reason per-machine consistency matters so much is that an engine mismatch is a latent bug, not a cosmetic one. A dependency that requires Node 20 for a specific API works on a developer's Node 20 and fails on a teammate's Node 18 at the moment that API is called, which may be far from the install where the warning appeared. Pinning the runtime removes the variance, so a genuine incompatibility surfaces uniformly — everyone sees it, or no one does — rather than hiding in whichever environment happens to run an older Node. This is the same determinism the packageManager field provides for the resolver, applied to the runtime itself.
Deciding whether to enforce with engine-strict
By default EBADENGINE is advisory, which is usually right — you do not want a trivial mismatch to block every install. But when a wrong runtime would genuinely break the build or ship broken behavior, promote the warning to an error so the mismatch cannot slip through. Setting engine-strict=true in .npmrc makes an unsupported engine fail the install outright, which is appropriate when a dependency uses a Node API added in a specific version and running an older Node would crash.
Reserve enforcement for cases where the engine requirement is load-bearing, and use a lighter touch elsewhere. For softer cases, a CI step that checks node --version against your declared floor gives an early, explicit failure without making local installs brittle for a developer who happens to be one minor version behind. The decision is really about how costly a wrong runtime is: enforce with engine-strict when the cost is a broken build, warn and check in CI when the cost is merely untidy. Matching the strictness to the actual risk keeps the signal meaningful — a hard failure means the runtime genuinely will not work, not that it is slightly off.
Setting engines for your own published package
There are two sides to EBADENGINE: hitting it on a dependency, and declaring engines on the package you publish. For your own package, the engines.node field is a contract you make with consumers, and it earns its keep only when it is a tested floor rather than a hopeful annotation. Declaring >=16 while relying on a Node 20 API means a Node 16 consumer installs cleanly and crashes at runtime — the field advertised support the code does not have. Verify the declared floor in CI by running the test suite on the lowest supported version, so the contract reflects reality.
Choosing the floor is a balance between reach and maintenance. A lower floor supports more consumers but constrains which language and runtime features you can use; a higher floor lets you use modern APIs but excludes consumers on older runtimes. Set it to the oldest Node you actually test against and are willing to support, raise it deliberately in a major version when you adopt a feature that requires it, and document the change. An honest, tested engines floor is what lets consumers trust that your package runs where it claims, and it is the same discipline you rely on when a dependency's engines warns you about a mismatch.
Frequently Asked Questions
Is EBADENGINE safe to ignore?
Often, for a minor mismatch — it is a warning, not a failure. But it means you are running an unsupported runtime, so treat it as a signal to align environments before it becomes a real incompatibility.
How do I make the warning a hard error?
Set engine-strict=true in .npmrc, or run npm install --engine-strict. Then an unsupported engine blocks the install instead of merely warning.
Is EBADENGINE safe to ignore?
For a minor mismatch, often — it is a warning, not a failure. But it means you are running a runtime the dependency was not authored for, so treat it as a prompt to align environments before the incompatibility becomes real.
How do I make the same Node version run everywhere?
Declare the floor in engines, pin the exact version with .nvmrc or Volta for local shells, and set the identical version in CI. When all three agree, the warning reflects a real decision rather than a per-machine accident.
When should I turn on engine-strict?
When a wrong runtime would genuinely break the build — for instance a dependency that uses a Node API from a specific version. For softer cases, a CI node --version check fails early without making local installs brittle.
Is EBADENGINE safe to ignore?
For a minor mismatch, often — it is a warning, not a failure. But it means you are running a runtime the dependency was not authored for, so treat it as a prompt to align environments before the incompatibility becomes a real crash on the machine that matters.
How do I make the same Node version run everywhere?
Declare the floor in engines, pin the exact version with .nvmrc or Volta for local shells, and set the identical version in CI. When all three agree, the warning reflects a deliberate decision rather than a per-machine accident.
When should I turn on engine-strict?
When a wrong runtime would genuinely break the build — for instance a dependency that uses a Node API from a specific version. For softer cases, a CI node --version check fails early without making local installs brittle for someone a minor version behind.
What Node version should I declare in my package's engines?
The oldest Node you actually test against and are willing to support. Verify it in CI by running the suite on that version, so the field is a tested contract rather than a hopeful annotation. Raise it deliberately in a major version when you adopt an API that requires a newer runtime.
Does EBADENGINE block the install?
Not by default — it is a warning and the install proceeds. It blocks only when you set engine-strict=true in .npmrc or pass --engine-strict, which turns an unsupported engine into a hard install failure.
Related
- Dependency Resolution Explained — how ranges and the graph decide installed versions.