High-throughput patterns: batching and async

boto3 · Expert · 12 min

Moving millions of items through AWS is dominated by request count and latency, and batching changes the economics.

Each AWS call costs a network round trip and often a per-request fee, so throughput work is about fewer, fuller requests and more of them in flight.

Batch APIs:

  • DynamoDB table.batch_writer() buffers put_item/delete_item calls into BatchWriteItem requests of 25 items and automatically resends unprocessed items; overwrite_by_pkeys=[...] dedupes within a buffer. Reads use batch_get_item (100 keys), where you must handle UnprocessedKeys yourself.
  • SQS send_message_batch and delete_message_batch take up to 10 entries; check the Failed list in the response, since partial failures don't raise.
  • S3 delete_objects removes up to 1000 keys per call.

Concurrency: share one client per service across a thread pool sized to the workload, with max_pool_connections matching. For asyncio services, aiobotocore/aioboto3 give awaitable clients with the same API.

Overheads to remove: create clients once (client creation parses models and can take tens of milliseconds), reuse connections (keep-alive is automatic), and avoid head_object before every get_object when the get alone tells you what you need.

Backpressure: watch for throttling (ProvisionedThroughputExceededException, SlowDown, ThrottlingException), use adaptive retries for batch jobs, and spread hot keys. Measure with CloudWatch request metrics, not assumptions.

Example

from decimal import Decimal
import boto3

ddb = boto3.resource("dynamodb", region_name="us-east-1")
table = ddb.create_table(TableName="events", BillingMode="PAY_PER_REQUEST",
    KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
    AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}])
table.wait_until_exists()

with table.batch_writer(overwrite_by_pkeys=["id"]) as batch:   # 25 items per request
    for i in range(1000):
        batch.put_item(Item={"id": f"evt-{i}", "value": Decimal(i)})
print(table.scan(Select="COUNT")["Count"])     # 1000, written with 40 requests

sqs = boto3.client("sqs", region_name="us-east-1")
url = sqs.create_queue(QueueName="jobs")["QueueUrl"]
resp = sqs.send_message_batch(QueueUrl=url, Entries=[
    {"Id": str(i), "MessageBody": f"job {i}"} for i in range(10)])
print(len(resp["Successful"]), resp.get("Failed", []))   # 10 []

Remember

  • Batch APIs cut request count: DynamoDB 25 writes, SQS 10 messages, S3 1000 deletes
  • batch_writer resends unprocessed items; batch_get_item and SQS batches need manual checks
  • Share clients across a sized thread pool, or use aioboto3 for asyncio
  • Create clients once; watch throttling metrics

Watch out for

  • Ignoring the Failed list of send_message_batch and silently losing messages.

Check yourself

  1. What does DynamoDB's batch_writer do with UnprocessedItems returned by BatchWriteItem?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: boto3.amazonaws.com/v1/documentation/api/latest/guide/dynamodb.html#batch-writing