Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

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

Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows. Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows.
Exact symptoms and error messages — the core idea of this section at a glance.
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.

Root cause analysis A package's engines field declares the runtime it supports. Root cause analysis A package's engines field declares the runtime it supports.
Root cause analysis — the core idea of this section at a glance.

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:

Resolution and configuration patch Pin the runtime so every environment matches, and decide whether to enforce: Resolution and configuration patch Pin the runtime so every environment matches, and decide whether to enforce:
Resolution and configuration patch — the core idea of this section at a glance.
// 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

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# 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 engines plus an .nvmrc/Volta so shells match CI.
  • Pin the package manager via packageManager and Corepack.
  • Set engine-strict=true when a wrong runtime would genuinely break the build.
  • Fail CI early with a Node-version check step rather than tolerating the warning.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.
  • Pin Node with engines plus an .nvmrc/Volta so shells match CI.
  • Pin the package manager via packageManager and Corepack.
  • Set engine-strict=true where 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.

Runtime pinning engines floor plus a shell pin plus a CI pin agree everywhere. engines floor >= supported version .nvmrc / Volta local shell pin CI node-version same version dependency satisfied warning gone
One authoritative Node version across engines, local shells, and CI removes drift.
{
  "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.

Enforce or warn Whether to promote EBADENGINE to a hard error. How costly is a wrong runtime? breaks build engine-strict wrong behavior CI version check cosmetic leave advisory
Enforce when a wrong runtime breaks the build; warn when it is merely untidy.
# .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.

Pinning the runtime across every environment The durable fix is to make one Node version authoritative and select it everywhere the code runs. Pinning the runtime across every environment The durable fix is to make one Node version authoritative and select it everywhere the code runs.
Pinning the runtime across every environment — the core idea of this section at a glance.

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.

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. 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.
Deciding whether to enforce with engine-strict — the core idea of this section at a glance.

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.

Declare a tested floor Set engines to the oldest tested Node, verify in CI. oldest supported Node the floor test on it in CI verified contract raise in a major when a feature needs it
An engines floor is a contract — test the lowest version so it 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