7 Async Patterns for Running AI Agents in Python
AI agents are becoming more capable, but making an agent intelligent is only part of the challenge. A practical agent also needs to perform tasks efficiently: calling APIs, reading files, searching databases, waiting for external services, and sometimes managing several operations at the same time.
This is where asynchronous programming in Python becomes useful.
Python's asyncio framework allows an application to perform other work while it is waiting for an operation to finish. For AI agents, this can significantly improve responsiveness, particularly when the workload is dominated by network requests and other I/O operations.
In this article, we will explore seven useful async patterns for running agents in Python, with simple examples and explanations of when each pattern makes sense.
What Is Async Programming?
In traditional synchronous code, operations generally execute one after another.
Imagine an agent needs to:
- Search the web.
- Call a weather API.
- Query a database.
- Ask an LLM for a response.
If every operation waits for completion before the next one starts, the agent can spend a lot of time simply waiting.
Asynchronous programming changes this behavior.
An asynchronous function can pause while waiting for an I/O operation, allowing other tasks to run during that time.
A basic Python async function looks like this:
import asyncio
async def agent_task():
print("Agent is working...")
await asyncio.sleep(1)
print("Task completed")
asyncio.run(agent_task())
The await keyword tells Python that the function can pause at that point while other asynchronous work gets an opportunity to execute.
1. Sequential Async Pattern
The first pattern is the simplest: use asynchronous functions but execute operations sequentially.
async def run_agent():
result1 = await call_tool_one()
result2 = await call_tool_two()
return result1, result2
Although the functions are asynchronous, the second operation does not begin until the first one finishes.
This approach is useful when tasks depend on each other.
For example:
Search → Analyze Results → Generate Answer
The agent cannot analyze the results until the search has completed.
When to use it
Use sequential async execution when:
- One operation depends on another.
- Execution order matters.
- You want simple and predictable control flow.
Async does not automatically mean parallel execution. Sometimes sequential execution is exactly what an agent needs.
2. Concurrent Tasks with asyncio.gather()
When tasks are independent, running them concurrently can save time.
Python provides asyncio.gather() for this purpose.
async def run_agent():
results = await asyncio.gather(
search_web(),
get_weather(),
query_database()
)
return results
Instead of waiting for each operation separately, the agent starts all three asynchronous operations and waits for their results.
This is particularly useful when an agent needs information from several independent tools.
For example:
Web Search + Database Query + API Request
If each operation takes two seconds, sequential execution could take roughly six seconds. Concurrent execution can potentially reduce the waiting time considerably, assuming the services can actually run concurrently.
3. Creating Background Tasks
Sometimes an agent needs to start work without immediately waiting for the result.
Python's asyncio.create_task() allows you to schedule a coroutine as a task.
async def agent():
background_task = asyncio.create_task(
update_memory()
)
result = await perform_main_task()
await background_task
return result
Here, update_memory() can run while the main task is being processed.
This pattern can be useful for agent activities such as:
- Updating non-critical state
- Preparing data
- Logging
- Prefetching information
- Performing background maintenance
However, background tasks should not simply be forgotten. If the result matters, you should eventually await the task or otherwise manage its lifecycle.
4. Producer-Consumer Pattern with asyncio.Queue
More advanced agents sometimes need a pipeline where one part produces work and another part processes it.
An asyncio.Queue is useful for this architecture.
import asyncio
queue = asyncio.Queue()
async def producer():
for item in range(5):
await queue.put(item)
async def consumer():
while True:
item = await queue.get()
if item is None:
break
print("Processing:", item)
queue.task_done()
The producer puts jobs into the queue, while the consumer retrieves and processes them.
This pattern works well for agents that handle many jobs, such as:
Incoming Requests → Task Queue → Agent Workers
You can also create multiple consumers to process tasks concurrently.
5. Async Timeouts
Agents often depend on external services. An API may become slow, unavailable, or temporarily unresponsive.
Allowing an agent to wait indefinitely is usually a bad idea.
Python provides timeout mechanisms that can prevent this problem.
For example:
async def run_agent():
try:
result = await asyncio.wait_for(
call_external_tool(),
timeout=10
)
return result
except asyncio.TimeoutError:
return "The tool timed out."
If the operation takes longer than ten seconds, the agent can stop waiting and take another action.
Timeouts are especially useful for:
- API calls
- Database operations
- Web requests
- Agent tool calls
- External model services
A robust agent should have a strategy for handling slow dependencies.
6. Retry with Exponential Backoff
External services can fail temporarily.
For example, an API might return an error because of a temporary network problem or rate limit.
Instead of immediately giving up, an agent can retry the operation.
A simplified pattern looks like this:
async def retry_operation():
delay = 1
for attempt in range(3):
try:
return await call_tool()
except Exception:
if attempt == 2:
raise
await asyncio.sleep(delay)
delay *= 2
The delays are approximately:
1 second → 2 seconds → 4 seconds
This technique is called exponential backoff.
It prevents an agent from repeatedly hitting a failing service in rapid succession.
In production applications, retries should normally distinguish between temporary errors and permanent failures. Not every exception should automatically trigger another request.
7. Async Agent Pipelines
The final pattern combines several async techniques into a structured workflow.
Imagine an agent that performs:
Input → Planning → Tool Calls → Validation → Final Response
Each stage can be represented as an asynchronous function.
async def run_agent(user_input):
plan = await create_plan(user_input)
results = await asyncio.gather(
execute_tool(plan[0]),
execute_tool(plan[1])
)
validated = await validate_results(results)
response = await generate_response(validated)
return response
This approach provides a clean architecture for larger agents.
Independent tool calls can execute concurrently, while dependent stages remain sequential.
For example:
User Request
↓
Planner
↓
┌───┴────┐
↓ ↓
Tool A Tool B
└───┬────┘
↓
Validator
↓
LLM Response
This hybrid model is often more practical than trying to make everything concurrent.
Choosing the Right Pattern
Different agent workloads require different approaches.
| Pattern | Best Use |
|---|---|
| Sequential async | Dependent operations |
asyncio.gather() |
Independent tasks |
| Background tasks | Non-blocking supporting work |
| Async queue | Job pipelines and worker systems |
| Timeouts | Unreliable or slow services |
| Retry/backoff | Temporary failures |
| Async pipeline | Multi-stage agent workflows |
The important point is that concurrency should be intentional.
Running everything simultaneously can create problems such as API rate-limit violations, excessive memory usage, race conditions, and difficult-to-debug failures.
Async Doesn't Make CPU-Heavy Work Automatically Faster
One common misconception is that asyncio makes every Python program faster.
It does not.
Async programming is particularly effective for I/O-bound workloads, where the application spends significant time waiting for external operations.
Examples include:
- HTTP requests
- Database queries
- File operations
- Network services
- Remote AI model calls
CPU-intensive operations may require other approaches, such as multiprocessing or specialized libraries.
Best Practices for Async Agents
When building production-quality agents, keep several principles in mind.
Use concurrency selectively. Only run tasks concurrently when they are independent.
Set timeouts. External operations should not be allowed to block an agent indefinitely.
Handle exceptions. One failed tool call should not necessarily crash the entire agent.
Limit concurrency. If an agent makes hundreds of requests simultaneously, the target service or your own application may become overloaded.
Track tasks carefully. Background tasks should have clear ownership and lifecycle management.
Keep workflows understandable. Highly complicated async code can become harder to maintain than simple sequential code.
Conclusion
Asynchronous programming is an important skill for building responsive Python agents. The asyncio ecosystem provides several patterns that can help agents manage multiple operations efficiently.
The seven patterns discussed here—sequential async execution, concurrent tasks, background tasks, producer-consumer queues, timeouts, retry with exponential backoff, and async pipelines—cover many common agent architectures.
The best design is rarely the one that uses the most concurrency. Instead, a good agent uses async execution where it provides a real advantage while keeping dependencies, failures, and task lifecycles under control.
Once you understand these patterns, you can begin building Python agents that interact with multiple tools, APIs, databases, and AI models without becoming unnecessarily slow or difficult to manage.



