Course outline

15.4

DynamoDB: Keys, Capacity and Consistency

Tables and items, partition and sort keys, RCU/WCU, on-demand vs provisioned, consistency and hot partitions.

30 min Reading + animations

Tip: tap or hover any dotted-underlined term to see what it means.

By the end of this lesson you will be able to

  • Say what Amazon DynamoDB is (a serverless NoSQL key-value and document database, replicated across three AZs, secured with IAM) and when it fits better or worse than a relational database.
  • Describe tables, items and attributes, the 400 KB item limit and the data types, and design a primary key: a partition key alone, or a partition key plus a sort key for one-to-many data and range queries.
  • Explain how the partition key places items on partitions, why a low-cardinality key creates a hot partition, and what the per-partition ceiling of 3,000 RCU and 1,000 WCU means.
  • Tell eventually consistent reads from strongly consistent reads (how they work on three replicas, what each costs, where strong reads are not available) and pick the right one.
  • Calculate RCU and WCU for any item size, read type and write type, including the 4 KB / 1 KB rounding and the double cost of transactions, and turn them into a monthly bill.
  • Use Query instead of Scan, know what filter expressions do and do not save, and keep big scans from hurting live traffic.
  • Choose between provisioned capacity (with auto scaling) and on-demand capacity, and know their limits: auto scaling lag, burst capacity, the double-the-previous-peak rule, pre-warming, the switching rules.
  • Diagnose ProvisionedThroughputExceededException and the classic hot-partition exam question, and fix it with DAX, key design, write sharding, on-demand or backoff as appropriate; place adaptive capacity, transactions, PartiQL and the Standard-IA table class.

#What DynamoDB is

Chapter 7 taught relational databases: RDS and Aurora, with tables, joins, SQL and an instance you choose and size. Many serverless applications want something else: a database with no server to pick, patch or scale, that answers in a few milliseconds whether it holds a thousand items or a billion, and that bills per request. That is Amazon DynamoDB.

  • NoSQL, key-value and document. You store items and read them back by key. There are no joins and no free-form SQL queries across tables; you design the keys around the questions the application will ask.
  • Fully managed and serverless. No instances, no storage to allocate, no maintenance windows, no patching. You create a table and use it.
  • Highly available. Every write is stored three times, in three Availability Zones of the Region, before it is acknowledged (more on this below). Losing an AZ loses no acknowledged data.
  • Scales to huge workloads. Millions of requests per second, trillions of items, hundreds of terabytes in one table, with consistent single-digit millisecond latency. A table has no practical size limit.
  • Security with IAM. There are no database users or passwords: every request is an AWS API call signed with IAM credentials, so an IAM policy (or the Lambda function’s execution role) decides who can read or write which table. Data is always encrypted at rest.
  • Cost. Pay per request (on-demand) or per hour for capacity you provision, plus storage. A free tier of 25 RCU, 25 WCU and 25 GB per Region (provisioned, Standard table class) covers small apps for ever.

The notes’ one-line summary from the database chapter is the exam’s too: “serverless key-value store with massive scale” → DynamoDB, millisecond latency; with microsecond latency it becomes DynamoDB + DAX (lesson 15.5).

RDS / Aurora (lessons 7.1–7.4)DynamoDB

  • Relational: fixed schema, SQL, joins, ad-hoc queries, complex reporting.
  • You pick an instance class; it scales up by changing size and out with read replicas.
  • Connections, users and passwords (or IAM auth); lives inside your VPC.
  • Best when the questions you will ask are not all known in advance, or the data is deeply related.
  • Key-value and document: only the key is fixed; every item can have different attributes; no joins.
  • No instance: capacity is a number (or pay per request) and storage grows by itself.
  • HTTPS API calls signed with IAM; a public AWS endpoint (or a VPC gateway endpoint, lesson 15.5).
  • Best when the access patterns are known (get the user by ID, list a driver’s trips by time) and must stay fast at any scale.

#Tables, items and attributes

ConceptWhat it isRelational equivalent
TableA named collection of items in one Region. Its primary key is chosen at creation and can never change.Table
ItemOne record, identified by its primary key. A table can hold an unlimited number of items.Row
AttributeA name and a value inside an item. Apart from the key, nothing is declared in advance: attributes can be added over time, and two items in one table can have completely different ones.Column

Because only the key is fixed, DynamoDB is called schema-less: you can add a loyalty_points attribute to new items tomorrow without any migration, and old items simply do not have it (a missing attribute is not even stored as null).

Data types. An attribute value is one of:

  • Scalar: String, Number, Binary, Boolean, Null.
  • Document: List (an ordered list of any values) and Map (nested name-value pairs, like a JSON object), nested up to 32 levels.
  • Set: String Set, Number Set, Binary Set (unique values of one type).

The 400 KB limit. One item, attribute names and values together, can be at most 400 KB. Long attribute names therefore cost space on every item. Large things (photos, PDFs, long logs) go to S3, and the item stores the object key plus a little metadata. Small items are also cheaper, because capacity is charged by item size.

#An example table

A quiz-game app in Dhaka keeps one item per player per game:

user_id (partition key)game_id (sort key)scoreresultother attributes
u-7791g-442192winstreak: 3
u-7791g-518014lose
u-8730g-189477winbadge: “Eid champion”
u-8730g-4521(game started, not finished: no score yet)

Two rows share u-7791 and two share u-8730: they are separate items because the pair (user_id, game_id) is unique. The last item has no score at all, and that is allowed. On the wire, items travel as typed JSON:

{ "user_id": {"S": "u-7791"}, "game_id": {"S": "g-4421"},
  "score": {"N": "92"}, "result": {"S": "win"}, "streak": {"N": "3"} }

#Primary keys: partition key and sort key

Every table has one of two kinds of primary key, and the choice decides what you can query fast.

#Partition key only (simple primary key)

One attribute, for example user_id. Its value must be unique in the table: a second PutItem with the same user_id replaces the first item. DynamoDB runs the value through an internal hash function, and the result decides which partition (physical storage) the item lives on; that is why the API calls it the HASH key. You read an item with GetItem and the exact key.

#Partition key plus sort key (composite primary key)

Two attributes, for example driver_id + trip_time. Many items can share a partition key, as long as the combination is unique. All items with the same partition key are stored together on one partition as an item collection, sorted by the sort key (the RANGE key). That gives you two powers a simple key does not have:

  • One-to-many: one driver, thousands of trips, all fetched with one Query.
  • Range queries on the sort key: between two times, begins_with a date, <, >, newest first or oldest first.

The notes’ exam trick: “one-to-many relationship” or “range query” (data between two dates) → composite primary key. And always choose a partition key with high cardinality (many distinct values, such as user_id) so the load spreads and no partition gets hot.

#How the partition key places items

A table is split into partitions, each holding a range of hash values, stored on SSDs and replicated three times. DynamoDB adds partitions as the table grows (each partition holds about 10 GB) or as throughput grows, and moves data for you. You never see or manage them, but you must know two numbers, because they do not change with the table’s total capacity:

  • one partition serves at most 3,000 read units and 1,000 write units per second;
  • and so does one item: a single key can never exceed those numbers.

Where an item lives: the partition key decides

A ride-sharing app in Dhaka stores users and trips in DynamoDB. The request router hashes the partition key of every item and sends it to one partition. Watch a simple key, a composite key with a range query, a badly chosen key, and an item that is too big.

Scenario

Users table, key user_id: one item per key, found in one hop.

Ride app
Request routerhash(partition key)
Partition 1
Partition 2
Partition 3
PutItem u-1001

Item

user_id

u-1001

name

Hasan

phone

+880 1700-000000

Step 1 of 4
1

The app saves a new rider with PutItem. The Users table has a simple primary key: the partition key user_id alone. There is no schema for the other attributes; this item happens to carry a name and a phone number.

#Choosing a good partition key

Partition keyCardinalityVerdict
user_id, order_id, device_idMillions of values, requests spread evenlyGood
driver_id + sort key trip_timeMany drivers; each driver’s trips togetherGood, for “one driver’s trips by time”
status (3 values), country (almost everyone in Bangladesh)A handful of valuesBad: hot partition
date (today’s date)All of today’s writes on one keyBad: today is always hot
product_id during a saleMany values, but one product gets most readsGood key, hot item: needs a cache (below)

#Reading and writing items

The everyday API (all of it is HTTPS calls signed with IAM):

  • PutItem writes a whole item (create or replace). Add a condition expression such as attribute_not_exists(user_id) to refuse overwriting; a failed condition returns ConditionalCheckFailedException and still consumes write capacity.
  • GetItem reads one item by its full primary key.
  • UpdateItem changes some attributes (or creates the item), including atomic counters (SET views = views + 1). Conditions on a version attribute give optimistic locking: “update only if version is still 7”.
  • DeleteItem removes one item.
  • BatchGetItem (up to 100 items, 16 MB) and BatchWriteItem (up to 25 puts or deletes) save round trips but are not all-or-nothing: unprocessed items come back for you to retry.
  • Query and Scan read many items (their own section below).

#Consistency: eventually or strongly consistent reads

To understand the two kinds of read, you need the way DynamoDB stores data. AWS documents that every item is stored in three Availability Zones; the finer details below come from the research paper AWS engineers published on DynamoDB’s internals in 2022, so read them as a model of how it works inside, not as an API promise. Every partition has three replicas, one in each of three AZs, and one replica is the leader for that partition. A write goes to the leader, the leader copies it to the other two, and the write is acknowledged (200 OK) once a quorum, two of the three, has written it durably to its log. The third catches up a few milliseconds later. That small gap is the whole story:

  • Eventually consistent read (the default for GetItem, Query and Scan): any of the three replicas may answer. If it is the one that is a few milliseconds behind, you get the previous value. Read again a moment later (usually within a second) and you see the new one. Costs half: one RCU gives two eventually consistent reads per second of an item up to 4 KB. Lowest latency, and still works when one replica is unavailable.
  • Strongly consistent read (ConsistentRead = true on the request): served by the leader, so it always reflects every write that was acknowledged before the read. Costs one full RCU per 4 KB, has slightly higher latency, and if the leader is unreachable for a moment (network trouble, a replica failing over) the read can fail with a server error (HTTP 500) that the SDK retries, while eventual reads keep working.

Strongly consistent reads exist only on tables and local secondary indexes. Global secondary indexes, DynamoDB Streams, the DAX item cache and (by default) the other Regions of a global table are always eventually consistent (all in lesson 15.5).

The notes describe a strong read as “DynamoDB first checks that the data has reached all three nodes”. The effect is right (you always see the latest acknowledged write), but the mechanism is the leader replica, not a check of all three: that is also why a strong read does not wait for the slowest replica.

Three copies, two kinds of read

Every partition is stored three times, in three Availability Zones. One replica is the leader and takes the writes. Hasan changes his profile name from Ali to Hasan; watch which read sees which name.

Scenario

The leader takes the write; 200 OK means it is durable.

AZ a
AZ b
AZ c
Profile screenwrites
Another screenreads
Request router
Replica AAli
Replica BleaderAli
Replica CAli
Step 1 of 4
1

Before the change, all three replicas hold name = Ali.

#Capacity units: RCU and WCU

DynamoDB measures throughput in units, and both capacity modes use the same arithmetic (provisioned mode calls them capacity units, on-demand calls them request units).

  • 1 RCU (read capacity unit) = one strongly consistent read per second of an item up to 4 KB, or two eventually consistent reads per second. A transactional read needs two RCU per 4 KB.
  • 1 WCU (write capacity unit) = one write per second of an item up to 1 KB. A transactional write needs two WCU per 1 KB.

#The rounding rules

  1. Round the item size up: to the next 4 KB for reads, to the next 1 KB for writes. A 4.1 KB item costs as much to read as an 8 KB item; a 2.5 KB item costs 3 WCU to write.
  2. Multiply by the read type: eventual × 0.5, strong × 1, transactional × 2. Writes: standard × 1, transactional × 2.
  3. Multiply by the requests per second.

Details that show up in questions:

  • GetItem and BatchGetItem round each item separately. Query and Scan add up the sizes of all items read and round the total once, so reading many small items with a Query is far cheaper than fetching them one by one.
  • UpdateItem is charged on the larger of the item’s size before and after the update, not on the size of the change: incrementing a counter in a 3 KB item costs 3 WCU.
  • A projection (asking for only some attributes) does not reduce read units: the whole item is read.
  • A write whose condition fails still consumes write units.

#Worked examples

WorkloadPer requestUnits per second
10 strongly consistent reads/s of 6 KB items6 → 8 KB = 2 blocks × 1 = 2 RCU20 RCU
80 eventually consistent reads/s of 6 KB items2 blocks × 0.5 = 1 RCU80 RCU
50 transactional reads/s of 3 KB items1 block × 2 = 2 RCU100 RCU
12 writes/s of 2.5 KB items2.5 → 3 KB = 3 WCU36 WCU
10 transactional writes/s of 1.5 KB items2 KB × 2 = 4 WCU40 WCU
One Query returning 12 items of 0.5 KB, eventual6 KB total → 8 KB = 2 blocks × 0.51 RCU for the whole Query

To turn units into money (US East, Standard table class): provisioned capacity costs 0.00013 USD per RCU-hour and 0.00065 USD per WCU-hour; on-demand costs 0.125 USD per million read request units and 0.625 USD per million write request units. The calculator below does both for the Dhaka shop’s Orders table.

RCU and WCU calculator

A Dhaka shop’s Orders table. Choose the item size, the traffic and the kind of reads and writes, then calculate the capacity units and a 30-day bill in each capacity mode (US East prices, Standard table class).

Item size

Busy hours per day

Reads per second

Kind of read

Writes per second

Kind of write

Press “Calculate” to see the result.

With the default settings (6 KB items, 100 strongly consistent reads and 5 writes per second, all day) the table needs 200 RCU and 30 WCU. For 30 days that is about 32.76 USD provisioned versus 113.40 USD on-demand: steady, fully used capacity is roughly 3.5 times cheaper provisioned. Set the busy hours to 2 and the picture flips: on-demand falls to about 9.45 USD while a fixed provisioned table still pays 32.76 USD for capacity that idles 22 hours a day.

#Query and Scan

Before reading many items, ask: do I know the partition key?

Query reads items from one partition key value. You give partition key = value (required, exactly one value) and optionally a sort key condition (=, <, <=, >, >=, between, begins_with). DynamoDB goes to one partition, reads one contiguous, sorted slice, and returns it in sort-key order (ScanIndexForward = false for descending). It is fast and you pay only for what that slice contains.

Scan reads every item in the table (or index), partition after partition, and returns them in no useful order. Its cost is the size of the whole table, whatever you get back.

Both return at most 1 MB of data read per call (before any filter). If there is more, the response includes LastEvaluatedKey, and you call again with it as the starting key; the CLI and SDK paginators do this for you. Limit caps the number of items read per call.

Filter expressions (fare > 500, city = Sylhet) are applied after the items are read. They reduce what is sent back over the network, not the capacity consumed: a Scan that reads 2 million items to return 300 is billed for 2 million items. A filter on a Query is cheaper only because the Query read less to begin with.

A parallel scan splits the table into segments (Segment 0 to N-1 of TotalSegments N) and lets N workers scan them at the same time: same items, same cost, finished N times faster, and N times the capacity drawn per second. Run big scans off-peak, cap each worker with Limit, or export the table to S3 and query it with Athena (lesson 15.5 and lesson 17.1), which uses no read capacity at all.

Query or Scan: one shelf or the whole warehouse

The Trips table holds 2 million trips (about 2 GB) on four partitions. Compare what each kind of read touches, and what you pay for.

Scenario

Partition key given: one partition, only the matching items.

Ride app
Request router
P1
P2
P3
P4
Query d-17
Step 1 of 3
1

Show driver d-17’s trips today: Query with driver_id = d-17 and trip_time beginning with 2026-10-28. A Query must name exactly one partition key value; the sort key condition is optional.

The numbers in the animation: 12 items of 0.5 KB read by a Query is 6 KB, rounded to 8 KB, so 2 RCU strongly consistent or 1 RCU eventually consistent. A Scan of a 2 GB table, eventually consistent, is 2 GB ÷ 4 KB = 524,288 blocks × 0.5 ≈ 262,000 RCU. On a table provisioned at 1,000 RCU that is over four minutes of its entire read capacity, enough to throttle every live request while it runs.

#Capacity modes: provisioned or on-demand

#Why capacity goes up and down

Two reasons, from the notes: handle traffic and save money. During a flash sale, hundreds of thousands of shoppers read product items and place orders at once; if the table’s capacity is lower than the traffic, requests are throttled (rejected with ProvisionedThroughputExceededException), so capacity must go up. At 2 AM almost nobody shops; keeping the evening’s capacity means paying for capacity nobody uses, so it should go down. DynamoDB offers two ways to manage this, the capacity modes.

#Provisioned capacity (with auto scaling)

You set the number of RCU and WCU per second the table (and each GSI) may use, and you pay for them every hour, used or not. Requests above the setting are throttled.

Changing the numbers by hand as traffic moves is tedious, so you add auto scaling (Application Auto Scaling with a target tracking policy): a minimum (say 100), a maximum (say 4,000) and a target utilization (say 70%, allowed 20–90%).

  • When consumed capacity stays above the target for two consecutive minutes (DynamoDB reports it in one-minute data points, and the alarm can add a delay of up to a few minutes), a CloudWatch alarm fires and Application Auto Scaling calls UpdateTable to raise capacity so that utilization returns to the target: 840 consumed RCU at a 70% target becomes 840 ÷ 0.7 = 1,200 RCU. The update takes a few minutes, with no downtime.
  • When utilization stays below target for 15 consecutive data points (about 15 minutes), it scales down, never below the minimum. Decreases are rationed: 4 at the start of each UTC day plus one more each hour, 27 per day at most. Increases are unlimited.
  • Burst capacity: DynamoDB keeps up to 300 seconds of unused capacity in reserve and spends it on short spikes. It covers seconds, not minutes.
  • So auto scaling follows predictable or gradual change; a steep, sudden spike outruns it and throttles in the gap.

Provisioned mode is the cheapest throughput when traffic is steady and well used, and reserved capacity (1- or 3-year commitment, Standard table class) cuts it further. The always-free tier (25 RCU, 25 WCU) also applies to provisioned mode only.

#On-demand capacity

No RCU or WCU to set and nothing to scale: you pay per read request unit and write request unit actually used (same arithmetic as RCU and WCU), and nothing for throughput when there is no traffic. Since November 2024, when AWS cut on-demand prices by 50%, AWS calls on-demand the default and recommended mode for most workloads, and the console preselects it.

“No capacity planning” is not “no limits”. What on-demand actually promises:

  • A new on-demand table can immediately sustain up to 4,000 writes and 12,000 reads per second.
  • It instantly serves up to double its previous peak. Traffic above that is accommodated as DynamoDB adds capacity, but growth of more than double the previous peak within 30 minutes can be throttled.
  • For a known event (an Eid launch, a ticket sale), pre-warm the table: since November 2024 you can raise its warm throughput (UpdateTable) to the level you expect, or grow traffic over at least 30 minutes.
  • The default table quota is 40,000 read and 40,000 write request units per second (adjustable). You can also set a maximum throughput per on-demand table as a cost guard.
  • The per-partition and per-item ceilings (3,000 / 1,000) still apply: on-demand does not cure a hot key.

#Switching between modes

The notes say you can switch between modes once every 24 hours. That rule has been relaxed: today you can switch a table from provisioned to on-demand up to four times in a rolling 24-hour window, and from on-demand to provisioned at any time. Older exam questions may still say “once per 24 hours”; if an option relies on switching back and forth daily, both rules allow it.

Provisioned (+ auto scaling)On-demand

  • You set RCU / WCU (min, max, target 70%); pay per hour for what is set.
  • Cheapest for steady, predictable traffic with high utilization; reserved capacity; free tier.
  • Auto scaling reacts in minutes; burst capacity covers seconds; sudden spikes throttle.
  • Throttles above the setting (ProvisionedThroughputExceededException).
  • Nothing to set; pay per request; zero traffic costs only storage.
  • Best for unpredictable, spiky or unknown traffic, new apps, dev and test, tables that idle most of the day. Default and recommended since 2024.
  • Instant up to 2 × previous peak; beyond that within 30 minutes may throttle; pre-warm for known events.
  • Per unit about 3.5 times the price of fully used provisioned capacity, so cheaper whenever provisioned capacity would sit below roughly 30% utilization.

Provisioned with auto scaling, or on-demand

An online shop’s Products table through a normal day, a sudden flash sale, a quiet night and an Eid launch. The table under the stage shows the reads per second asked for and the capacity the table had at that moment.

Scenario

Target tracking keeps used capacity near 70%.

Shoppers
Products table500 RCU
CloudWatch alarm
Application Auto Scaling
300 reads/s

Products table

TimeTrafficCapacityResult
09:0030050060% used, fine
Step 1 of 4
1

Provisioned mode: you set the RCU and WCU and pay for them every hour, used or not. Auto scaling is on: minimum 100, maximum 4,000 RCU, target utilization 70%. At 9 AM the table has 500 RCU and shoppers use 300.

#Throttling and hot partitions

#What throttling looks like

When a request needs more capacity than is available, DynamoDB rejects it with HTTP 400 and ProvisionedThroughputExceededException (the name is historical: on-demand tables return it too when a partition or the table maximum is exceeded). The AWS SDKs retry throttled requests automatically with exponential backoff and jitter (wait about 50 ms, then 100, then 200, each time a random fraction so thousands of clients do not retry in lockstep). Short bursts are absorbed this way; if throttling never stops, retries only add load and the real fix is capacity or key design. CloudWatch shows it as ReadThrottleEvents / WriteThrottleEvents, and CloudWatch Contributor Insights for DynamoDB lists the most-accessed and most-throttled keys.

There are two different causes, and the fixes differ:

CauseSymptomWhat fixes it
The table is short of capacity: total traffic exceeds provisioned RCU/WCUAll keys throttle; consumed capacity at 100%Auto scaling, a higher maximum, on-demand, pre-warming
A hot partition / hot key: a few keys get most of the trafficThrottles while the table’s total capacity is mostly unusedKey design, a cache (DAX) for hot reads, write sharding or a queue for hot writes

#Burst and adaptive capacity

Adaptive capacity is always on and free. When access is uneven, it instantly lets a busy partition use unused capacity from the rest of the table, and if a few items stay hot it moves them onto partitions of their own so they do not share limits with neighbours. What it cannot do is lift the ceiling: one partition, and therefore one item, still gets at most 3,000 RCU and 1,000 WCU per second. Together with burst capacity, this is why moderate skew rarely hurts, and extreme skew still does.

#The Eid sale question

The notes’ classic exam question, set in Dhaka: an e-commerce site uses DynamoDB. During last year’s Eid sale (the original says Christmas) a few very popular items were read very often and the table threw ProvisionedThroughputExceededException. How do you prevent it this year?

Root cause. Everyone was reading the same few product items. Each item lives on one partition, so those partitions received far more reads than they can serve (a hot partition, or hot key), even though the table as a whole had capacity to spare. The worked numbers: 8,000 eventually consistent views per second of a 2 KB product item need 4,000 RCU on one key; the ceiling is 3,000, so about a quarter of the reads fail.

The answers, in the order the exam wants them:

  1. DynamoDB Accelerator (DAX). An in-memory, write-through cache built for DynamoDB that speaks the same API: the app swaps the client and endpoint, nothing else. The first read of the item is a miss that DAX fetches from the table; the next thousands are hits served from memory in microseconds. The hot key stops reaching the table, throttling stops and the RCU bill drops. “Read often”, “popular items”, “hot partition on reads”, “microsecond latency”, “minimal code changes” → DAX. (ElastiCache would also cache, but needs application code for every cache read and invalidation. DAX in depth: lesson 15.5.)
  2. On-demand mode, or auto scaling with a higher maximum (the notes’ second and third answers). These fix a table that is short of capacity overall, and an exam question that only offers them expects one of them. But they do not lift the 3,000 RCU per-item ceiling, so for a truly hot item they are not enough on their own. The notes’ “on-demand never throttles” is too strong.
  3. Better key design for skew you create yourself (a status or date key): use a high-cardinality partition key.
  4. Exponential backoff on the client: right for brief spikes, not a cure.

If the hot traffic is writes, DAX does not help: it writes through to the table, so every vote or counter update still lands on the same key. For a reality show where 2,500 viewers per second vote for contestant 7, use write sharding (add a random suffix 1–10 to the key, votes#7#1 … votes#7#10, so each shard takes about 250 writes per second, and add the ten shards up when reading), or put the votes in an SQS queue (lesson 13.1) and let a consumer write them at a steady rate, or aggregate in memory and write totals.

The Eid sale hot partition, and what fixes it

Last Eid the shop’s table threw ProvisionedThroughputExceededException although it had plenty of capacity in total. A few very popular products were read again and again. Watch why, and compare the fixes.

Scenario

One item gets more reads than one partition can serve.

Shoppers
Product API
P1
P2: p-501
P3
8,000 views/s
Step 1 of 3
1

The table is provisioned with 12,000 RCU. At midnight the Eid offer on one phone, item p-501, goes live, and 8,000 shoppers per second open its 2 KB page.

#Side notes: transactions, SQL syntax and table classes

#Transactions

DynamoDB supports ACID transactions across items and tables in one account and Region. TransactWriteItems groups up to 100 put, update, delete and condition-check actions (up to 4 MB) that succeed or fail together; TransactGetItems reads up to 100 items as one consistent snapshot. Example: an order that decrements stock in Products, inserts the order in Orders and charges the wallet in Wallets, all or nothing. They cost double (2 WCU per 1 KB written, 2 RCU per 4 KB read), because each item is prepared and then committed, and a cancelled transaction is still charged. Use them where money or stock is involved, not for every write.

#PartiQL

DynamoDB also accepts a SQL-compatible query language, PartiQL (ExecuteStatement, BatchExecuteStatement, ExecuteTransaction, and a query editor in the console):

SELECT * FROM "Trips" WHERE driver_id = 'd-17' AND trip_time BETWEEN '2026-10-28T08:00' AND '2026-10-28T12:00'

It is only a different syntax for the same operations: a WHERE on the partition key becomes a Query, and a SELECT without one becomes a full Scan, with the same cost. There are still no joins.

#Table classes: Standard and Standard-IA

Every table has a table class. Standard (the default) is right for most tables. Standard-Infrequent Access (Standard-IA) stores data for 60% less (0.10 instead of 0.25 USD per GB-month in US East) but charges about 25% more per read and write. It pays off when storage is the dominant cost, roughly when storage exceeds half of the throughput cost: order history, old posts, logs, past game achievements. Performance, durability and features are the same, the API does not change, and you can change a table’s class twice in any 30 days. Standard-IA is not covered by the free tier or reserved capacity.

#What goes wrong

SymptomLikely causeFix
ProvisionedThroughputExceededException on every key, consumed capacity at 100%Table short of capacityAuto scaling with a higher maximum, on-demand, pre-warm before events
Throttling while most of the table’s capacity is unusedHot partition or hot itemKey with higher cardinality; DAX for hot reads; write sharding or SQS for hot writes
Throttling for a few minutes at the start of every flash saleAuto scaling lags a steep spike; burst capacity used upRaise the minimum before the sale, scheduled scaling, on-demand + pre-warm
On-demand table throttles at a launchTraffic more than doubled the previous peak within 30 minutes, or hit the 40,000-unit table quotaPre-warm (warm throughput), ramp up, request a quota increase
The app shows the old value right after savingEventually consistent read after a writeConsistentRead = true where it matters, or read your own write from the response
ConsistentRead rejected on an indexStrong reads requested on a GSIGSIs are eventual only; read the table or use an LSI
ValidationException on PutItemItem larger than 400 KB (names count)Store the blob in S3, keep its key in the item; shorten attribute names
A second PutItem silently replaced dataSame primary key, no conditionattribute_not_exists condition; optimistic locking with a version attribute
A report job makes the live app slowScan or parallel scan consuming the table’s capacityRun off-peak with Limit; export to S3 and use Athena; build a GSI for the access pattern
Bill much higher than expectedScans with filters, large items, strong reads where eventual would do, idle provisioned capacityQuery instead of Scan, smaller items, eventual reads, on-demand or scale-down
A batch call “succeeds” but some items are missingBatchWriteItem returned unprocessed items under throttlingRetry UnprocessedItems with backoff (the SDK helpers do it)

#DynamoDB in system design

  • The default serverless database. API Gateway → Lambda → DynamoDB (lesson 15.6) has no server anywhere and scales from zero; the Lambda execution role grants table access (lesson 15.1). No connection pool to exhaust, unlike RDS (lesson 15.3).
  • Session state and carts for stateless EC2 or container fleets (the chapter 9 stateless-app pattern): session_id as the key, TTL to expire them (lesson 15.5).
  • Design keys from access patterns. List the questions first (“get user”, “a driver’s trips by time”, “orders by status”), then pick the partition key, sort key and indexes that answer each with a Query. If you cannot write the list, a relational database may suit better.
  • Spread load. High-cardinality keys; caches for hot reads; sharding or queues for hot writes; on-demand for unknown traffic.
  • Choose consistency per read, not per table: strong for balances and stock, eventual for feeds and counters.
  • Big things in S3, pointers in DynamoDB; analytics on an S3 export, not on Scans.
  • Multi-Region, streams, backups and indexes are the next lesson (lesson 15.5); a whole serverless app built on them is lesson 15.8.

#Try it yourself

Create an on-demand Trips table, write and read items, and watch the consumed capacity. Output is shortened; names and numbers are examples.

  1. 1. Create a table with a composite key

    aws dynamodb create-table --table-name Trips-example \
      --attribute-definitions AttributeName=driver_id,AttributeType=S AttributeName=trip_time,AttributeType=S \
      --key-schema AttributeName=driver_id,KeyType=HASH AttributeName=trip_time,KeyType=RANGE \
      --billing-mode PAY_PER_REQUEST --region ap-southeast-1
    
    "TableStatus": "CREATING",
    "TableArn": "arn:aws:dynamodb:ap-southeast-1:111122223333:table/Trips-example",
    "BillingModeSummary": { "BillingMode": "PAY_PER_REQUEST" }
    

    aws dynamodb wait table-exists --table-name Trips-example returns when it is ACTIVE.

  2. 2. Write an item, refusing duplicates

    aws dynamodb put-item --table-name Trips-example \
      --item '{"driver_id":{"S":"d-17"},"trip_time":{"S":"2026-10-28T08:05"},"from":{"S":"Mirpur"},"to":{"S":"Banani"},"fare":{"N":"320"}}' \
      --condition-expression "attribute_not_exists(trip_time)" \
      --return-consumed-capacity TOTAL
    
    "ConsumedCapacity": { "TableName": "Trips-example", "CapacityUnits": 1.0 }
    

    Run it again and it fails with ConditionalCheckFailedException instead of overwriting the trip. Add two more trips for d-17 the same way, at 2026-10-28T10:40 (Banani to Gulshan) and 2026-10-28T13:15 (Gulshan to Motijheel).

  3. 3. Read it, eventually and strongly consistent

    aws dynamodb get-item --table-name Trips-example \
      --key '{"driver_id":{"S":"d-17"},"trip_time":{"S":"2026-10-28T08:05"}}' \
      --return-consumed-capacity TOTAL
    # "CapacityUnits": 0.5
    
    aws dynamodb get-item --table-name Trips-example \
      --key '{"driver_id":{"S":"d-17"},"trip_time":{"S":"2026-10-28T08:05"}}' \
      --consistent-read --return-consumed-capacity TOTAL
    # "CapacityUnits": 1.0
    

    Same item, half the price for the default read.

  4. 4. Query a range, then compare with a Scan

    aws dynamodb query --table-name Trips-example \
      --key-condition-expression "driver_id = :d AND trip_time BETWEEN :a AND :b" \
      --expression-attribute-values '{":d":{"S":"d-17"},":a":{"S":"2026-10-28T08:00"},":b":{"S":"2026-10-28T12:00"}}' \
      --return-consumed-capacity TOTAL
    
    "Count": 2, "ScannedCount": 2,
    "ConsumedCapacity": { "CapacityUnits": 0.5 }
    
    aws dynamodb scan --table-name Trips-example \
      --filter-expression "#t = :c" --expression-attribute-names '{"#t":"to"}' \
      --expression-attribute-values '{":c":{"S":"Sylhet"}}' --return-consumed-capacity TOTAL
    
    "Count": 0, "ScannedCount": 3,
    "ConsumedCapacity": { "CapacityUnits": 0.5 }
    

    ScannedCount is what you paid for; Count is what the filter let through. On a table of millions of items, that gap is the bill.

  5. 5. Switch to provisioned with auto scaling

    aws dynamodb update-table --table-name Trips-example --billing-mode PROVISIONED \
      --provisioned-throughput ReadCapacityUnits=100,WriteCapacityUnits=20
    
    aws application-autoscaling register-scalable-target --service-namespace dynamodb \
      --resource-id table/Trips-example --scalable-dimension dynamodb:table:ReadCapacityUnits \
      --min-capacity 100 --max-capacity 4000
    
    aws application-autoscaling put-scaling-policy --service-namespace dynamodb \
      --resource-id table/Trips-example --scalable-dimension dynamodb:table:ReadCapacityUnits \
      --policy-name trips-read-70 --policy-type TargetTrackingScaling \
      --target-tracking-scaling-policy-configuration \
      '{"TargetValue":70.0,"PredefinedMetricSpecification":{"PredefinedMetricType":"DynamoDBReadCapacityUtilization"}}'
    

    The policy creates the two CloudWatch alarms (scale out, scale in) you saw in the animation.

  6. 6. Pre-warm for an event, then clean up

    aws dynamodb update-table --table-name Trips-example \
      --warm-throughput ReadUnitsPerSecond=40000,WriteUnitsPerSecond=10000
    
    aws dynamodb describe-table --table-name Trips-example --query "Table.WarmThroughput"
    # { "ReadUnitsPerSecond": 40000, "WriteUnitsPerSecond": 10000, "Status": "UPDATING" }
    
    aws dynamodb delete-table --table-name Trips-example
    

    Pre-warming is billed once for the increase; delete the table when you are done.

#Summary

Key takeaways

  • DynamoDB = serverless NoSQL key-value and document database: fully managed, 3 copies in 3 AZs, millions of requests per second, single-digit ms, IAM for access, no joins. Big blobs go to S3; an item is at most 400 KB (names + values).
  • Tables → items → attributes, schema-less except the key. Primary key = partition key (unique, hashed to a partition) or partition key + sort key (item collections sorted by the sort key: one-to-many and range queries). Pick high-cardinality partition keys.
  • One partition, and one item, serves at most 3,000 RCU and 1,000 WCU; total table capacity cannot rescue a hot key.
  • Eventually consistent reads (default) may miss a write from milliseconds ago and cost half; strongly consistent (ConsistentRead = true) read from the leader, cost full price, can fail briefly in outages, and are not available on GSIs.
  • 1 RCU = 1 strong or 2 eventual reads/s of up to 4 KB; 1 WCU = 1 write/s of up to 1 KB; round sizes up; transactions cost double; Query and Scan round the total, Get rounds each item.
  • Query = one partition key value, sorted slice, cheap. Scan = whole table, 1 MB pages, expensive; filters do not reduce capacity; parallel scan is faster, not cheaper. Search by another attribute → GSI (15.5).
  • Provisioned + auto scaling (target ~70%, reacts in minutes, 27 decreases a day, 300 s burst) for steady traffic; on-demand (pay per request, instant to 2 × previous peak, pre-warm, 40,000-unit table quota, default and recommended since 2024) for spiky or unknown traffic. Switch to on-demand up to 4 times per 24 h, back any time.
  • ProvisionedThroughputExceededException: table short → auto scaling / on-demand; hot read key → DAX; hot write key → write sharding or SQS; always exponential backoff. Adaptive capacity helps skew up to the per-partition ceiling.
  • Side notes: transactions (100 items, all-or-nothing, double cost), PartiQL (SQL syntax, same Query/Scan underneath), Standard-IA table class (60% cheaper storage, dearer requests).