Skip to content

Performance: AJV schema compiled on every validation call (api.js)#5461

Open
barttran2k wants to merge 2 commits into
NginxProxyManager:developfrom
barttran2k:contribai/perf/ajv-schema-compiled-on-every-validation-
Open

Performance: AJV schema compiled on every validation call (api.js)#5461
barttran2k wants to merge 2 commits into
NginxProxyManager:developfrom
barttran2k:contribai/perf/ajv-schema-compiled-on-every-validation-

Conversation

@barttran2k
Copy link
Copy Markdown
Contributor

Problem

In backend/lib/validator/api.js, ajv.compile(schema) is called inside apiValidator on every single request. AJV schema compilation is computationally expensive and should be done once per schema, then the compiled validate function should be reused. Under load, this causes significant CPU overhead on every API request that involves validation.

Severity: high
File: backend/lib/validator/api.js

Solution

Cache compiled validators using a Map keyed by schema (or schema $id). For example: const cache = new Map(); function getValidator(schema) { let v = cache.get(schema); if (!v) { v = ajv.compile(schema); cache.set(schema, v); } return v; } Then call getValidator(schema) instead of ajv.compile(schema) on each invocation.

Changes

  • backend/lib/validator/api.js (modified)
  • backend/lib/validator/index.js (modified)

Testing

  • Existing tests pass
  • Manual review completed
  • No new warnings/errors introduced

In `backend/lib/validator/api.js`, `ajv.compile(schema)` is called inside `apiValidator` on every single request. AJV schema compilation is computationally expensive and should be done once per schema, then the compiled validate function should be reused. Under load, this causes significant CPU overhead on every API request that involves validation.

Affected files: api.js, index.js

Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
In `backend/lib/validator/api.js`, `ajv.compile(schema)` is called inside `apiValidator` on every single request. AJV schema compilation is computationally expensive and should be done once per schema, then the compiled validate function should be reused. Under load, this causes significant CPU overhead on every API request that involves validation.

Affected files: api.js, index.js

Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
@nginxproxymanagerci
Copy link
Copy Markdown

Docker Image for build 1 is available on DockerHub:

nginxproxymanager/nginx-proxy-manager-dev:pr-5461

Note

Ensure you backup your NPM instance before testing this image! Especially if there are database changes.
This is a different docker image namespace than the official image.

Warning

Changes and additions to DNS Providers require verification by at least 2 members of the community!

@jc21
Copy link
Copy Markdown
Member

jc21 commented May 14, 2026

Code Review

Thanks for the contribution! The intent here is valid — avoiding repeated AJV schema compilation is a real concern — but the two files need to be assessed separately.

api.js — Works correctly ✅

The cache works as intended here. getValidationSchema() returns a property reference from the module-level compiledSchema singleton (cached in backend/schema/index.js), so the same object reference is returned on every request for the same route. WeakMap.get(schema) will hit after the first call.

Why it helps: AJV 8 internally caches compiled schemas using stableStringify(schema) to generate a lookup key. Even with AJV's own caching, stableStringify runs on every ajv.compile() call. The WeakMap lookup by object reference avoids that serialization cost on subsequent requests.

index.js — Cache never hits ❌

This is the critical issue. The validator exported from index.js is called throughout the codebase with inline schema object literals:

// e.g. backend/routes/nginx/proxy_hosts.js
const data = await validator(
    {
        additionalProperties: false,
        properties: {
            expand: { $ref: "common#/properties/expand" },
        },
    },
    req.body
);

Each invocation of the route handler creates a new object in memory. WeakMap uses object identity (reference equality), so compiledSchemas.get(schema) will always return undefined — the cache never hits. ajv.compile(schema) is called on every request exactly as before, making the added code dead weight.

Fix options:

  1. Move the inline schema objects in the callers to module-level constants so the same reference is reused across requests — then the WeakMap approach works.
  2. Use a string-keyed Map with a deterministic key (e.g. an assigned $id on each schema).
  3. Use AJV's own ajv.addSchema(schema, key) / ajv.getSchema(key) APIs with explicit string keys.

Other notes

  • The "high severity" label is overstated. This is a proxy manager admin panel with low request throughput, AJV already does its own internal caching, and schemas for api.js are already long-lived module-level objects.
  • No tests are included or checked off in the PR template.

Happy to see a revision that fixes the index.js callers to hoist their schemas to module-level constants — that would make both changes effective.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants