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
ProvisionedThroughputExceededExceptionand 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,PartiQLand 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
| Concept | What it is | Relational equivalent |
|---|---|---|
| Table | A named collection of items in one Region. Its primary key is chosen at creation and can never change. | Table |
| Item | One record, identified by its primary key. A table can hold an unlimited number of items. | Row |
| Attribute | A 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) | score | result | other attributes |
|---|---|---|---|---|
| u-7791 | g-4421 | 92 | win | streak: 3 |
| u-7791 | g-5180 | 14 | lose | |
| u-8730 | g-1894 | 77 | win | badge: “Eid champion” |
| u-8730 | g-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:
betweentwo times,begins_witha 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.
Item
user_id
u-1001
name
Hasan
phone
+880 1700-000000
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 key | Cardinality | Verdict |
|---|---|---|
user_id, order_id, device_id | Millions of values, requests spread evenly | Good |
driver_id + sort key trip_time | Many drivers; each driver’s trips together | Good, for “one driver’s trips by time” |
status (3 values), country (almost everyone in Bangladesh) | A handful of values | Bad: hot partition |
date (today’s date) | All of today’s writes on one key | Bad: today is always hot |
product_id during a sale | Many values, but one product gets most reads | Good key, hot item: needs a cache (below) |
#Reading and writing items
The everyday API (all of it is HTTPS calls signed with IAM):
PutItemwrites a whole item (create or replace). Add a condition expression such asattribute_not_exists(user_id)to refuse overwriting; a failed condition returnsConditionalCheckFailedExceptionand still consumes write capacity.GetItemreads one item by its full primary key.UpdateItemchanges some attributes (or creates the item), including atomic counters (SET views = views + 1). Conditions on aversionattribute give optimistic locking: “update only if version is still 7”.DeleteItemremoves one item.BatchGetItem(up to 100 items, 16 MB) andBatchWriteItem(up to 25 puts or deletes) save round trips but are not all-or-nothing: unprocessed items come back for you to retry.QueryandScanread 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,QueryandScan): 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 = trueon 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.
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
- 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.
- Multiply by the read type: eventual × 0.5, strong × 1, transactional × 2. Writes: standard × 1, transactional × 2.
- Multiply by the requests per second.
Details that show up in questions:
GetItemandBatchGetItemround each item separately.QueryandScanadd 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.UpdateItemis 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
| Workload | Per request | Units per second |
|---|---|---|
| 10 strongly consistent reads/s of 6 KB items | 6 → 8 KB = 2 blocks × 1 = 2 RCU | 20 RCU |
| 80 eventually consistent reads/s of 6 KB items | 2 blocks × 0.5 = 1 RCU | 80 RCU |
| 50 transactional reads/s of 3 KB items | 1 block × 2 = 2 RCU | 100 RCU |
| 12 writes/s of 2.5 KB items | 2.5 → 3 KB = 3 WCU | 36 WCU |
| 10 transactional writes/s of 1.5 KB items | 2 KB × 2 = 4 WCU | 40 WCU |
| One Query returning 12 items of 0.5 KB, eventual | 6 KB total → 8 KB = 2 blocks × 0.5 | 1 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.
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
UpdateTableto 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%.
Products table
| Time | Traffic | Capacity | Result |
|---|---|---|---|
| 09:00 | 300 | 500 | 60% used, fine |
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:
| Cause | Symptom | What fixes it |
|---|---|---|
| The table is short of capacity: total traffic exceeds provisioned RCU/WCU | All 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 traffic | Throttles while the table’s total capacity is mostly unused | Key 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:
- 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.)
- 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.
- Better key design for skew you create yourself (a
statusordatekey): use a high-cardinality partition key. - 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.
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
| Symptom | Likely cause | Fix |
|---|---|---|
ProvisionedThroughputExceededException on every key, consumed capacity at 100% | Table short of capacity | Auto scaling with a higher maximum, on-demand, pre-warm before events |
| Throttling while most of the table’s capacity is unused | Hot partition or hot item | Key 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 sale | Auto scaling lags a steep spike; burst capacity used up | Raise the minimum before the sale, scheduled scaling, on-demand + pre-warm |
| On-demand table throttles at a launch | Traffic more than doubled the previous peak within 30 minutes, or hit the 40,000-unit table quota | Pre-warm (warm throughput), ramp up, request a quota increase |
| The app shows the old value right after saving | Eventually consistent read after a write | ConsistentRead = true where it matters, or read your own write from the response |
ConsistentRead rejected on an index | Strong reads requested on a GSI | GSIs are eventual only; read the table or use an LSI |
ValidationException on PutItem | Item 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 data | Same primary key, no condition | attribute_not_exists condition; optimistic locking with a version attribute |
| A report job makes the live app slow | Scan or parallel scan consuming the table’s capacity | Run off-peak with Limit; export to S3 and use Athena; build a GSI for the access pattern |
| Bill much higher than expected | Scans with filters, large items, strong reads where eventual would do, idle provisioned capacity | Query instead of Scan, smaller items, eventual reads, on-demand or scale-down |
| A batch call “succeeds” but some items are missing | BatchWriteItem returned unprocessed items under throttling | Retry 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_idas 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. 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-examplereturns when it isACTIVE.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
ConditionalCheckFailedExceptioninstead of overwriting the trip. Add two more trips ford-17the same way, at2026-10-28T10:40(Banani to Gulshan) and2026-10-28T13:15(Gulshan to Motijheel).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.0Same item, half the price for the default read.
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 }ScannedCountis what you paid for;Countis what the filter let through. On a table of millions of items, that gap is the bill.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. 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-examplePre-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).
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
Which description of Amazon DynamoDB is correct?
Self-checkQuestion
An item in a DynamoDB table can be at most how large, and what counts toward the limit?
Self-checkQuestion
A ride-sharing app must show each driver’s trips between two times, newest first. Which primary key fits best?
Self-checkQuestion
An app with 50 million users in Bangladesh writes heavily to a table. Which partition key will most likely create a hot partition?
Self-checkQuestion
Which two statements about eventually consistent reads in DynamoDB are true?
Choose 2
Self-checkQuestion
After a user withdraws 1,000 taka from a wallet, the app must show the new balance with no chance of the old one. What should the read be?
Self-checkQuestion
How many RCU does a table need for 10 strongly consistent reads per second of 6 KB items?
Self-checkQuestion
The same 6 KB items are read 80 times per second, eventually consistent. How many RCU?
Self-checkQuestion
An app writes 12 items per second, each 2.5 KB, with ordinary PutItem calls. How many WCU are needed?
Self-checkQuestion
An order service performs 10 transactional writes per second (TransactWriteItems), each touching one item of 1.5 KB. How many write units does it consume per second?
Self-checkQuestion
A Query returns 12 items of 0.5 KB each, eventually consistent. A BatchGetItem fetching the same 12 items, also eventually consistent, costs how much compared with the Query?
Self-checkQuestion
A table holds 1,000,000 items of 1 KB. A Scan with the filter expression city = Sylhet, eventually consistent, returns 500 items. Roughly what does it consume?
Self-checkQuestion
An Orders table has partition key order_id. Support staff need all orders of one customer, many times a day, at the lowest cost and latency. What should the team do?
Self-checkQuestion
Which two statements about Scan are true?
Choose 2
Self-checkQuestion
A startup is launching a new mobile app and has no idea how much traffic it will get. Which capacity setting is most appropriate for its DynamoDB tables?
Self-checkQuestion
A table serves a steady 1,000 reads per second around the clock, every day, and the company wants the lowest cost. What should it use?
Self-checkQuestion
Auto scaling on a provisioned table targets 70% read utilization. Consumed capacity has been steady at 840 RCU for several minutes. What will auto scaling set the read capacity to?
Self-checkQuestion
A shop uses provisioned capacity with auto scaling. When a celebrity posts its link, reads jump six-fold within ten seconds, and the app throttles for several minutes before recovering. Why?
Self-checkQuestion
An on-demand table’s highest traffic so far is 6,000 reads per second. Tomorrow at midnight an Eid campaign will send 40,000 reads per second within a minute. What should the team do?
Self-checkQuestion
Study notes say you can switch a DynamoDB table between provisioned and on-demand only once every 24 hours. What is the current rule?
Self-checkQuestion
An e-commerce site on DynamoDB got ProvisionedThroughputExceededException last Eid because a few very popular products were read constantly. What is the best way to prevent it this year?
Self-checkQuestion
One product item receives reads needing 4,000 RCU per second. Which two measures actually stop the throttling on that item?
Choose 2
Self-checkQuestion
During a live television show, 2,500 viewers per second vote for contestant 7, each vote incrementing the item votes#7. Writes are throttled. What is the best fix?
Self-checkQuestion
A table occasionally throttles for a second or two at busy moments. How should the client application behave?
Self-checkQuestion
Which statement about DynamoDB adaptive capacity is correct?
Self-checkQuestion
A table with 10,000 WCU throttles on writes while CloudWatch shows only about 15% of its write capacity used. The partition key is status (pending, paid, delivered). What fixes the problem?
Self-checkQuestion
Which two statements about DynamoDB transactions are true?
Choose 2
Self-checkQuestion
A table stores 2 TB of order history that is rarely read, and its storage cost is far higher than its read and write cost. What change reduces the bill most?
Self-checkQuestion
Which two statements, common in study notes, are wrong or out of date?
Choose 2
Self-checkQuestion
During a brief failure of one replica, a strongly consistent GetItem returns HTTP 500 while eventually consistent reads of the same item succeed. What explains this?
Self-checkQuestion
A table serves 100 strongly consistent reads per second of 6 KB items, around the clock for 30 days. On-demand costs 0.125 USD per million read request units. What is the monthly read bill on-demand?
Self-checkQuestion
The same workload on provisioned capacity needs 200 RCU. At 0.00013 USD per RCU-hour, what does it cost for 30 days, and what does that say?
Self-checkQuestion
Using DynamoDB’s SQL-compatible query language, a developer selects every trip with a fare above 500 from the Trips table (partition key driver_id), with no condition on driver_id. What happens?