DEV Community

Cover image for DynamoDB Atomic Counters
DynoTable
DynoTable

Posted on • Originally published at dynotable.com

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 (or SET 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 SELECT then UPDATE; here you skip the read entirely and the operation is still safe under concurrency.
  • Atomic counters are not idempotent. A retried UpdateItem increments again. If you can't tolerate over- or undercounting, use a conditional update.
  • ADD on 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 UpdateItem blindly 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#TOTAL can hit a per-partition write ceiling. Shard it: spread writes across STATS#TOTAL#0..N and sum on read.
  • No batch increment. BatchWriteItem is put/delete only — it can't run update expressions. Counters go through UpdateItem, one item per call. If you must bump several counters atomically, TransactWriteItems runs Update actions on up to 100 items in one request, at roughly double the write cost.
  • ADD is numbers and sets only. It won't touch strings or booleans; that's a SET. 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)