Guide · AWS DynamoDB

MCP Server DynamoDB Transactions — idempotent multi-step tool calls, TransactWriteItems

DynamoDB transactions (TransactWriteItems) let MCP servers atomically write to up to 25 items across one or more tables — all succeed or all fail — but the cost is 2× the write capacity units of the equivalent individual writes. For MCP tool calls, three patterns matter most: idempotency tokens — passing a client-generated token to TransactWriteItems makes the call idempotent for 10 minutes; retrying with the same token returns the original result without re-executing the transaction; ConditionCheck — include a ConditionCheck item in the transaction to assert that a prerequisite item exists or has the expected version before committing the write; and TransactionConflictException — this is the only retriable error from transactions; it means another transaction modified one of the same items concurrently; backoff and retry (the recommended strategy is 50ms + jitter, up to 3 retries).

TL;DR

Use TransactWriteItems for MCP tool calls that must atomically update multiple items or assert preconditions. Pass a ClientRequestToken (UUID derived from the tool call ID) for idempotency. Include a ConditionCheck to guard against concurrent modification. Cap at 25 items per transaction. Retry on TransactionConflictException with exponential backoff (50ms base, up to 3 retries). Never retry on TransactionCanceledException with a ConditionalCheckFailed reason — that is a business logic failure, not a transient error.

Basic TransactWriteItems pattern for MCP tool calls

An MCP tool call that creates a resource and logs the operation atomically — both writes succeed or both are rolled back:

import {
  DynamoDBClient,
  TransactWriteItemsCommand,
  TransactWriteItemsCommandInput,
} from "@aws-sdk/client-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";

const ddb = new DynamoDBClient({});

export async function createResourceWithAudit(
  toolCallId: string,    // MCP tool call ID — use as idempotency key
  sessionId: string,
  resourceId: string,
  resourceData: Record
): Promise {
  const nowIso = new Date().toISOString();
  const ttlSec = Math.floor(Date.now() / 1000) + 86400; // 24h

  const input: TransactWriteItemsCommandInput = {
    // ClientRequestToken makes this transaction idempotent for 10 minutes.
    // Use the MCP tool call ID — if the caller retries the same tool call,
    // the same token ensures the transaction doesn't double-execute.
    ClientRequestToken: toolCallId,

    TransactItems: [
      // 1. Assert the session is alive (ConditionCheck — read-only assertion)
      {
        ConditionCheck: {
          TableName: "mcp-sessions",
          Key: marshall({ pk: `session#${sessionId}`, sk: "metadata" }),
          ConditionExpression: "attribute_exists(pk) AND #ttl > :now",
          ExpressionAttributeNames: { "#ttl": "ttl" },
          ExpressionAttributeValues: marshall({ ":now": Math.floor(Date.now() / 1000) }),
        },
      },
      // 2. Create the resource item
      {
        Put: {
          TableName: "mcp-resources",
          Item: marshall({
            pk: `resource#${resourceId}`,
            sk: "v1",
            resourceId,
            sessionId,
            data: resourceData,
            status: "active",
            createdAt: nowIso,
            ttl: ttlSec,
          }),
          // Prevent accidental overwrites of existing resources
          ConditionExpression: "attribute_not_exists(pk)",
        },
      },
      // 3. Append an audit log entry
      {
        Put: {
          TableName: "mcp-sessions",
          Item: marshall({
            pk: `session#${sessionId}`,
            sk: `audit#${nowIso}#${toolCallId}`,
            toolCallId,
            action: "create_resource",
            resourceId,
            recordedAt: nowIso,
            ttl: ttlSec,
          }),
        },
      },
    ],
  };

  await ddb.send(new TransactWriteItemsCommand(input));
}

Retry strategy for TransactionConflictException

TransactionConflictException means two transactions tried to modify the same item at the same time. DynamoDB uses optimistic concurrency — the loser is rejected and must retry. This is the only error from transactions that should be retried. All other errors (TransactionCanceledException, ValidationException) indicate either a business logic failure or a programming error and should not be retried.

import { TransactionConflictException } from "@aws-sdk/client-dynamodb";

const MAX_RETRIES = 3;
const BASE_DELAY_MS = 50;

export async function executeToolCallTransaction(
  toolCallId: string,
  buildTransaction: () => TransactWriteItemsCommandInput
): Promise {
  let attempt = 0;

  while (true) {
    try {
      await ddb.send(new TransactWriteItemsCommand(buildTransaction()));
      return; // success
    } catch (err) {
      if (err instanceof TransactionConflictException && attempt < MAX_RETRIES) {
        // Exponential backoff with jitter for concurrent transaction conflicts
        const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 25;
        await new Promise((resolve) => setTimeout(resolve, delay));
        attempt++;
        continue;
      }
      // Re-throw all other errors — TransactionCanceledException is a business
      // logic failure (e.g., ConditionCheck failed) and must NOT be retried.
      throw err;
    }
  }
}

// Parse TransactionCanceledException to surface which condition failed
import { TransactionCanceledException } from "@aws-sdk/client-dynamodb";

function handleTransactionCanceled(err: unknown): string {
  if (!(err instanceof TransactionCanceledException)) throw err;
  const reasons = err.CancellationReasons ?? [];
  for (let i = 0; i < reasons.length; i++) {
    const code = reasons[i].Code;
    if (code === "ConditionalCheckFailed") {
      // Map index to the relevant TransactItem for a descriptive error
      return `Precondition failed for item ${i}: ${reasons[i].Message}`;
    }
    if (code === "ItemSizeLimitExceeded") {
      return `Item ${i} exceeds 400 KB DynamoDB item size limit`;
    }
  }
  return "Transaction canceled for unknown reason";
}

25-item limit and capacity cost

DynamoDB transactions have two constraints that affect MCP server design:

ConstraintValueImplication for MCP servers
Max items per transaction25 items across all tablesAn MCP tool call that touches more than 25 items (e.g., batch-creating resources) must be split across multiple transactions; this breaks atomicity — use saga pattern or accept partial failure
Write capacity cost2× individual write WCUsEach transactional write consumes 2 WCUs instead of 1; on provisioned throughput tables, transaction-heavy workloads may need 2× the write capacity compared to non-transactional writes
Read capacity cost (TransactGetItems)2× individual read RCUsTransactional reads are always strongly consistent; if you only need eventual consistency for reads, use BatchGetItem (1× RCU) instead
Max aggregate transaction size4 MB total payloadThe sum of all item sizes in a transaction cannot exceed 4 MB; for MCP sessions storing large context blobs, check total payload before transacting
Cross-table transactionsSupported (any tables in same region, same account)MCP servers can atomically write session state + audit log + resource record across three different tables in one transaction
Cross-region transactionsNot supportedIf you use DynamoDB Global Tables, transactions are local to the Region they are sent to; they will eventually replicate but are not globally atomic

Idempotency token lifetime and semantics

The ClientRequestToken makes a TransactWriteItems call idempotent for exactly 10 minutes. If you retry with the same token within that window, DynamoDB returns the original response without re-executing the transaction. After 10 minutes, the token is forgotten and the same token would execute a new transaction.

For MCP tool calls, the tool call ID (which the MCP framework provides) is the ideal idempotency token. If the MCP client or framework retries a timed-out tool call within 10 minutes, the same token prevents double execution. After 10 minutes, a re-submitted tool call with the same ID would create a duplicate — guard against this with a ConditionExpression: "attribute_not_exists(toolCallId)" on the primary item write.

Token requirements: must be 1–36 characters (UUID fits), only alphanumerics and hyphens.

Common failure modes

SymptomCauseFix
Tool call creates duplicate resources when retriedNo ClientRequestToken or idempotency ConditionExpression; each retry executes a new transactionPass ClientRequestToken: toolCallId for retries within 10 minutes; add ConditionExpression: "attribute_not_exists(pk)" on the resource Put as a belt-and-suspenders guard
TransactionCanceledException with ConditionalCheckFailedA ConditionCheck or Put condition failed — the session is expired, the resource already exists, or the version check failedDo not retry — this is a business logic failure; inspect CancellationReasons[i].Code to identify which item's condition failed and surface the appropriate error to the MCP caller
Transaction fails with ValidationException: Transaction request cannot include multiple operations on one itemThe transaction includes two operations touching the same item (e.g., a Put and a ConditionCheck on the same key)Each item can appear only once in a transaction; merge the condition into the Put's own ConditionExpression instead of adding a separate ConditionCheck
High latency (P99 spikes) on transactionsTransaction conflict rate is high; retries compound latency; 2× WCU cost under sustained load causes throttlingMonitor TransactionConflict CloudWatch metric; if high, redesign to reduce hot items; consider optimistic locking with version attributes + conditional update instead of transactions for single-item atomic updates
Transaction fails with 25-item limit errorA batch MCP tool call (e.g., "create 30 tasks") exceeds the 25-item transaction limitSplit into multiple transactions of ≤25 items; accept partial atomicity or implement a saga pattern with compensating writes for rollback