Symptoms
TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]or[TransactionConflict, None].- Plain
PutItem/UpdateItemcalls fail withTransactionConflictException. - 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
TransactionConflictand throttling with exponential backoff and jitter. The SDKs retry some cases automatically; check your SDK's behaviour forTransactionCanceledException(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.