Concurrency: threads, clients and sessions

boto3 · Advanced · 10 min

Parallel API calls give big speedups for I/O-bound AWS work, but sharing the wrong objects across threads causes subtle bugs.

boto3 calls are blocking network I/O, so threads parallelize them well despite the GIL. The thread-safety rules from the boto3 docs:

  • Clients are generally thread-safe: create one and share it among threads. (Mutating a client's event handlers or config while in use isn't safe, which ordinary code doesn't do.)
  • Sessions are not thread-safe: don't create clients from a shared session concurrently. Create clients up front in the main thread, or a session per thread.
  • Resources are not thread-safe: create one per thread.

Each client has a connection pool of max_pool_connections (default 10). With 32 threads sharing a client, 22 wait for connections and urllib3 logs "Connection pool is full" warnings. Set Config(max_pool_connections=32) to match your thread count.

concurrent.futures.ThreadPoolExecutor is the simple pattern: submit one task per key, region or account, collect results with as_completed, and handle exceptions per task.

Mind API quotas: parallel calls hit throttling sooner. Use the standard or adaptive retry mode and cap concurrency per service.

For multiprocessing, create clients in each child process after the fork; clients hold sockets and must not be shared across processes. For asyncio applications, the third-party aiobotocore and aioboto3 packages provide async clients.

Example

from concurrent.futures import ThreadPoolExecutor, as_completed
import boto3
from botocore.config import Config

s3 = boto3.client("s3", region_name="us-east-1",
                  config=Config(max_pool_connections=16))   # match the pool to threads
s3.create_bucket(Bucket="demo-threads-2026")
keys = [f"in/{i:03}.txt" for i in range(40)]
for k in keys:
    s3.put_object(Bucket="demo-threads-2026", Key=k, Body=k.encode())

def size_of(key):                                   # shares the thread-safe client
    return key, s3.head_object(Bucket="demo-threads-2026", Key=key)["ContentLength"]

sizes = {}
with ThreadPoolExecutor(max_workers=16) as pool:
    futures = [pool.submit(size_of, k) for k in keys]
    for fut in as_completed(futures):
        key, size = fut.result()                   # re-raises errors per task
        sizes[key] = size
print(len(sizes), sum(sizes.values()))              # 40 400

Remember

  • Share clients across threads; not sessions or resources
  • Match max_pool_connections to the number of threads
  • ThreadPoolExecutor parallelizes I/O-bound AWS calls well
  • Create clients per process after forking; use aioboto3 for asyncio

Watch out for

  • Calling boto3.client() concurrently from many threads using the shared default session.

Check yourself

  1. Which boto3 object is safe to share among threads?

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/clients.html#multithreading-or-multiprocessing-with-clients