DynamoDB Atomic Counters
An atomic counter is a numeric attribute you bump in place with a single
UpdateItem call — no read first, no read-modify-write race. DynamoDB applies
each increment in arrival order and never lets two writers clobber each other's
count.
What is a DynamoDB atomic counter?
A DynamoDB atomic counter is a numeric attribute you increment in place with a single UpdateItem call using an ADD (or SET x = x + :n) update expression. DynamoDB reads, adds, and writes the value server-side, so concurrent writers serialize without lost updates — but it is not idempotent, so a retried call increments twice.
- Use
ADD(orSET x = x + :n) to increment in one call. DynamoDB reads, adds, and writes server-side — concurrent callers serialize, no lost updates. - No read first. Coming from SQL you'd
SELECTthenUPDATE; here you skip the read entirely and the operation is still safe under concurrency. - Atomic counters are not idempotent. A retried
UpdateItemincrements again. If you can't tolerate over- or undercounting, use a . ADDon a missing attribute starts at 0, so the very first increment just works — no seed write needed.
The problem with read-modify-write
Say you track views on a video. The naive instinct, straight from SQL, is:
GetItem, add one in your app, PutItem the new total back.
Two viewers hit play at once. Both read views = 41. Both write 42. You
counted one view, not two. That's a lost update — the classic concurrency
footgun, and it doesn't show up until you have traffic.
In SQL you'd dodge it with UPDATE videos SET views = views + 1, pushing the
arithmetic into the database. DynamoDB has the same move, and it's the whole
point of an atomic counter.
Increment in one call
Model a per-video stats item. Partition key VID#<id>, sort key STATS#TOTAL,
with a numeric play_count:
| PK | SK | play_count |
|---|---|---|
| "VID#9f3a" | "STATS#TOTAL" | 41 |
To register a play, send one UpdateItem with an ADD clause:
# UpdateItem
Key PK = "VID#9f3a", SK = "STATS#TOTAL"
UpdateExpression ADD play_count :one
Values :one = 1
DynamoDB reads play_count, adds 1, and writes the result inside a single
server-side operation. There's no window for another writer to slip in. Ten
concurrent plays produce +10, every time — that's what "atomic" buys you.
You can build and copy this exact expression — names, values, and all four clause types — with the DynamoDB Expression Builder.
ADD works even when play_count doesn't exist yet: DynamoDB treats a missing
numeric attribute as 0, so the first play creates it at 1. No separate seed
write. (AWS: Using update expressions)
ADD vs SET +: pick one
Two expressions do the same arithmetic. AWS recommends SET for general use
because it composes with other SET actions and reads more explicitly. (AWS:
Using update expressions)
ADD play_count :one | SET play_count = play_count + :one | |
|---|---|---|
| Missing attr | Creates it, starting at 0 | Errors — needs if_not_exists |
| Data types | Numbers and sets only | Numbers (and more) via SET |
Combine w/ SET | Separate clause | One SET clause, comma-separated |
| AWS guidance | Fine for counters | Recommended default |
If the attribute might not exist and you want SET, guard it:
SET play_count = if_not_exists(play_count, :zero) + :one. With ADD you skip
that — it seeds from 0 for free.
Do it in DynoTable
Open the item, edit play_count, and you can watch an atomic increment land
without hand-writing JSON — the update panel emits the ADD expression for you
and shows the new value the moment it commits.
The trap: counters aren't idempotent
Here's the part that bites teams in production. An atomic counter increments
every time UpdateItem runs. (AWS: Working with items)
Picture a network blip: you send the increment, the connection drops before the response comes back, and you don't know whether it landed. You retry. If the first call did succeed, you've now counted that play twice.
For video views that's fine — a few double-counts in a million plays won't hurt anyone, and AWS calls this exact "track visitors" case the canonical use of atomic counters. (AWS: Working with items)
It is not fine for anything that must be exact: inventory you can oversell, credits you can double-spend, a balance you can corrupt. There, reach for a conditional update.
When you need exactness: conditional updates
A conditional update is idempotent if you condition on the same attribute you're
changing. Increment play_count to 42, but only if it's currently 41:
# UpdateItem
Key PK = "VID#9f3a", SK = "STATS#TOTAL"
UpdateExpression SET play_count = :next
ConditionExpression play_count = :current
Values :next = 42, :current = 41
Now a retry is safe: if the first write already moved play_count to 42, the
condition play_count = 41 fails the second time and nothing changes. (AWS:
Working with items)
The cost is concurrency. Two writers racing on the same condition means one wins
and one gets a ConditionalCheckFailedException to retry — you've traded the
unconditional counter's throughput for correctness. For exact, contended
counters that's the right trade. For view counts it's overkill.
Pitfalls
- One . A single counter row is one partition key. A viral video
hammering
VID#9f3a/STATS#TOTALcan hit a per-partition write ceiling. Shard it: spread writes acrossSTATS#TOTAL#0..Nand sum on read. - No batch increment.
BatchWriteItemis put/delete only — it can't run . Counters go throughUpdateItem, one item per call. ADDis numbers and sets only. It won't touch strings or booleans; that's aSET. See DynamoDB data types for the full attribute model.
Next steps
Atomic counters are a write pattern; how you read aggregates back is a modeling
question — see single-table design for keeping
stats items beside their parent, and Query vs Scan so
rolling up a sharded counter stays a Query.
Draft and copy the increment in the DynamoDB Expression Builder, then try DynoTable to run atomic updates against your own tables and watch the counts move.