Skip to main content

Batch Operations

Batch operations allow you to read or write multiple items in a single request, significantly improving performance and reducing API calls compared to individual operations. This guide covers batch get and batch write operations with best practices for handling unprocessed items.

Overview​

DynamoDB provides two batch operations:

BatchGetItem:

  • Retrieve up to 100 items or 16MB of data
  • Read from one or more tables
  • Items retrieved in parallel
  • Supports projection expressions and consistent reads

BatchWriteItem:

  • Put or delete up to 25 items
  • Write to one or more tables
  • Operations processed in parallel
  • No conditional expressions supported (use Transactions if you need conditions)

Quick Start​

The batch API uses static entry points and reuses existing request builders:

using Amazon.DynamoDBv2;
using Oproto.FluentDynamoDb;

var client = new AmazonDynamoDBClient();
var userTable = new UsersTable(client, "users");
var orderTable = new OrdersTable(client, "orders");

// Batch write - put multiple items
await DynamoDbBatch.Write
.Add(userTable.Put(user1))
.Add(userTable.Put(user2))
.Add(orderTable.Delete(orderId))
.ExecuteAsync();

// Batch get with tuple destructuring
var (user, order) = await DynamoDbBatch.Get
.Add(userTable.Get(userId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Order>();

Important: Always check for unprocessed items in the response. DynamoDB may not process all items due to capacity limits.

Batch Write Operations​

Batch write operations put or delete multiple items in a single request using the DynamoDbBatch.Write entry point.

Basic Batch Put​

var users = new List<User>
{
new User { UserId = "user1", Name = "Alice", Email = "alice@example.com" },
new User { UserId = "user2", Name = "Bob", Email = "bob@example.com" },
new User { UserId = "user3", Name = "Charlie", Email = "charlie@example.com" }
};

await DynamoDbBatch.Write
.Add(userTable.Put(users[0]))
.Add(userTable.Put(users[1]))
.Add(userTable.Put(users[2]))
.ExecuteAsync();

Basic Batch Delete​

await DynamoDbBatch.Write
.Add(userTable.Delete("user1"))
.Add(userTable.Delete("user2"))
.Add(userTable.Delete("user3"))
.ExecuteAsync();

Delete with Composite Keys​

For tables with both partition key and sort key:

await DynamoDbBatch.Write
.Add(orderTable.Delete("customer123", "order1"))
.Add(orderTable.Delete("customer123", "order2"))
.Add(orderTable.Delete("customer456", "order3"))
.ExecuteAsync();

Mixed Put and Delete Operations​

Combine put and delete operations in a single batch:

await DynamoDbBatch.Write
// Add new users
.Add(userTable.Put(newUser1))
.Add(userTable.Put(newUser2))

// Delete old users
.Add(userTable.Delete("oldUser1"))
.Add(userTable.Delete("oldUser2"))
.ExecuteAsync();

Batch Write to Multiple Tables​

Write to different tables in a single batch request:

await DynamoDbBatch.Write
.Add(userTable.Put(user))
.Add(orderTable.Put(order))
.Add(auditTable.Put(auditEntry))
.ExecuteAsync();

Note: Batch writes do not support conditional expressions. If you need conditions, use Transactions instead.

Batch Get Operations​

Batch get operations retrieve multiple items efficiently in a single request using the DynamoDbBatch.Get entry point.

Basic Batch Get​

var response = await DynamoDbBatch.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(userTable.Get("user3"))
.ExecuteAsync();

// Deserialize items
var users = response.GetItemsRange<User>(0, 2);
foreach (var user in users)
{
if (user != null)
{
Console.WriteLine($"User: {user.Name}");
}
}

ExecuteAndMapAsync - Tuple Destructuring​

For convenience with small numbers of items, use ExecuteAndMapAsync to get a tuple:

// 2 items
var (user, order) = await DynamoDbBatch.Get
.Add(userTable.Get(userId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Order>();

// 3 items
var (user, account, order) = await DynamoDbBatch.Get
.Add(userTable.Get(userId))
.Add(accountTable.Get(accountId))
.Add(orderTable.Get(orderId))
.ExecuteAndMapAsync<User, Account, Order>();

// Up to 8 items supported
var (item1, item2, item3, item4, item5, item6, item7, item8) =
await DynamoDbBatch.Get
.Add(table1.Get(key1))
.Add(table2.Get(key2))
// ... up to 8 items
.ExecuteAndMapAsync<T1, T2, T3, T4, T5, T6, T7, T8>();

Response Deserialization Methods​

The BatchGetResponse provides multiple ways to deserialize items:

var response = await DynamoDbBatch.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(userTable.Get("user3"))
.Add(orderTable.Get("order1"))
.ExecuteAsync();

// Get single item by index
var user1 = response.GetItem<User>(0);

// Get multiple items of same type by indices
var users = response.GetItems<User>(0, 1, 2);

// Get contiguous range of items
var allUsers = response.GetItemsRange<User>(0, 2); // indices 0, 1, 2

// Get item from different table
var order = response.GetItem<Order>(3);

// Check total count
Console.WriteLine($"Retrieved {response.Count} items");

Batch Get with Composite Keys​

var response = await DynamoDbBatch.Get
.Add(orderTable.Get("customer123", "order1"))
.Add(orderTable.Get("customer123", "order2"))
.Add(orderTable.Get("customer456", "order3"))
.ExecuteAsync();

var orders = response.GetItemsRange<Order>(0, 2);

Batch Get with Projection​

Retrieve only specific attributes to reduce data transfer:

var response = await DynamoDbBatch.Get
.Add(userTable.Get("user1").WithProjection("name, email"))
.Add(userTable.Get("user2").WithProjection("name, email"))
.Add(userTable.Get("user3").WithProjection("name, email"))
.ExecuteAsync();

var users = response.GetItemsRange<User>(0, 2);

Projection Benefits:

  • Reduces network bandwidth
  • Lowers read capacity consumption
  • Improves response time for large items

Batch Get with Consistent Reads​

var response = await DynamoDbBatch.Get
.Add(userTable.Get("user1").UsingConsistentRead())
.Add(userTable.Get("user2").UsingConsistentRead())
.ExecuteAsync();

var users = response.GetItemsRange<User>(0, 1);

Note: Consistent reads consume twice the read capacity. Use them only when you need the most up-to-date data.

Batch Get from Multiple Tables​

var response = await DynamoDbBatch.Get
.Add(userTable.Get("user123"))
.Add(userTable.Get("user456"))
.Add(orderTable.Get("customer123", "order1"))
.Add(productTable.Get("prod789"))
.ExecuteAsync();

// Items are returned in the order they were added
var user1 = response.GetItem<User>(0);
var user2 = response.GetItem<User>(1);
var order = response.GetItem<Order>(2);
var product = response.GetItem<Product>(3);

Handling Unprocessed Items​

DynamoDB may not process all items in a batch request due to capacity limits or other constraints. Always check for and handle unprocessed items.

Checking for Unprocessed Items​

// Batch get
var getResponse = await DynamoDbBatch.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.ExecuteAsync();

if (getResponse.HasUnprocessedKeys)
{
Console.WriteLine($"Unprocessed keys in {getResponse.UnprocessedKeys.Count} tables");
// Implement retry logic
}

// Batch write
var writeResponse = await DynamoDbBatch.Write
.Add(userTable.Put(user1))
.Add(userTable.Put(user2))
.ExecuteAsync();

if (writeResponse.UnprocessedItems.Count > 0)
{
Console.WriteLine($"Unprocessed items in {writeResponse.UnprocessedItems.Count} tables");
// Implement retry logic
}

Retry Logic with Exponential Backoff​

public async Task<BatchGetResponse> BatchGetWithRetry(
BatchGetBuilder builder,
int maxRetries = 3)
{
var response = await builder.ExecuteAsync();
var retryCount = 0;

while (response.HasUnprocessedKeys && retryCount < maxRetries)
{
// Exponential backoff: 100ms, 200ms, 400ms
var delayMs = 100 * (int)Math.Pow(2, retryCount);
await Task.Delay(delayMs);

Console.WriteLine($"Retry {retryCount + 1}: unprocessed keys remaining");

// Retry with unprocessed keys
var retryRequest = new BatchGetItemRequest
{
RequestItems = response.UnprocessedKeys
};

var retryResponse = await client.BatchGetItemAsync(retryRequest);
response = new BatchGetResponse(retryResponse, tableOrder);
retryCount++;
}

if (response.HasUnprocessedKeys)
{
Console.WriteLine($"Failed to process all items after {maxRetries} retries");
}

return response;
}

Chunking Large Batches​

When you have more items than the batch limit allows, split them into chunks.

Chunking Batch Writes (25 items per batch)​

public async Task BatchWriteInChunks<T>(
List<T> items,
Func<T, PutItemRequestBuilder<T>> putBuilder,
int chunkSize = 25)
{
// Split into chunks of 25 (BatchWriteItem limit)
for (int i = 0; i < items.Count; i += chunkSize)
{
var chunk = items.Skip(i).Take(chunkSize).ToList();

var batch = DynamoDbBatch.Write;
foreach (var item in chunk)
{
batch.Add(putBuilder(item));
}

var response = await batch.ExecuteAsync();

// Handle unprocessed items
if (response.UnprocessedItems.Count > 0)
{
Console.WriteLine($"Chunk {i / chunkSize + 1}: unprocessed items");
// Implement retry logic
}
}
}

// Usage
await BatchWriteInChunks(
allUsers,
user => userTable.Put(user)
);

Parallel Batch Operations​

For better throughput with large datasets:

public async Task ParallelBatchWrite<T>(
List<T> items,
Func<T, PutItemRequestBuilder<T>> putBuilder,
int maxParallel = 4)
{
// Split into chunks
var chunks = items
.Select((item, index) => new { item, index })
.GroupBy(x => x.index / 25)
.Select(g => g.Select(x => x.item).ToList())
.ToList();

// Process chunks in parallel (with limit)
var semaphore = new SemaphoreSlim(maxParallel);
var tasks = chunks.Select(async chunk =>
{
await semaphore.WaitAsync();
try
{
var batch = DynamoDbBatch.Write;
foreach (var item in chunk)
{
batch.Add(putBuilder(item));
}
await batch.ExecuteAsync();
}
finally
{
semaphore.Release();
}
});

await Task.WhenAll(tasks);
}

Best Practices​

Always Handle Unprocessed Items​

// ✅ Good - handles unprocessed items
var response = await DynamoDbBatch.Write
.Add(userTable.Put(user))
.ExecuteAsync();

if (response.UnprocessedItems.Count > 0)
{
// Retry with exponential backoff
}

// ❌ Avoid - ignores unprocessed items
await DynamoDbBatch.Write.Add(userTable.Put(user)).ExecuteAsync();

Use Projection Expressions​

// ✅ Good - only retrieve needed attributes
await DynamoDbBatch.Get
.Add(userTable.Get("user123").WithProjection("name, email"))
.ExecuteAsync();

// ❌ Avoid - retrieves all attributes
await DynamoDbBatch.Get
.Add(userTable.Get("user123"))
.ExecuteAsync();

Chunk Large Batches​

// ✅ Good - chunks into batches of 25
await BatchWriteInChunks(allUsers, user => userTable.Put(user), 25);

// ❌ Avoid - trying to write more than 25 items
var batch = DynamoDbBatch.Write;
foreach (var user in allUsers) // Could be > 25 items
{
batch.Add(userTable.Put(user));
}
await batch.ExecuteAsync(); // Will throw ValidationException

Use Batch Operations for Bulk Reads/Writes​

// ✅ Good - single batch request
await DynamoDbBatch.Get
.Add(userTable.Get("user1"))
.Add(userTable.Get("user2"))
.Add(userTable.Get("user3"))
.ExecuteAsync();

// ❌ Avoid - multiple individual requests
foreach (var userId in userIds)
{
await userTable.Get(userId).GetItemAsync();
}

Implement Exponential Backoff​

// ✅ Good - exponential backoff for retries
var delayMs = 100 * (int)Math.Pow(2, retryCount);
await Task.Delay(delayMs);

// ❌ Avoid - fixed delay or immediate retry
await Task.Delay(100); // Same delay every time

Batch Operations vs Transactions​

FeatureBatch OperationsTransactions
AtomicityPartial success possibleAll succeed or all fail
Capacity Cost1x standard operations2x standard operations
Conditional ExpressionsNot supportedSupported
Max Items25 writes / 100 reads100 items or 4MB
Use CaseIndependent bulk operationsACID requirements

Choose Batch Operations When:

  • Operations are independent
  • Partial success is acceptable
  • Cost optimization is important

Choose Transactions When:

  • Operations must succeed or fail together
  • You need conditional expressions across items
  • Data consistency is critical

See Also​