{"record":{"id":"6a4dea0047924bc2","repo":"appwrite/appwrite","slug":"transaction-limit-exceeded-6a4dea","errorCode":"transaction_limit_exceeded","errorMessage":"Transaction already has {existing} operations, adding 1 would exceed the maximum of {maxBatch}","messagePattern":"Transaction already has (.+?) operations, adding 1 would exceed the maximum of (.+?)","errorType":"exception","errorClass":"Appwrite\\Extend\\Exception","httpStatus":400,"severity":"error","filePath":"src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php","lineNumber":132,"sourceCode":"\n        try {\n            $queries = Query::parseQueries($queries);\n        } catch (QueryException $e) {\n            throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());\n        }\n\n        // Handle transaction staging\n        if ($transactionId !== null) {\n            $transaction = $dbForProject->getDocument('transactions', $transactionId);\n            if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {\n                throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');\n            }\n\n            // Enforce max operations per transaction\n            $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;\n            $existing = $transaction->getAttribute('operations', 0);\n            if (($existing + 1) > $maxBatch) {\n                throw new Exception(\n                    Exception::TRANSACTION_LIMIT_EXCEEDED,\n                    'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch\n                );\n            }\n\n            // Stage the operation in transaction logs\n            $staged = new Document([\n                '$id' => ID::unique(),\n                'databaseInternalId' => $database->getSequence(),\n                'collectionInternalId' => $collection->getSequence(),\n                'transactionInternalId' => $transaction->getSequence(),\n                'action' => 'bulkDelete',\n                'data' => [\n                    'queries' => $originalQueries,\n                ],\n            ]);\n\n            $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/appwrite/appwrite/blob/a1d520eea492ebd1da1ef2dc8db5b2b34e277dce/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php#L114-L150","documentation":"Same staged-transaction cap as elsewhere, hit on the bulk delete route: when transactionId is supplied, the whole bulk delete counts as ONE staged operation, and it is rejected with transaction_limit_exceeded when the transaction's 'operations' counter is already at the plan's databasesTransactionSize (default APP_LIMIT_DATABASE_TRANSACTION). The check happens before the transactionLogs entry is written, so the transaction state is unchanged.","triggerScenarios":"Staging a bulk delete into a transaction that already holds the maximum staged operations (e.g. after many single-document creates/updates/deletes/increments staged against it).","commonSituations":"Composite workflows staging create + update + bulk phases into one transaction; sync jobs that lose count of staged ops; default self-hosted limits being smaller than the job's write count.","solutions":["Commit the current transaction and open a new one for the bulk delete","Track staged operations client-side and rotate transactions before the cap","If the workflow must be atomic beyond the cap, split into multiple committed transactions or negotiate a higher databasesTransactionSize"],"exampleFix":"// before: staging a bulk delete into an already-full transaction\nawait sdk.databases.deleteDocuments(dbId, colId, queries, fullTxId);\n\n// after: rotate transactions at the plan limit before staging\nif (stagedCount >= TX_LIMIT) {\n  await commitTransaction(txId);\n  txId = (await createTransaction()).$id;\n  stagedCount = 0;\n}\nawait sdk.databases.deleteDocuments(dbId, colId, queries, txId);\nstagedCount++;","handlingStrategy":"try-catch","validationCode":"const res = await fetch(`${endpoint}/databases/transactions/${txId}`, {\n  headers: { 'X-Appwrite-Project': projectId, 'X-Appwrite-Key': apiKey },\n});\nconst tx = await res.json();\nif (tx.status !== 'pending' || (tx.operations + 1) > TX_LIMIT) {\n  await commitTransaction(txId);\n  txId = (await createTransaction()).$id;\n}\nawait sdk.databases.deleteDocuments(dbId, colId, queries, txId);","typeGuard":null,"tryCatchPattern":"try {\n  await sdk.databases.deleteDocuments(dbId, colId, queries, txId);\n} catch (e) {\n  if (e.code === 'transaction_limit_exceeded') {\n    await commitTransaction(txId);\n    const tx = await createTransaction();\n    await sdk.databases.deleteDocuments(dbId, colId, queries, tx.$id); // a bulk op counts as ONE staged operation\n  } else throw e;\n}","preventionTips":["Remember each staged bulk operation counts as exactly one operation regardless of matched document count","Rotate transactions at the plan's databasesTransactionSize instead of waiting for the rejection","Keep staged-operation counters in the orchestrating client, not in ad-hoc code paths"],"tags":["transactions","limits","bulk-delete","staging","databases"],"backgroundTag":"transaction-batch-limit-exceeded","analyzedSha":"a1d520eea492ebd1da1ef2dc8db5b2b34e277dce","analyzedAt":"2026-08-18T21:34:56.994Z","contentChangedAt":"2026-08-18T21:34:56.994Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}