DynamoDB TransactionCanceledException: Read "[None, ConditionalCheckFailed]", Get CancellationReasons in boto3 and the CLI, and Fix TransactionConflict, ThrottlingError and ValidationError (With a Tested Retry Helper)

Logeshwaran
—

Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed] is DynamoDB's TransactionCanceledException: one action in your TransactWriteItems (or TransactGetItems) call failed, so DynamoDB rolled back every action in it. The list in square brackets is the whole diagnosis, once you know how to read it: one code per action, in the same order as your request. None means that action was fine; the other entry names the culprit. In [None, ConditionalCheckFailed], your second action's condition was false. The usual codes are ConditionalCheckFailed, TransactionConflict (another write touched the same item at the same moment), ThrottlingError or ProvisionedThroughputExceeded, and ValidationError. Two facts catch almost everyone. First, the AWS SDKs do not retry a canceled transaction, even when the reason is a short-lived conflict that a second try would clear, so retrying is your job, and only for the reasons that are worth retrying. Second, a canceled transaction still costs you: DynamoDB charges the prepare-and-commit capacity for every item it attempted, success or not. This page shows how to read the reasons in Python, the AWS CLI and other SDKs, what each code means and its fix, a tested retry helper, and the lookalike errors that are not this one.

Jake's phone repair shop takes online orders for spare screens. Ethan, his developer friend, wrote the order step as one DynamoDB transaction: create the order and take one screen out of stock, both or neither, so a customer can never pay for a screen that is not there. On launch day some orders failed with a sentence about "cancellation reasons" and a little list in brackets. Jake asked whether the shop had run out of screens. Sometimes yes, Ethan found; but sometimes the stock was fine and two customers had simply clicked at the same second. Telling those two apart is most of this error.

⚡ Quick Answer

• Read the brackets: one code per action, in request order. None = fine. The other code names the action and the reason. How to read it.

• Get the full reasons: Python e.response["CancellationReasons"]; CLI --cli-error-format json; Java e.cancellationReasons(). Per tool.

• ConditionalCheckFailed: a business rule said no. Do not retry; add ReturnValuesOnConditionCheckFailure: ALL_OLD to see the item. Fix.

• TransactionConflict / ThrottlingError: retry with backoff yourself; the SDK will not. Retry helper.

• "cannot include multiple operations on one item" or more than 100 actions: a ValidationException before anything runs; fix the request. Request errors.

Also: canceled transactions still cost capacity · ClientRequestToken · testing with DynamoDB Local · fixes by code.

New to DynamoDB? Our plain-English DynamoDB guide explains tables, items and keys in about ten minutes. You can follow this page without it.

🧭 NEW HERE? READ THESE FIRST

New to DynamoDB or its error handling? These five pages give you the words this one uses:

📌 Bookmark this; the reason-code table and the retry helper are the parts you will come back for.

What a DynamoDB transaction is, and why one failure cancels all

A DynamoDB transaction groups up to 100 actions (Put, Update, Delete and ConditionCheck) across up to 100 different items, in one or more tables of the same account and Region, into a single all-or-nothing call. Either every action happens, or none does. That is exactly what Jake's order needs: "create order" and "take one from stock" must never happen one without the other.

All-or-nothing has a direct consequence. If any one action cannot proceed, DynamoDB cancels the whole request and rolls back the rest, and it tells you so with TransactionCanceledException. The exception is not about the transaction as a whole being wrong; it is about one or more actions inside it, and the job is to find which. The single-item cousin of this error, ConditionalCheckFailedException from a plain PutItem or UpdateItem, has its own guide; inside a transaction the same failure arrives wrapped in this exception instead.

Reading "[None, ConditionalCheckFailed]": one code per action

Here is the full exception from a two-action order transaction (put the order, decrement stock) when the screen is out of stock, captured from a test run:

Code:    TransactionCanceledException
Message: Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]
CancellationReasons: [{"Code": "None"},
                      {"Code": "ConditionalCheckFailed", "Message": "The conditional request failed"}]

Read the list left to right against your TransactItems array: position 0 is the order Put, and its code is None, meaning it had no problem; position 1 is the stock Update, and its condition (qty >= 1) was false. Swap the situation (the stock is fine but the order ID already exists) and the same transaction says [ConditionalCheckFailed, None]. The bracket list in the message and the CancellationReasons array say the same thing; the array adds a message per action and, if you ask for it, the item itself.

Three reading rules:

  • None is a literal string, not an empty value. Code that checks if reason["Code"] treats it as a failure; compare against "None".
  • More than one action can fail, and the list shows each one.
  • The order is the request's order, so log the request alongside the reasons, or map the index back to your actions as the helper below does.

Jake: "So the brackets are a row of ticks and crosses, one for each thing we asked it to do."

Ethan: "And the cross tells you which job it was and why. The first time I saw it I read it as one error. It's a checklist."

Getting the cancellation reasons in Python, the CLI and other SDKs

In Python (boto3), the reasons are on the error response. Our boto3 error-handling guide covers ClientError in general; for this exception:

from botocore.exceptions import ClientError

try:
    ddb.transact_write_items(TransactItems=items)
except ClientError as e:
    if e.response["Error"]["Code"] == "TransactionCanceledException":
        for i, r in enumerate(e.response["CancellationReasons"]):
            if r["Code"] != "None":
                print(i, r["Code"], r.get("Message"))
    raise

In the AWS CLI v2, the default error output hides the reasons and tells you how to see them:

aws: [ERROR]: An error occurred (TransactionCanceledException) when calling the TransactWriteItems operation:
Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]

Additional error details:
CancellationReasons: <complex value>
Use "--cli-error-format json" or another error format to see the full details.

Add the flag and you get the whole structure:

aws dynamodb transact-write-items --transact-items file://items.json --cli-error-format json
{
    "Message": "Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]",
    "Code": "TransactionCanceledException",
    "CancellationReasons": [
        { "Code": "None" },
        { "Code": "ConditionalCheckFailed", "Message": "The conditional request failed" }
    ]
}

In the other SDKs, the exception object carries the same CancellationReasons list (each entry has Code, Message and an optional Item): in the JavaScript SDK v3 it is the CancellationReasons property of the TransactionCanceledException error, and in the Java SDK v2 it is e.cancellationReasons(), a list whose entries have code(), message() and item(). Older SDKs (the JavaScript SDK v2 especially) exposed only the message, which is why some answers online parse the bracket list out of the message text. That works, because the message lists the codes in order, but on a current SDK read the structured list instead.

Every cancellation code, what it means and what to do

CodeMeansRetry?Fix
NoneThis action was finen/aLook at the others
ConditionalCheckFailedA condition expression was falseNoBusiness outcome: tell the user, or re-read and decide
TransactionConflictAnother write was changing the same item at that momentYes, with backoffRetry; reduce contention on hot items
ThrottlingErrorOn-demand table or index scaling upYes, with backoffRetry; check for a hot key
ProvisionedThroughputExceededProvisioned table or index out of capacityYes, with backoffMore capacity or auto scaling; remember transactions use double
ItemCollectionSizeLimitExceededA local secondary index's item collection is fullNoData model change
ValidationErrorThe change itself is invalid: wrong type, too large, bad path, number overflowNoFix the data or expression for that action

ConditionalCheckFailed: a rule said no, so do not retry

This is the code you want to see, in a sense: your own rule worked. "Order ID must not exist," "stock must be at least 1," "version must still be 7." Retrying will fail the same way, because the data has not changed. Treat it as an answer: show "out of stock," refuse the duplicate order, or re-read the item and decide again.

To see why the condition failed without a second read, add ReturnValuesOnConditionCheckFailure set to ALL_OLD to the action. The failing reason then carries the item as it was:

{"Update": {"TableName": "Stock", "Key": {"pk": {"S": "screen-a52"}},
            "UpdateExpression": "SET qty = qty - :one",
            "ConditionExpression": "qty >= :one",
            "ExpressionAttributeValues": {":one": {"N": "1"}},
            "ReturnValuesOnConditionCheckFailure": "ALL_OLD"}}
CancellationReasons: [{"Code": "None"},
  {"Item": {"pk": {"S": "screen-a52"}, "qty": {"N": "0"}},
   "Code": "ConditionalCheckFailed", "Message": "The conditional request failed"}]

That is the difference between "something failed" and "stock was 0." It is available on Put, Update, Delete and ConditionCheck actions. In Java, the item is reason.item(); if it comes back empty, the action did not set the option.

TransactionConflict: two writes on one item at the same moment

This is the code behind the most-read question on this error, titled "Mysterious TransactionConflict," and the mystery is a design choice. DynamoDB does not lock items for the length of a transaction. Instead, if another write touches one of the transaction's items while the transaction is in flight, DynamoDB cancels the transaction rather than wait. It happens when:

  • a plain PutItem, UpdateItem or DeleteItem hits an item that an ongoing transaction includes (the plain write then fails with its own TransactionConflictException);
  • two transactions include the same item at the same time;
  • a TransactGetItems read includes an item that a write is changing.

It looks random because it depends on timing: on a quiet day it never happens; at a launch, with many orders for the same popular screen, it happens a lot. It is also the reason that matters most for retries, because the SDKs do not retry a canceled transaction, even though a conflict usually clears in milliseconds. DynamoDB counts each failed item-level request in the TransactionConflict CloudWatch metric, which is the first thing to graph when you suspect this.

The fix is a retry with backoff that only retries the reasons worth retrying. This helper does exactly that, and passes condition failures straight back to you:

import random, time
from botocore.exceptions import ClientError

RETRYABLE = {"TransactionConflict", "ThrottlingError", "ProvisionedThroughputExceeded"}

def transact_write(ddb, items, attempts=5):
    """Run TransactWriteItems; retry only when every failure reason is retryable."""
    for attempt in range(1, attempts + 1):
        try:
            return ddb.transact_write_items(TransactItems=items)
        except ClientError as e:
            if e.response["Error"]["Code"] != "TransactionCanceledException":
                raise
            reasons = e.response.get("CancellationReasons", [])
            failed = [(i, r["Code"], r.get("Message", "")) for i, r in enumerate(reasons) if r["Code"] != "None"]
            if attempt < attempts and failed and all(code in RETRYABLE for _, code, _ in failed):
                time.sleep(min(2 ** attempt * 0.05, 1.0) + random.random() * 0.05)
                continue
            for i, code, msg in failed:
                print(f"action {i} ({next(iter(items[i]))}): {code} {msg}")
            raise
transact_write(ddb, order_items)

Run against an out-of-stock order in a test table, it printed action 1 (Update): ConditionalCheckFailed The conditional request failed and raised immediately without retrying; with stock in place, the same call returned 200. If conflicts are constant rather than occasional, retries only hide a data-model problem: many transactions all touching one counter item (a global stock total, a sequence number) will keep colliding. Spread the hot item (stock per warehouse or per shard), keep transactions small, and read our hot-partition guide, which covers the same item from the throughput side.

Jake: "So two customers clicked 'buy' together and the computer canceled one instead of making him wait?"

Ethan: "Exactly. It doesn't queue people at the counter. It says 'busy, try again,' and our code now tries again a few milliseconds later. The customer never sees it."

ThrottlingError and ProvisionedThroughputExceeded: capacity, doubled

These two are capacity problems surfacing inside a transaction. ThrottlingError appears on on-demand tables and indexes while DynamoDB scales up behind the scenes, with a message saying it is "automatically scaling your table or index so please try again shortly" and pointing at hot keys if it persists. ProvisionedThroughputExceeded appears on provisioned tables and indexes that have run out of the capacity you set. Both are retryable, and the helper above retries them.

The capacity math is where transactions surprise people: DynamoDB performs two underlying reads or writes for every item in a transaction, one to prepare and one to commit. So one transaction per second writing three 500-byte items needs six write capacity units, not three. If you sized a provisioned table for plain writes and then moved those writes into transactions, you halved your headroom. Our capacity guide covers units and auto scaling.

Canceled transactions still cost capacity

The prepare-and-commit capacity is consumed even when the transaction is canceled, including for a ConditionalCheckFailed. A flash sale where thousands of customers hit "buy" on an out-of-stock item runs thousands of transactions that all cancel, and each one still consumes the capacity for the items it attempted. Two cheap defenses: check the obvious condition with a normal read before building the transaction (show "sold out" without a write), and keep transactions to the items that truly need to be atomic. On on-demand tables that is money; on provisioned tables it is throttling for everyone else. The on-demand vs provisioned guide has the per-request prices.

Request errors that look similar: ValidationException up front

Some mistakes never reach the cancel stage. DynamoDB rejects the request before running anything, with a plain ValidationException and no reasons list. In a test run:

  • Two actions on the same item: Transaction request cannot include multiple operations on one item. An Update and a ConditionCheck on one key count as two. Merge them into one action with a combined condition.
  • More than 100 actions: DynamoDB Local answered Member must have length less than or equal to 100. Split the work, and if it is bulk loading, use BatchWriteItem instead, because transactions are the wrong tool for bulk ingestion. Our BatchWriteItem guide covers that path.
  • Over 4 MB in total, or a table in a different account or Region: transactions are limited to one account and Region and 4 MB of items.

A ValidationError inside the reasons list is different: the request was well formed, but one action's change was invalid against the stored item, for example adding a number to a string attribute, pushing an item past 400 KB, or updating a document path that does not exist. The message names which; our "parameter values were invalid" guide decodes the common ones.

ClientRequestToken, idempotency and TransactionInProgressException

A ClientRequestToken (up to 36 characters, such as a UUID per order) makes a transaction idempotent: send the identical request again with the same token and DynamoDB returns success without applying it twice. That is how you retry safely after a timeout, when you do not know whether the first attempt landed. The AWS SDKs add a token automatically; set your own when you retry across process restarts, so a crash between "send" and "record success" cannot double-charge stock.

  • The token is valid for 10 minutes after the first request using it completes. After that, the same token is treated as a new request.
  • Same token with a different request inside those 10 minutes fails with IdempotentParameterMismatchException. A test that reused one token for two different orders reproduced it.
  • TransactionInProgressException means a request with that token is still running. AWS's recommended client settings: a request timeout of 1 second or more, exponential backoff, and an overall execution timeout that allows at least one retry after 5 seconds.

Testing with DynamoDB Local: three differences to know

DynamoDB Local is the free way to test transactions; our Docker setup guide gets it running in two minutes, and every message on this page except the conflict cases came from it. It is faithful for condition failures, ALL_OLD, idempotency tokens and the same-item rule. It differs in three places worth knowing so a passing test does not mislead you:

  • No conflicts. Twenty threads running transactions on one item all succeeded; Local serializes them. You cannot reproduce TransactionConflict locally, so test your retry path by forcing it, not by racing.
  • No throttling. Capacity codes never appear locally.
  • Some data errors come back as a plain ValidationException (for example "An operand in the update expression has an incorrect data type") where the real service lists ValidationError inside the reasons. Handle both shapes.

To test the retry path anyway, force the conflict code instead of racing for it:

  1. Wrap the client call in your test so the first attempt raises a ClientError whose response has Code: TransactionCanceledException and CancellationReasons of [{"Code": "TransactionConflict"}, {"Code": "None"}].
  2. Let the second attempt call DynamoDB Local for real.
  3. Assert the helper slept once and returned the real response, and that a ConditionalCheckFailed reason is raised on the first attempt with no retry.

Global tables and batch operations: what "atomic" does not cover

Two limits on what a transaction promises. With global tables, the all-or-nothing guarantee applies only in the Region where you made the call; while the changes replicate, another Region can briefly show some of the transaction's writes and not others. And a transaction is isolated from each individual write in a BatchWriteItem, but not from the batch as a unit, so a batch running alongside a transaction can interleave with it. If another Region or a batch job reads transaction results, design for that window.

The ten-minute fix order

  1. Read the bracket list against your request: find the position that is not None.
  2. Get the full reasons (Python CancellationReasons, CLI --cli-error-format json) and log them with the request.
  3. ConditionalCheckFailed: not a bug; add ALL_OLD to see the item and handle the outcome.
  4. TransactionConflict, ThrottlingError, ProvisionedThroughputExceeded: retry with backoff (the helper), then fix hot items or capacity if it persists.
  5. ValidationError or a plain ValidationException: fix the action's data or the request shape (same item twice, over 100 actions, over 4 MB).
  6. Add a ClientRequestToken for retries across restarts.

Fixes by code

You seeDo this
[None, ConditionalCheckFailed]Second action's condition is false; handle as an outcome, add ALL_OLD
[TransactionConflict, ...] occasionallyRetry with backoff
TransactionConflict constantlyHot item: spread it, shrink transactions
ThrottlingError / ProvisionedThroughputExceededRetry; budget double capacity for transactional writes
ValidationError in the reasonsFix that action's types, size or path
cannot include multiple operations on one itemMerge the two actions on that key
length less than or equal to 100Split; use BatchWriteItem for bulk
IdempotentParameterMismatchExceptionNew token per distinct request
CLI shows <complex value>Add --cli-error-format json

Frequently asked questions about DynamoDB TransactionCanceledException

What does TransactionCanceledException mean in DynamoDB?

One or more actions in a TransactWriteItems or TransactGetItems call could not proceed, so DynamoDB canceled every action in it. The CancellationReasons list names the failing actions and why.

How do I read "[None, ConditionalCheckFailed]"?

It is one code per action, in the same order as your TransactItems. None means that action was fine. ConditionalCheckFailed at position 1 means the second action's condition expression was false.

How do I get CancellationReasons in boto3?

Catch ClientError, check e.response["Error"]["Code"] == "TransactionCanceledException", then read e.response["CancellationReasons"], a list of dicts with Code, Message and optionally Item.

How do I see the cancellation reasons in the AWS CLI?

Add --cli-error-format json to the transact-write-items command. The default output shows only "CancellationReasons: complex value" and a hint to use that flag.

What is TransactionConflict in DynamoDB?

Another write touched one of the transaction's items while it was in progress. DynamoDB does not lock items; it cancels the transaction instead. Retry with backoff, and reduce contention if it happens constantly.

Does the AWS SDK retry TransactionCanceledException?

No. The SDKs do not retry a canceled transaction, even for TransactionConflict. Retry yourself, and only for TransactionConflict, ThrottlingError and ProvisionedThroughputExceeded, never for ConditionalCheckFailed.

What is the difference between TransactionCanceledException and TransactionConflictException?

TransactionCanceledException is thrown by a transaction that was canceled. TransactionConflictException is thrown by a plain PutItem, UpdateItem or DeleteItem that collided with an ongoing transaction on the same item.

How do I see which item failed the condition?

Set ReturnValuesOnConditionCheckFailure to ALL_OLD on the action. The failing CancellationReason then includes the item as it was stored, so you can see why the condition was false.

Why do I get "Transaction request cannot include multiple operations on one item"?

Two actions in the same transaction target the same key, such as an Update and a ConditionCheck. Merge them into a single action with a combined condition expression.

How many items can a DynamoDB transaction have?

Up to 100 actions on up to 100 distinct items, in tables of the same account and Region, with an aggregate item size up to 4 MB.

Do canceled DynamoDB transactions consume capacity?

Yes. DynamoDB performs a prepare and a commit for every item and consumes that capacity even when the transaction is canceled, including for ConditionalCheckFailed.

How much capacity does a DynamoDB transaction use?

Twice a normal write or read per item: one to prepare, one to commit. One transaction per second writing three 500-byte items needs six write capacity units.

What is ClientRequestToken in TransactWriteItems?

An idempotency key up to 36 characters. Resending the identical request with the same token within 10 minutes does not apply it twice. A different request with the same token fails with IdempotentParameterMismatchException.

Why does my transaction work in DynamoDB Local but fail in AWS?

DynamoDB Local does not simulate conflicts or throttling, so TransactionConflict, ThrottlingError and ProvisionedThroughputExceeded only appear in the real service. Test the retry path separately.

Are DynamoDB transactions atomic across Regions with global tables?

No. The all-or-nothing guarantee holds only in the Region where you called it. Other Regions can briefly see some of the transaction's writes while it replicates.

How do I fix ThrottlingError in a DynamoDB transaction?

Retry with backoff; on-demand tables throttle briefly while scaling. If it persists, look for a hot key, and remember transactions consume double capacity per item.

Should I use transactions for bulk writes?

No. Transactions cost double capacity and cancel as a unit. Use BatchWriteItem for bulk loading and keep transactions for the few items that must change together.

Why does my .NET or old JavaScript SDK not show cancellation reasons?

Older SDK versions exposed only the message. Upgrade to a current SDK, which carries CancellationReasons on the exception, or parse the bracket list from the message, which lists the codes in request order.

The shop's order step now logs the bracket list with every failure, shows "sold out" on a ConditionalCheckFailed, and quietly retries conflicts. On the next launch, the dashboard showed a few hundred conflicts and not one failed order from them. Jake asked what the difference was. "Before, we treated 'busy' and 'sold out' as the same thing," said Ethan. "Now we only tell customers the one that's true."

📌 If you keep one line from this page

The bracket list is one code per action in your request's order: never retry ConditionalCheckFailed, always retry TransactionConflict yourself because the SDK will not, and remember every canceled transaction still pays for its prepare and commit.

CancellationReasons in boto3 · --cli-error-format json in the CLI · ALL_OLD to see the item · 100 actions, 4 MB, one Region · token valid 10 minutes.

Revision note. Written October 16, 2026, with every message captured from DynamoDB Local and the AWS CLI. "If at first you don't succeed, try, try again," the proverb says; DynamoDB adds one condition: only when the reason says try again.

Related