diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index c9a9eada63bf73205ead28aef047d0aeb20d8816..7e95c2d6c2887e07616d39e53f9b8952722bc0e3 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -602,11 +602,37 @@ Renovate supports two options:
 - `none`: No release filtering (all releases allowed)
 - `strict`: If the release's constraints match the package file constraints, then it's included
 
-We are working on adding more advanced filtering options.
+More advanced filtering options may come in future.
 
-Note: There must be a `constraints` object in your Renovate config for this to work.
+There must be a `constraints` object in your Renovate config, or constraints detected from package files, for this to work.
 This feature is limited to `packagist`, `npm`, and `pypi` datasources.
 
+<!-- prettier-ignore -->
+!!! warning
+    Enabling this feature may result in many package updates being filtered out silently.
+    See below for a description of how it works.
+
+When `constraintsFiltering=strict`, the following logic applies:
+
+- Are there `constraints` for this repository, either detected from source or from config?
+- Does this package's release declare constraints of its own (e.g. `engines` in Node.js)?
+- If so, filter out this release unless the repository constraint is a _subset_ of the release constraint
+
+Here are some examples:
+
+| Your repo engines.node   | Dependency release engines.node | Result   |
+| ------------------------ | ------------------------------- | -------- |
+| `18`                     | `16 \|\| 18`                    | allowed  |
+| `^18.10.0`               | `>=18`                          | allowed  |
+| `^16.10.0 \|\| >=18.0.0` | `>= 16.0.0`                     | allowed  |
+| `>=16`                   | `16 \|\| 18`                    | filtered |
+| `16`                     | `^16.10.0`                      | filtered |
+
+When using with `npm`, we recommend you:
+
+- Use `constraintsFiltering` on `dependencies`, not `devDependencies` (usually you do not need to be strict about development dependencies)
+- Do _not_ enable `rollbackPrs` at the same time (otherwise your _current_ version may be rolled back if it's incompatible)
+
 ## defaultRegistryUrls
 
 Override a datasource's default registries with this config option.
diff --git a/lib/modules/datasource/index.ts b/lib/modules/datasource/index.ts
index ec0f65099e4e9e0bf02c2348282e08d91fb0fdb4..f9529ddfe1c61289dcfd9c8b8510758b2be0971e 100644
--- a/lib/modules/datasource/index.ts
+++ b/lib/modules/datasource/index.ts
@@ -399,6 +399,7 @@ export async function getPkgReleases(
   res.releases = uniq(res.releases, (x, y) => x.version === y.version);
 
   if (config?.constraintsFiltering === 'strict') {
+    const filteredReleases: string[] = [];
     // Filter releases for compatibility
     for (const [constraintName, constraintValue] of Object.entries(
       config.constraints ?? {}
@@ -411,7 +412,7 @@ export async function getPkgReleases(
             return true;
           }
 
-          return constraint.some(
+          const satisfiesConstraints = constraint.some(
             // If the constraint value is a subset of any release's constraints, then it's OK
             // fallback to release's constraint match if subset is not supported by versioning
             (releaseConstraint) =>
@@ -419,9 +420,22 @@ export async function getPkgReleases(
               (version.subset?.(constraintValue, releaseConstraint) ??
                 version.matches(constraintValue, releaseConstraint))
           );
+          if (!satisfiesConstraints) {
+            filteredReleases.push(release.version);
+          }
+          return satisfiesConstraints;
         });
       }
     }
+    if (filteredReleases.length) {
+      logger.debug(
+        `Filtered ${
+          filteredReleases.length
+        } releases for ${packageName} due to constraintsFiltering=strict: ${filteredReleases.join(
+          ', '
+        )}`
+      );
+    }
   }
   // Strip constraints from releases result
   res.releases.forEach((release) => {
diff --git a/lib/modules/datasource/npm/get.spec.ts b/lib/modules/datasource/npm/get.spec.ts
index c0a7850746ca9c4fdddc05edc53625251985085a..907ba35ab9d8a20a23fe02f710f9b06886ec2f8a 100644
--- a/lib/modules/datasource/npm/get.spec.ts
+++ b/lib/modules/datasource/npm/get.spec.ts
@@ -334,6 +334,9 @@ describe('modules/datasource/npm/get', () => {
               type: 'git',
               url: 'https://github.com/vuejs/vue-next.git',
             },
+            engines: {
+              node: '>= 8.9.0',
+            },
           },
         },
         'dist-tags': { latest: '2.0.0' },
diff --git a/lib/modules/datasource/npm/get.ts b/lib/modules/datasource/npm/get.ts
index 1ff4276dc029248a498f2c22e7ece29804f89371..30f18299df45533880013fa522b86205e2d7df55 100644
--- a/lib/modules/datasource/npm/get.ts
+++ b/lib/modules/datasource/npm/get.ts
@@ -176,6 +176,10 @@ export async function getDependency(
       if (res.versions?.[version].deprecated) {
         release.isDeprecated = true;
       }
+      const nodeConstraint = res.versions?.[version].engines?.node;
+      if (is.nonEmptyString(nodeConstraint)) {
+        release.constraints = { node: [nodeConstraint] };
+      }
       const source = PackageSource.parse(res.versions?.[version].repository);
       if (source.sourceUrl && source.sourceUrl !== dep.sourceUrl) {
         release.sourceUrl = source.sourceUrl;
diff --git a/lib/modules/datasource/npm/types.ts b/lib/modules/datasource/npm/types.ts
index 64a9764705f0e57746fd90bbe1a2d8afd6402524..e67dd987a0e0efb7ca92085ae035b44b973028ad 100644
--- a/lib/modules/datasource/npm/types.ts
+++ b/lib/modules/datasource/npm/types.ts
@@ -17,6 +17,7 @@ export interface NpmResponseVersion {
   gitHead?: string;
   dependencies?: Record<string, string>;
   devDependencies?: Record<string, string>;
+  engines?: Record<string, string>;
 }
 
 export interface NpmResponse {