DynamoDB Batch Operations: BatchGetItem & BatchWriteItem
Read and write many items with BatchGetItem and BatchWriteItem — limits, unprocessed items, retries with backoff, and when to use transactions instead.
On this page
BatchGetItem and BatchWriteItem let you read and write many items in a single API call, cutting round trips and network overhead. They are best-effort and non-atomic: parts of a batch can fail independently and come back as unprocessed, so correct retry handling is mandatory, not optional.
This guide covers the limits, the unprocessed-items contract, retry strategy, and when you should reach for transactions instead.
What batch operations are (and aren’t)
A batch request bundles many individual reads or writes so they travel over one HTTP request. That’s the entire benefit — fewer round trips and lower latency. Batch operations do not give you:
- Atomicity. Each item succeeds or fails on its own. There’s no rollback.
- Conditions.
BatchWriteItemhas noConditionExpression. - Updates. You can
Put(overwrite) orDeletea whole item — there’s no partial update.
If you need any of those, you want UpdateItem, or transactions for atomicity across items.
BatchGetItem
BatchGetItem fetches up to 100 items or 16 MB of data per call, across one or more tables. You supply the full primary key of each item.
{
"RequestItems": {
"Orders": {
"Keys": [
{ "PK": {"S": "USER#1"}, "SK": {"S": "ORDER#1"} },
{ "PK": {"S": "USER#1"}, "SK": {"S": "ORDER#2"} }
],
"ProjectionExpression": "orderId, #s",
"ExpressionAttributeNames": { "#s": "status" }
}
}
}
You can pass a ProjectionExpression to trim returned attributes (see expressions) and set ConsistentRead: true per table if you need strongly consistent reads.
Critically, the response may include an UnprocessedKeys map — keys that DynamoDB didn’t return, typically because the batch exceeded throughput or the 16 MB limit. You must reissue those keys.
BatchWriteItem
BatchWriteItem performs up to 25 put or delete requests per call, capped at 16 MB. Each entry is either a PutRequest (full item) or a DeleteRequest (a key).
{
"RequestItems": {
"Orders": [
{ "PutRequest": { "Item": { "PK": {"S":"USER#1"}, "SK": {"S":"ORDER#3"}, "total": {"N":"42"} } } },
{ "DeleteRequest": { "Key": { "PK": {"S":"USER#1"}, "SK": {"S":"ORDER#0"} } } }
]
}
}
Constraints worth memorizing:
- A
PutRequestreplaces the entire item — it’s an unconditional overwrite, not a merge. - You can’t reference the same key twice in one batch.
- Item-size limits (400 KB each) still apply.
The response carries an UnprocessedItems map with any writes that didn’t complete.
The limits at a glance
| Operation | Max items | Max payload | Atomic? | Conditions? |
|---|---|---|---|---|
BatchGetItem | 100 | 16 MB | No | N/A |
BatchWriteItem | 25 | 16 MB | No | No |
TransactGetItems | 100 | 4 MB | Yes | Yes |
TransactWriteItems | 100 | 4 MB | Yes | Yes |
Handling unprocessed items — the part people skip
This is the single most important thing about batch operations: a 200 response does not mean every item succeeded. DynamoDB can partially complete a batch and return the remainder in UnprocessedKeys (reads) or UnprocessedItems (writes). Causes include insufficient provisioned capacity, throttling, or hitting the payload limit.
If you ignore these, you silently lose writes. The correct pattern is to loop, retrying only the unprocessed remainder, with exponential backoff and jitter so you don’t hammer a throttled partition.
import random, time
def batch_write_all(client, requests):
request_items = {"Orders": requests}
attempt = 0
while request_items:
resp = client.batch_write_item(RequestItems=request_items)
request_items = resp.get("UnprocessedItems") or {}
if request_items:
attempt += 1
delay = min(2 ** attempt * 0.05, 5) + random.uniform(0, 0.1)
time.sleep(delay)
The same loop shape applies to BatchGetItem with UnprocessedKeys. Cap your attempts so a persistently throttled table doesn’t spin forever — surface the failure instead.
Chunking beyond the limits
Because a single BatchWriteItem handles only 25 items, bulk loading means slicing your data into chunks of 25 (and 100 for reads) and issuing many batches.
def chunks(items, n):
for i in range(0, len(items), n):
yield items[i:i + n]
for chunk in chunks(all_items, 25):
batch_write_all(client, [{"PutRequest": {"Item": x}} for x in chunk])
For large imports, run chunks concurrently to saturate available throughput — but watch for throttling, which shows up as rising UnprocessedItems. If most of a batch comes back unprocessed, you’re outrunning your table’s capacity; slow down or raise capacity. See best practices for managing write-heavy workloads.
When to use transactions instead
Reach for TransactWriteItems over BatchWriteItem when correctness requires it:
- All-or-nothing semantics. Money movement, inventory decrements, or any multi-item invariant that must never be left half-applied.
- Conditional writes. You need
ConditionExpressionchecks (e.g. “only if balance ≥ amount”) evaluated atomically. - Read-then-act atomicity.
TransactGetItemsgives a consistent snapshot across items.
The trade-offs: transactions cap at 100 actions and 4 MB, can’t span the same item twice, and cost twice the write capacity units of an equivalent batch. So use batch for high-volume, independent, idempotent writes (logs, denormalized fan-out, bulk import) and transactions for the small set of operations where partial completion would corrupt your data. See transactions for the full semantics.
Idempotency matters
Because you retry unprocessed items, your writes should be idempotent. PutRequest naturally is — replaying the same put produces the same item. DeleteRequest is idempotent too (deleting an absent item is a no-op). This is part of why batch writes only support put and delete: both are safely repeatable, which is exactly what an at-least-once retry loop needs.
Watching batches in practice
Partial failures are invisible until you look for them, and a bulk load that “worked” can be quietly missing 3% of its rows. A DynamoDB GUI like Tablyne can run batch imports, surface the count of unprocessed items per round, and show consumed capacity as the load progresses — which turns silent throttling into something you can see and react to before the data ends up incomplete.
The mental model is simple: batch saves round trips, never guarantees completion. Always loop on unprocessed items with backoff, chunk to the 25/100 limits, keep writes idempotent, and switch to transactions the moment atomicity or conditions enter the picture.
Frequently asked questions
What is the difference between BatchWriteItem and TransactWriteItems?
BatchWriteItem is best-effort and non-atomic — individual writes can fail and come back as UnprocessedItems while others succeed. TransactWriteItems is all-or-nothing and supports conditions, but caps at 100 actions and costs twice the write capacity.
Why does BatchWriteItem return UnprocessedItems?
DynamoDB throttled or couldn't complete part of the batch, usually due to insufficient capacity or a hot partition. The unfinished writes are returned so you can retry them, ideally with exponential backoff and jitter.
Can BatchWriteItem update items or use conditions?
No. BatchWriteItem only supports full PutRequest and DeleteRequest entries — no partial updates, no ConditionExpression, no atomic counters. For conditional or partial writes, use UpdateItem or TransactWriteItems.