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.

5 min read· Updated Jun 15, 2026
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. BatchWriteItem has no ConditionExpression.
  • Updates. You can Put (overwrite) or Delete a 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 PutRequest replaces 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

OperationMax itemsMax payloadAtomic?Conditions?
BatchGetItem10016 MBNoN/A
BatchWriteItem2516 MBNoNo
TransactGetItems1004 MBYesYes
TransactWriteItems1004 MBYesYes

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 ConditionExpression checks (e.g. “only if balance ≥ amount”) evaluated atomically.
  • Read-then-act atomicity. TransactGetItems gives 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.

Keep reading