fix: memoize version collection fields to bound flattenedFieldsCache growth#17356
Open
alevochko wants to merge 1 commit into
Open
fix: memoize version collection fields to bound flattenedFieldsCache growth#17356alevochko wants to merge 1 commit into
alevochko wants to merge 1 commit into
Conversation
alevochko
force-pushed
the
fix/version-collection-fields-cache-leak
branch
from
July 16, 2026 09:10
c161fe3 to
4aeb298
Compare
alevochko
marked this pull request as ready for review
July 16, 2026 09:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(payload): memoize version collection fields to bound flattenedFieldsCache growth
What / Why
buildVersionCollectionFields()is called on every operation against aversioned/drafts-enabled collection (
find,findVersions,findVersionByID,countVersions,update, …). Each call allocates a brand-newfieldsarray(array literal +
collection.fields.filter(...)).Those arrays are then flattened by
flattenAllFields(), whose module-levelflattenedFieldsCache(a plainMap) is keyed by array identity and alwaysruns
flattenedFieldsCache.set(fields, result). Because the key is a fresh arrayeach call, the cache never hits and grows by (at least) one strong-referenced
entry per request — an unbounded memory leak that scales with request count,
not with the number of collections.
Under public read traffic this is a slow, steady heap climb on every pod.
Root cause (confirmed from a production heap-snapshot diff)
Two snapshots ~19h apart of the same process (identical traffic) showed a
monotonic heap climb driven by ~218k new
Object+ ~103k newArraynodes,96–97% retained via a single
Map(flattenedFieldsCache) whosetableheld~36,000 entries (1.8 MB). The entries were
Map<fieldsArray, flattenedFields>pairs, and the values were exactly the fields produced here —
parent,version(group),publishedLocale,latest,createdAt,updatedAt— i.e.the version collection schema, re-created and re-cached on every read.
The schema is derived purely from the sanitized (stable) collection config,
so it is identical for every call — it should be built once and reused.
The fix
Memoize
buildVersionCollectionFieldsper collection in aWeakMapkeyed by thesanitized collection config, caching the flattened and unflattened variants
separately (the
flattenflag changes theversiongroup shape). This returns areferentially stable array, so:
flattenedFieldsCachenow hits (stable key) instead of growing — the leak isgone, and the cache does what it was designed to do.
WeakMapkeeps it safe: if a collection config is ever discarded, its entry isreclaimed.
This touches only
buildVersionCollectionFields, so all seven call sites benefitwithout changes.
Tests
Added
packages/payload/src/versions/buildCollectionFields.spec.ts:parent/version/createdAt/updatedAt/latest; theversiongroup drops the baseid, keeps others).Affected versions
Present on
main(4.0.0-beta) and all 3.x releases checked (3.79 → 3.86.0) and4.0.0-canary — the code is identical across them.