Stop Lost Updates in DynamoDB with Conditional Writes and a Version Attribute
DynamoDB conditional writes with a version attribute prevent lost updates on single items without transactions. Learn the pattern, costs, and limits.
01 Jul 2026, 13:45 UTC

When two writers collide on the same item
Imagine two warehouse operators pulling up the same inventory record at the same time. Both read quantity: 10. Operator A sells two units and writes quantity: 8. Operator B sells three units and writes quantity: 7. The final number is wrong because A's update vanished. This is a lost update, and DynamoDB's default last-writer-wins behavior makes it easy to trigger.
You could wrap every edit in a DynamoDB transaction, but that adds latency and complexity for a single-item change. A lighter approach is optimistic concurrency control: assume conflicts are rare, detect them when they happen, and retry. DynamoDB supports this natively through conditional writes and a version attribute.
The version attribute pattern
Optimistic locking works by attaching a monotonically increasing version number to each item. The client reads the item, notes the version, performs its calculation, and then issues an update that says "only apply this change if the version is still what I read." DynamoDB evaluates that condition atomically against the stored item. If it matches, the write succeeds and the version increments. If another writer already changed the item, DynamoDB rejects the write with a ConditionalCheckFailedException. The result is serializable isolation for that single item: concurrent updates are applied one at a time, even though no locks are held.
The critical rule is that the version increment and the data change must happen in the same UpdateItem expression. A separate read-modify-write cycle reintroduces the race you are trying to eliminate.
Worked example: a conditional update
The following commands use the AWS CLI. Run them from a shell where your credentials and default region are already configured. They assume a standard DynamoDB table with a string partition key and a recent AWS CLI version, and they require dynamodb:UpdateItem permission on the target table. Replace <table-name> and <pk-value> with your own values.
First, seed an item with version = 1:
aws dynamodb put-item \\
--table-name <table-name> \\
--item '{\n \"pk\": {\"S\": \"<pk-value>\"},\n \"quantity\": {\"N\": \"10\"},\n \"version\": {\"N\": \"1\"}\n }'
Now perform a conditional update that only succeeds if the version is still 1:
aws dynamodb update-item \\
--table-name <table-name> \\
--key '{\"pk\": {\"S\": \"<pk-value>\"}}' \\
--update-expression \"SET quantity = :new_qty, version = version + :one\" \\
--condition-expression \"version = :expected\" \\
--expression-attribute-values '{\n \":new_qty\": {\"N\": \"8\"},\n \":one\": {\"N\": \"1\"},\n \":expected\": {\"N\": \"1\"}\n }' \\
--return-values ALL_NEW \\
--return-consumed-capacity TOTAL
If you run the exact same command a second time, DynamoDB returns a ConditionalCheckFailedException because the stored version is now 2. That failure is your signal to re-read the item and retry with the new version. The --return-consumed-capacity TOTAL flag lets you confirm in the response metadata that even the rejected write consumed write capacity.
Trade-offs: cost, contention, and scope
Conditional writes are not free. A rejected update still consumes write capacity units in provisioned mode or write request units in on-demand mode. Under heavy contention on a single item, repeated failures can inflate your bill or exhaust provisioned throughput. Your retry loop should therefore use exponential backoff with jitter and a bounded number of attempts.
Read consistency matters too. If you fetch the version with an eventually consistent read, you may act on stale data and suffer avoidable failures. A strongly consistent read costs more but reduces spurious retries.
The biggest limitation is scope. A ConditionExpression only sees the single item being written. It cannot enforce uniqueness across a partition, validate a cross-item invariant, or coordinate two items. For those cases, DynamoDB Transactions are the supported mechanism.
Practical checklist
- Add a numeric
versionattribute to items that receive concurrent edits. - Read the item with a strongly consistent read when you need to minimize retries.
- Always increment the version inside the same
UpdateItemcall that changes the data. - Handle
ConditionalCheckFailedExceptionby re-reading and retrying with backoff. - Monitor consumed capacity and exception rates; if contention stays high, consider sharding the item or moving to transactions.
Optimistic locking with conditional writes gives you serializable isolation for a single item without the overhead of a transaction. It is a pragmatic default for counters, inventory reservations, and profile edits, as long as you respect its boundaries.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.