Home / AWS

AWSTroubleshootingAdvanced

DynamoDB TransactionCanceledException and TransactionConflict: reading cancellation reasons and designing around contention

TransactWriteItems calls fail with TransactionCanceledException under load. Read the per-item cancellation reasons, tell conflicts from conditions and throttling, and fix hot items, retries and idempotency.

Deepak Bhardwaj4 min read

Applies toAmazon DynamoDB transactions (TransactWriteItems, TransactGetItems), AWS SDKs, on-demand and provisioned capacity

Symptoms

  • TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed] or [TransactionConflict, None].
  • Plain PutItem/UpdateItem calls fail with TransactionConflictException.
  • The failure rate rises sharply with traffic, even though table capacity looks fine.

Step 1: read the cancellation reasons

The exception carries one reason per item, in the order you sent them. None means that item was fine.

Code Meaning
ConditionalCheckFailed A ConditionExpression was false (business rule, e.g. balance too low, version changed)
TransactionConflict Another transaction (or a write) was touching the same item at the same time
ThrottlingError Capacity or partition throughput exceeded for that item's partition
ValidationError Bad request: same item twice in one transaction, size limits, wrong types
ItemCollectionSizeLimitExceeded Item collection over the LSI size limit
ProvisionedThroughputExceeded Provisioned capacity exceeded

Python example that logs reasons properly:

import boto3, botocore
ddb = boto3.client("dynamodb")
try:
    ddb.transact_write_items(TransactItems=items, ClientRequestToken=request_id)
except botocore.exceptions.ClientError as e:
    if e.response["Error"]["Code"] == "TransactionCanceledException":
        reasons = e.response.get("CancellationReasons", [])
        for i, r in enumerate(reasons):
            if r.get("Code") != "None":
                print(i, r.get("Code"), r.get("Message"), r.get("Item"))
    raise

Add ReturnValuesOnConditionCheckFailure: "ALL_OLD" on condition checks to get the current item back in the reason, which saves a read when debugging.

Step 2: check the metrics

aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name TransactionConflict \
  --dimensions Name=TableName,Value=Orders --statistics Sum --period 60 \
  --start-time 2026-10-28T08:00:00Z --end-time 2026-10-28T10:00:00Z

aws cloudwatch get-metric-statistics --namespace AWS/DynamoDB --metric-name ThrottledRequests \
  --dimensions Name=TableName,Value=Orders Name=Operation,Value=TransactWriteItems \
  --statistics Sum --period 60 --start-time 2026-10-28T08:00:00Z --end-time 2026-10-28T10:00:00Z

CloudWatch Contributor Insights for DynamoDB shows the most accessed and most throttled keys:

aws dynamodb update-contributor-insights --table-name Orders --contributor-insights-action ENABLE

Step 3: fix by cause

TransactionConflict: hot items

Transactions take short locks on every item they touch. If many transactions touch the same item (a global counter, an inventory row for a best-seller, a tenant "summary" item), they conflict.

Design Change
One counter item updated in every transaction Shard the counter (counter#0..N), sum on read; or update it asynchronously from a stream
Inventory row per product for a flash sale Pre-split stock into buckets; reserve from a random bucket
"Last updated" timestamp on a parent item in every child write Drop it from the transaction; derive it from Streams
Read-modify-write of the same document by many workers Use UpdateItem with atomic ADD outside a transaction where atomicity across items isn't required

Also: a non-transactional write to an item that's part of an in-flight transaction fails with TransactionConflictException. Background jobs touching the same items cause this.

ConditionalCheckFailed: expected, not an outage

These are business-rule failures. Don't retry blindly. Return the reason to the caller (e.g. "out of stock") or re-read and decide.

ThrottlingError

Transactions consume roughly twice the capacity of normal writes (prepare and commit). A table sized for normal writes may throttle when switched to transactions. Check provisioned capacity or on-demand limits, and look for hot partitions with Contributor Insights.

ValidationError

The same key can't appear twice in one transaction, and the total size and item count are limited (check the current transaction limits in the DynamoDB documentation). Split large transactions, or rethink whether you need all items in one atomic unit.

Step 4: retries and idempotency

  • Retry TransactionConflict and throttling with exponential backoff and jitter. The SDKs retry some cases automatically; check your SDK's behaviour for TransactionCanceledException (often not retried, because of conditional failures).
  • Use ClientRequestToken. A retry with the same token within the idempotency window doesn't apply the transaction twice, which matters when a timeout hides whether the first attempt committed.
import uuid
request_id = str(uuid.uuid4())      # create once per business operation, reuse on retries

Warning

Generating a new token on each retry removes the protection: a timed-out attempt that actually committed, followed by a retry, applies the change twice (for example charging a customer twice).

Step 5: check you need a transaction

Need Simpler alternative
Atomic update of one item UpdateItem with a condition
Exactly-once side effects Idempotency key item with attribute_not_exists condition
Keeping derived data in sync DynamoDB Streams + Lambda, eventually consistent
Cross-item consistency for a few items Transactions (this is what they're for)

Prevent it next time

  • Log full cancellation reasons, not just the exception name.
  • Model hot items out of the transactional path during design reviews.
  • Enable Contributor Insights on tables that use transactions.
  • Load test with realistic key distribution (Zipf, not uniform) to reveal hot items early.