Async and Parallelism in Agent Systems#

Async lets one program work on other tasks while it waits for a network, model, database, or file operation.

Agent systems spend a lot of time waiting, so async can reduce total waiting time.

Short video#

Asyncio in Python — Yash Jain (15 min, November 2025)

Key terms#

TermSimple meaning
SynchronousFinish one task before starting the next.
AsynchronousWork on another task while waiting.
ConcurrencySeveral tasks make progress during the same period.
ParallelismSeveral tasks run at the exact same time.
CoroutineFunction declared with async def.
awaitPause this coroutine until an operation finishes.
Event loopSchedules coroutines that are ready to run.

Sequential vs concurrent#

flowchart LR
    subgraph Sequential
        A1[Call A] --> B1[Call B] --> C1[Call C]
    end
    subgraph Concurrent
        S[Start] --> A2[Call A]
        S --> B2[Call B]
        S --> C2[Call C]
    end

Small example#

import asyncio

async def call_api(name):
    await asyncio.sleep(1)  # represents network waiting
    return f"{name} finished"

async def main():
    results = await asyncio.gather(call_api("A"), call_api("B"))
    print(results)

asyncio.run(main())

Both calls wait together, so this takes about one second instead of two.

Which approach should you use?#

WorkGood choice
API, model, or database callsasyncio
Old blocking I/O libraryThread
Heavy CPU calculationProcess
Simple dependent stepsSequential code

Practical rules#

  • Run calls concurrently only when they are independent.
  • Set a timeout for every external call.
  • Limit concurrency so an API is not overloaded.
  • Retry only temporary failures and cap the retry count.
  • Do not run blocking code such as time.sleep() inside async def.
  • Keep writes ordered when one action depends on another.

A bounded parallel-read helper#

Use this for independent reads such as fetching several known URLs. It has a per-call timeout, a concurrency limit, and a result shape that makes partial failure explicit.

import asyncio
import httpx

limit = asyncio.Semaphore(5)

async def fetch(client: httpx.AsyncClient, url: str) -> dict:
    try:
        async with limit, asyncio.timeout(10):
            response = await client.get(url)
            response.raise_for_status()
            return {"url": url, "status": "ok", "text": response.text}
    except (httpx.HTTPError, TimeoutError) as error:
        return {"url": url, "status": "failed", "error": str(error)}

async def fetch_all(urls: list[str]) -> list[dict]:
    async with httpx.AsyncClient(follow_redirects=True) as client:
        return await asyncio.gather(*(fetch(client, url) for url in urls))
uv add httpx
uv run python fetch_sources.py

Do not use this pattern for dependent work: search → choose source → summarize must remain sequential. For writes, add an idempotency key and stop if any required precondition fails.

Choose the execution model#

WorkStart withPractical rule
Independent HTTP, model, or database readsasyncioUse a semaphore and timeout
Blocking SDK you cannot replaceasyncio.to_thread()Keep shared state out of the thread
CPU-heavy parsing or image workProcessPass file paths, not huge Python objects
Ordered writes or decisionsSequential codeVerify one step before the next

Cancellation and result handling#

results = await fetch_all(urls)
successful = [item for item in results if item["status"] == "ok"]
failed = [item for item in results if item["status"] != "ok"]

if not successful:
    raise RuntimeError("No source could be fetched")

Use try/finally around files, database connections, and browser sessions. Set a clear policy for partial success: a research digest may continue with two sources; a payment or deployment should fail closed.

References#