appwrite/appwrite · error · Appwrite\Extend\Exception

transaction_limit_exceeded

transaction_limit_exceeded

Error message

Transaction already has {existing} operations, adding 1 would exceed the maximum of {maxBatch}

What it means

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.

Source

Thrown at src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php:132

        try {
            $queries = Query::parseQueries($queries);
        } catch (QueryException $e) {
            throw new Exception(Exception::GENERAL_QUERY_INVALID, $e->getMessage());
        }

        // Handle transaction staging
        if ($transactionId !== null) {
            $transaction = $dbForProject->getDocument('transactions', $transactionId);
            if ($transaction->isEmpty() || $transaction->getAttribute('status', '') !== 'pending') {
                throw new Exception(Exception::GENERAL_BAD_REQUEST, 'Invalid or non‑pending transaction');
            }

            // Enforce max operations per transaction
            $maxBatch = $plan['databasesTransactionSize'] ?? APP_LIMIT_DATABASE_TRANSACTION;
            $existing = $transaction->getAttribute('operations', 0);
            if (($existing + 1) > $maxBatch) {
                throw new Exception(
                    Exception::TRANSACTION_LIMIT_EXCEEDED,
                    'Transaction already has ' . $existing . ' operations, adding 1 would exceed the maximum of ' . $maxBatch
                );
            }

            // Stage the operation in transaction logs
            $staged = new Document([
                '$id' => ID::unique(),
                'databaseInternalId' => $database->getSequence(),
                'collectionInternalId' => $collection->getSequence(),
                'transactionInternalId' => $transaction->getSequence(),
                'action' => 'bulkDelete',
                'data' => [
                    'queries' => $originalQueries,
                ],
            ]);

            $dbForProject->withTransaction(function () use ($dbForProject, $transactionId, $staged) {

View on GitHub (pinned to a1d520eea4)

Solutions

  1. Commit the current transaction and open a new one for the bulk delete
  2. Track staged operations client-side and rotate transactions before the cap
  3. If the workflow must be atomic beyond the cap, split into multiple committed transactions or negotiate a higher databasesTransactionSize

Example fix

// before: staging a bulk delete into an already-full transaction
await sdk.databases.deleteDocuments(dbId, colId, queries, fullTxId);

// after: rotate transactions at the plan limit before staging
if (stagedCount >= TX_LIMIT) {
  await commitTransaction(txId);
  txId = (await createTransaction()).$id;
  stagedCount = 0;
}
await sdk.databases.deleteDocuments(dbId, colId, queries, txId);
stagedCount++;
Defensive patterns

Strategy: try-catch

Validate before calling

const res = await fetch(`${endpoint}/databases/transactions/${txId}`, {
  headers: { 'X-Appwrite-Project': projectId, 'X-Appwrite-Key': apiKey },
});
const tx = await res.json();
if (tx.status !== 'pending' || (tx.operations + 1) > TX_LIMIT) {
  await commitTransaction(txId);
  txId = (await createTransaction()).$id;
}
await sdk.databases.deleteDocuments(dbId, colId, queries, txId);

Try / catch

try {
  await sdk.databases.deleteDocuments(dbId, colId, queries, txId);
} catch (e) {
  if (e.code === 'transaction_limit_exceeded') {
    await commitTransaction(txId);
    const tx = await createTransaction();
    await sdk.databases.deleteDocuments(dbId, colId, queries, tx.$id); // a bulk op counts as ONE staged operation
  } else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of appwrite/appwrite@a1d520eea4 (2026-08-18). Data as JSON: /api/errors/6a4dea0047924bc2. Report an issue: GitHub.