Repository navigation
[plugin-multi-tenant] Race condition in auto-injected relationship field validation produces false "invalid relationship" errors #16462
Description
Describe the Bug
When a tenant-enabled collection is referenced from an array field on another tenant-enabled collection, creating a document with multiple rows that each reference a tenant-scoped item intermittently fails with per-row "This relationship field has an invalid relationship" validation errors — even though every referenced id exists, belongs to the correct tenant, and is otherwise valid.
The failure is non-deterministic and the rate scales with row count. With 8 rows against MongoDB it reproduces in roughly 1–4 of every 5 attempts; with more rows it approaches 100%.
Suspected root cause
The multi-tenant plugin auto-injects filterOptions on every relationship pointing at a tenant-enabled collection. Payload's relationship validator then runs validateFilterOptions, which calls payload.find to check the chosen id is in the allowed set.
Payload validates array rows in parallel via Promise.all. With N rows each holding a tenant-scoped relationship, N concurrent payload.find calls run on the same MongoDB session inside the create's transaction. MongoDB rejects all but one with:
MongoServerError: Given transaction number X does not match any in-progress transactions on session Y
validateFilterOptions swallows the error in its catch and returns an empty allow-list, so the field is reported as "invalid" for every row whose query lost the race.
This is structurally adjacent to #4078 / #4087 — that fix made validateFilterOptions honor the transaction, but didn't address N validators in the same transaction trying to use the session concurrently. The multi-tenant plugin makes this very easy to hit because it auto-injects filterOptions on every tenant-scoped relationship — the user's schema never declares one.
Link to the code that reproduces this issue
https://github.com/amitzur/payload-issue-multi-tenant-relationship-validation-race
Reproduction Steps
Run the reproducer script from the repo amitzur/payload-issue-multi-tenant-relationship-validation-race:
pnpm install
pnpm reproExpected: the test passes — failures stays empty across all 5 trials.
Actual: the test fails — one or more trials produce per-row "This relationship field has an invalid relationship" errors.
The reproducer registers three collections via the multi-tenant plugin:
tenantsitems(tenant-enabled)holders(tenant-enabled, hasrows: arrayofrelationship→items)
It then creates one tenant + 8 items in that tenant, then attempts five times to create a holder whose rows reference all 8 items. The expectation is that all five succeed; in practice one or more fail with per-row validation errors.
Race condition proof
Uncomment the following to make the repro pass: https://github.com/amitzur/payload-issue-multi-tenant-relationship-validation-race/blob/62ba2a89245934f7d9ec59f4df1a42db377af8e1/payload.config.ts#L74
Which area(s) are affected?
db: mongodb, plugin: multi-tenant
Environment Info
Node.js: v24.12.0
payload: 3.84.1
@payloadcms/plugin-multi-tenant: 3.84.1
@payloadcms/db-mongodb: 3.84.1
Next.js: 16.2.4
Activity
remiroyc commentedon Sep 30, 2026
We hit this in production without
plugin-multi-tenant, so the race looks like a core /db-mongodbissue rather than a plugin one.Setup
payload3.88.0,@payloadcms/db-mongodb3.88.0, MongoDB Atlas (replica set)blocksfield. Several blocks contain a plainuploadfield with a staticfilterOptions(a mime type filter):What happens
Creating a document with two or more of these fields and publishing it in the same request (
POST /api/posts, nodraft) intermittently fails withThe following fields are invalid: … Video. The referenced uploads exist and match the filter. The server logs oneError validating filter options for collection blog-mediaper failed field, with:(
NoSuchTransaction, code 251), and on later attemptsTransaction … has been aborted.Only
createis affected. Saving as a draft first and then publishing always works, which fits the analysis above:updateByIDreads the existing document on the session before validating, so the transaction already exists on the server when the concurrentvalidateFilterOptionsfinds start. Oncreate, those finds are the first commands on the session.Reproduction
With our config against a local single-node replica set, creating a document with 6 such upload blocks and
draft: falsefailed in 6 of 15 runs with the same error.Workaround
A collection
beforeOperationhook that awaits one read on the request's session before validation. With it, 0 of 8 runs failed in the same repro:This only makes sure the transaction is started before the parallel finds; the finds still run concurrently on the session afterwards. Serializing them as in #16463 looks like the proper fix, and it would cover our case too since it isn't specific to the multi-tenant plugin.