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 conditional update. -
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:
| Example | Notes |
|---|---|
# 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 stats item to inspect the live counter, then roll up a sharded counter
with SUM and GROUP BY in the SQL Workbench to see the total across every
STATS#TOTAL#0..N row. To draft the increment itself, use the web
DynamoDB Expression Builder to compose the
ADD UpdateItem expression, names and values included.
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.
Warning
Don't retry an atomic-counter
UpdateItemblindly when exactness matters. A
"failed" request may have committed. Each retry risks another increment.
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:
| Example | Notes |
|---|---|
# 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 hot item. 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 update expressions. Counters go throughUpdateItem, one item per call. If you must bump several counters atomically,TransactWriteItemsruns Update actions on up to 100 items in one request, at roughly double the write cost. -
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.
Top comments (0)