We had this gross ThreadPoolExecutor pattern in a Lambda that was hitting DynamoDB and external APIs. Thought it was clever. It wasn't. Just added complexity and kept hitting memory limits.
Switched to asyncio with httpx and boto3 async wrappers. The gains were immediate. Same workload, half the memory, 3x faster. No threading overhead.
async def fetch_batch(keys):
async with httpx.AsyncClient() as client:
tasks = [client.get(f"/api/{k}") for k in keys]
return await asyncio.gather(*tasks)
Key insight: Python's async is not about true parallelism, it's about not blocking on I/O. If you're doing CPU work, you're wasting time. We had some computation mixed in and it actually got slower until we moved that to a separate step.
The only real friction was aioboto3. It works but the DynamoDB client API is clunky. Ended up batching queries manually instead of relying on the convenience methods.
Lambda cold starts went down too, probably because the runtime isn't spinning up thread stacks. Worth doing if you're I/O bound. If you're CPU bound, save yourself the headache and use processes or just accept synchronous code.
Threading in Lambda is almost always the wrong move. You're fighting the runtime, not with it. Good call ripping it out.
That said, watch your async library choices. httpx is solid, but boto3's async story is still rough around the edges. aioboto3 works but adds a dependency. We ended up wrapping the sync boto3 calls in a thread pool with asyncio.to_thread() for DynamoDB - sounds backwards but it actually performs better than fighting boto3's internals.
The real win here is Lambda's event-driven nature. You're getting IO concurrency without managing threads. Exactly what async was built for.
Jake Morrison
DevOps engineer. Terraform and K8s all day.
Threading in Lambda is a trap. You're fighting the runtime, not with it. Async is the right call there.
One thing though: if you're wrapping boto3, make sure you're using aioboto3 or similar. The async wrappers people write themselves often don't actually parallelize the I/O properly, just shuffle thread overhead around.
The memory win you're seeing is real. ThreadPoolExecutor keeps thread stacks alive even when idle. Asyncio tasks are basically free compared to that.
Did you hit any issues with connection pooling or Lambda's execution environment when you switched? That's usually where async setups fail in Lambda.