From 4388f764857c8b735c034500121f78878e2f3d20 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 26 Mar 2026 18:19:24 -0600 Subject: [PATCH] fix(delete): process bulk deletes sequentially to prevent silent data loss The bulk delete operation used .map(async ...) to create promises for each document, then either Promise.all or for...of await. However, .map(async ...) eagerly starts all callbacks immediately -- the for...of await loop only controls the order results are collected, not execution order. This caused concurrent deleteOne calls on a shared transaction connection, where interleaved queries silently dropped deletes. Replace with a proper sequential for...of loop that awaits each document deletion before starting the next. Reproduces consistently on Payload 3.80.0 with @payloadcms/db-postgres. Verified not present in 3.41.0 (regression). Fixes #16075 Related: #15100 Made-with: Cursor --- .../src/collections/operations/delete.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/payload/src/collections/operations/delete.ts b/packages/payload/src/collections/operations/delete.ts index a08e6c15f48..98adf67cedc 100644 --- a/packages/payload/src/collections/operations/delete.ts +++ b/packages/payload/src/collections/operations/delete.ts @@ -136,7 +136,7 @@ export const deleteOperation = async < const errors: BulkOperationResult['errors'] = [] - const promises = docs.map(async (doc) => { + const processDoc = async (doc: (typeof docs)[number]) => { let result const { id } = doc @@ -307,18 +307,17 @@ export const deleteOperation = async < }) } return null - }) + } - // Process sequentially when using single transaction mode to avoid shared state issues - // Process in parallel when using one transaction for better performance - let awaitedDocs - if (req.payload.db.bulkOperationsSingleTransaction) { - awaitedDocs = [] - for (const promise of promises) { - awaitedDocs.push(await promise) - } - } else { - awaitedDocs = await Promise.all(promises) + // Process documents sequentially to prevent concurrent deleteOne calls + // from interleaving queries on a shared transaction connection. + // Using .map(async ...) would eagerly start all callbacks, causing + // silent data loss even with for...of await (which only controls the + // order results are collected, not execution order). + // See: https://github.com/payloadcms/payload/issues/16075 + const awaitedDocs = [] + for (const doc of docs) { + awaitedDocs.push(await processDoc(doc)) } // /////////////////////////////////////