Skip to content

16. Cheat sheet

Everything worth having to hand, in one place.

1. THE PARTITION KEY decides the partition, the
machine, the throughput ceiling and the hot-key
risk. It is permanent.
2. QUERY reads ONE partition by exact PK, with an
optional SORT KEY range. SCAN reads everything and
belongs in batch jobs only.
3. 3,000 RCU / 1,000 WCU / 10 GB per PARTITION —
whatever the table's capacity is.
4. Writes round up to 1 KB, reads to 4 KB. Eventually
consistent reads cost HALF. Transactions cost
DOUBLE.
5. A GSI is a SECOND COPY of your data. Every table
write writes every matching index — and a throttled
index backpressures the table.
6. ALWAYS paginate on LastEvaluatedKey. The 1 MB cut
is silent.
item size 400 KB
query/scan page 1 MB (filters apply AFTER)
partition key value 2048 bytes
sort key value 1024 bytes
item collection unlimited — 10 GB if an LSI
exists
BatchGetItem 100 items
BatchWriteItem 25 items
TransactWrite/GetItems 100 items, 4 MB
GSIs per table 20 (soft)
LSIs per table 5, at creation only
per partition 3,000 RCU / 1,000 WCU / 10 GB
stream retention 24 hours
PITR window 35 days
WRITE ceil(bytes / 1 KB) WCU
READ ceil(bytes / 4 KB) RCU strongly consistent
half that, eventually consistent
TXN 2× the above
1 KB item, 1,000 writes/s → 1,000 WCU
3 KB item, 1,000 reads/s → 1,000 RCU strong
500 RCU eventual
0.2 KB item → still 1 WCU
GOOD PK userId, deviceId, tenant#entity — high
cardinality, even access
BAD PK status, country, "ORDERS", today's date
SORT KEY ORDER#2026-03-11#A917
ISO dates · # separators · zero-padded
numbers · most general segment first
HOT KEY FIX PK = "PRODUCT#123#" + shard(0..N)
fan out reads, or derive the shard from
something the reader knows
KEY CONDITION PK = :pk AND SK BETWEEN :a AND :b
PK = :pk AND begins_with(SK, :prefix)
CONDITION attribute_not_exists(PK)
attribute_exists(PK)
version = :v
balance >= :amt
attribute_type(x, :type)
contains(tags, :t) size(list) < :n
UPDATE SET a = :v, b = if_not_exists(b, :d)
SET list = list_append(list, :items)
REMOVE oldAttr
ADD counter :one ← atomic
DELETE tagSet :values
NAMES #s = "status" ← alias reserved words
VALUES :v = {"S": "..."}
Terminal window
aws dynamodb create-table --table-name AppData \
--attribute-definitions AttributeName=PK,AttributeType=S \
AttributeName=SK,AttributeType=S \
--key-schema AttributeName=PK,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST
aws dynamodb get-item --table-name AppData \
--key '{"PK":{"S":"CUSTOMER#42"},"SK":{"S":"PROFILE"}}' \
--return-consumed-capacity TOTAL
aws dynamodb query --table-name AppData \
--key-condition-expression 'PK = :pk AND begins_with(SK, :p)' \
--expression-attribute-values '{":pk":{"S":"CUSTOMER#42"},":p":{"S":"ORDER#"}}' \
--no-scan-index-forward
aws dynamodb update-item --table-name AppData \
--key '{"PK":{"S":"CUSTOMER#42"},"SK":{"S":"PROFILE"}}' \
--update-expression 'SET #n = :n ADD logins :one' \
--expression-attribute-names '{"#n":"name"}' \
--expression-attribute-values '{":n":{"S":"Alice"},":one":{"N":"1"}}' \
--condition-expression 'attribute_exists(PK)'
aws dynamodb describe-table --table-name AppData
aws dynamodb update-time-to-live --table-name AppData \
--time-to-live-specification "Enabled=true, AttributeName=expiresAt"
aws dynamodb update-continuous-backups --table-name AppData \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
aws dynamodb update-table --table-name AppData --deletion-protection-enabled
kwargs = {"KeyConditionExpression": Key("PK").eq(pk)}
while True:
resp = table.query(**kwargs)
yield from resp["Items"]
if "LastEvaluatedKey" not in resp: break
kwargs["ExclusiveStartKey"] = resp["LastEvaluatedKey"]
table.put_item(
Item={"PK": f"IDEMPOTENCY#{rid}", "SK": "LOCK",
"expiresAt": int(time.time()) + 86400},
ConditionExpression="attribute_not_exists(PK)")
client.transact_write_items(TransactItems=[
{"Put": {"TableName": T, "Item": {"PK": {"S": "EMAIL#a@b.c"},
"SK": {"S": "UNIQUE"}},
"ConditionExpression": "attribute_not_exists(PK)"}},
{"Put": {"TableName": T, "Item": user_item}},
])
table read, default eventually consistent
table read, ConsistentRead strongly consistent
GSI read ALWAYS eventual
LSI read can be strong
TransactGetItems serializable snapshot
global tables, other region eventual, LAST-WRITER-WINS
view types KEYS_ONLY · NEW_IMAGE · OLD_IMAGE ·
NEW_AND_OLD_IMAGES
events INSERT · MODIFY · REMOVE (incl. TTL)
ordering per ITEM only
delivery AT LEAST ONCE → consumers idempotent
Lambda a failed batch BLOCKS the shard. set
BisectBatchOnFunctionError, retry
limits and an on-failure destination.
Config(retries={"max_attempts": 5, "mode": "adaptive"},
connect_timeout=1, read_timeout=2,
max_pool_connections=50)
# create the client ONCE, outside the handler
ThrottledRequests / Read|WriteThrottleEvents > 0
ConsumedCapacity ÷ Provisioned > 80%
SuccessfulRequestLatency p99
SystemErrors (5xx), UserErrors (4xx)
ScannedCount vs ReturnedItemCount ← efficiency
per-INDEX consumed capacity ← separately!
Contributor Insights ← names hot keys
ProvisionedThroughputExceededException
→ table capacity, hot partition, or GSI. check
per-index metrics and Contributor Insights.
ConditionalCheckFailedException
→ your condition was false. expected outcome.
still billed.
TransactionCanceledException
→ READ CancellationReasons. ConditionalCheckFailed
= final. TransactionConflict = retryable.
ValidationException: reserved keyword
→ alias the attribute name with #
ValidationException: item size exceeded 400 KB
→ an unbounded attribute. split into items or S3.
ItemCollectionSizeLimitExceededException
→ an LSI item collection passed 10 GB.
ResourceNotFoundException on a GSI query
→ index still backfilling, or IAM missing
/index/* on the resource ARN
# newest N items for one parent
table.query(KeyConditionExpression=Key("PK").eq("USER#42")
& Key("SK").begins_with("DOC#"),
ScanIndexForward=False, Limit=20)
# a time window
table.query(KeyConditionExpression=Key("PK").eq("DEVICE#7")
& Key("SK").between("READING#2026-09-01",
"READING#2026-09-30"))
# optimistic lock
table.update_item(Key=key,
UpdateExpression="SET #s = :new, version = :next",
ConditionExpression="version = :cur",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={":new": "done", ":next": v+1, ":cur": v})
# atomic counter, no read
table.update_item(Key=key, UpdateExpression="ADD views :one",
ExpressionAttributeValues={":one": 1})
# decrement with an invariant, returning the result
table.update_item(Key=key, UpdateExpression="ADD stock :neg",
ConditionExpression="stock >= :one",
ExpressionAttributeValues={":neg": -1, ":one": 1},
ReturnValues="ALL_NEW")
# sparse work queue: join the index
table.update_item(Key=key,
UpdateExpression="SET GSI1PK = :q, GSI1SK = :t",
ExpressionAttributeValues={":q": "REVIEW#pending", ":t": now_iso()})
# …and leave it
table.update_item(Key=key, UpdateExpression="REMOVE GSI1PK, GSI1SK")
# TTL: expire in 30 days (SECONDS, as a number)
{"expiresAt": int(time.time()) + 30*86400}
# batch write with the retry loop
req = {"AppData": [{"PutRequest": {"Item": i}} for i in batch]}
while req:
resp = client.batch_write_item(RequestItems=req)
req = resp.get("UnprocessedItems") or None
□ eventually consistent reads where allowed (−50%)
□ TTL instead of DeleteItem (free)
□ drop unused GSIs
□ KEYS_ONLY / INCLUDE projections
□ batch small items together
□ shorten attribute names
□ export to S3 for analytics
□ provisioned + reserved for steady baselines
□ access patterns written down beside the table
□ no Scan on any request path
□ partition keys: high cardinality, even access
□ pagination helper used everywhere
□ writes idempotent; retries adaptive
□ PITR + deletion protection on
□ table name from configuration
□ alarms: throttles, latency, per-index capacity
□ a restore practised and timed

Read alongside: Cassandra for the same ideas self-hosted, PostgreSQL for what you gave up, Redis for the layer in front, and Kafka for where your streams should land.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source