Home Python Async Patterns with anyio: Backend-Agnostic Async in Python
Advanced 3 min · July 14, 2026

Async Patterns with anyio: Backend-Agnostic Async in Python

Master anyio for backend-agnostic async I/O in Python.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 20-25 min read
  • Basic understanding of async/await in Python
  • Familiarity with asyncio or trio is helpful but not required
  • Python 3.7+ installed
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • anyio provides a unified async API that works across asyncio, trio, and curio backends.
  • Use anyio.run() instead of asyncio.run() to start your async program.
  • Replace asyncio.sleep() with anyio.sleep() for backend-agnostic delays.
  • Use anyio.create_task_group() for structured concurrency.
  • Use anyio.Queue for async producer-consumer patterns.
✦ Definition~90s read
What is Async Patterns with anyio?

anyio is a backend-agnostic async library that lets you write portable asynchronous code compatible with asyncio, trio, and curio.

Think of anyio as a universal remote that works with any TV brand.
Plain-English First

Think of anyio as a universal remote that works with any TV brand. Instead of learning separate remotes for asyncio, trio, and curio, you learn one set of buttons that works everywhere. This means you can write async code once and run it on any backend.

Asynchronous programming in Python has evolved rapidly, but the ecosystem remains fragmented. The standard library's asyncio is the most popular, but alternatives like trio and curio offer different philosophies—structured concurrency, better cancellation, and simpler error handling. However, choosing one backend locks you into its ecosystem. Enter anyio: a backend-agnostic async library that provides a unified API across all major async backends. With anyio, you can write portable async code that runs on asyncio, trio, or curio without modification. This is a game-changer for library authors who want to support multiple backends and for developers who want to future-proof their code. In this tutorial, you'll learn the core patterns of anyio: starting an async program, structured concurrency with task groups, cancellation and timeouts, synchronization primitives, and networking. You'll also see a real-world production incident where anyio's portability saved the day. By the end, you'll be able to write robust, backend-agnostic async code that works everywhere.

Getting Started with anyio

To begin, install anyio with pip: pip install anyio. The simplest anyio program uses anyio.run() to execute an async function. This replaces asyncio.run() and works with any backend. By default, anyio uses asyncio, but you can switch to trio by passing backend='trio'. Here's a basic example:

hello_anyio.pyPYTHON
1
2
3
4
5
6
7
8
import anyio

async def main():
    print('Hello, anyio!')
    await anyio.sleep(1)
    print('Done sleeping')

anyio.run(main)
Output
Hello, anyio!
Done sleeping
💡Backend Selection
📊 Production Insight
In production, you might want to allow the backend to be configured via environment variable. For example: backend = os.getenv('ANYIO_BACKEND', 'asyncio').
🎯 Key Takeaway
Use anyio.run() as the entry point for your async program. It abstracts away the backend selection.

Structured Concurrency with Task Groups

One of anyio's most powerful features is structured concurrency via TaskGroup. Unlike raw asyncio.create_task(), a task group ensures that all child tasks are completed or cancelled before the group exits. This prevents orphaned tasks and makes error handling predictable. If any task raises an exception, all sibling tasks are cancelled, and the exception is propagated. Here's an example:

task_group.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import anyio

async def worker(name, delay):
    print(f'{name} starting')
    await anyio.sleep(delay)
    print(f'{name} finished')
    return f'{name} result'

async def main():
    async with anyio.create_task_group() as tg:
        tg.start_soon(worker, 'A', 2)
        tg.start_soon(worker, 'B', 1)
        tg.start_soon(worker, 'C', 3)
    print('All tasks done')

anyio.run(main)
Output
A starting
B starting
C starting
B finished
A finished
C finished
All tasks done
⚠ Exception Propagation
📊 Production Insight
Task groups are ideal for fan-out/fan-in patterns, like making multiple API calls in parallel and aggregating results.
🎯 Key Takeaway
Use anyio.create_task_group() for structured concurrency. It ensures all tasks complete or are cancelled before moving on.

Cancellation and Timeouts

anyio provides a unified cancellation model through CancelScope. Unlike asyncio's CancelledError, anyio's cancellation is more predictable. You can create a cancel scope and cancel it manually, or use fail_after() for timeouts. Here's an example:

cancellation.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import anyio

async def long_running():
    try:
        await anyio.sleep(10)
    except anyio.get_cancelled_exc_class():
        print('Cancelled!')
        raise

async def main():
    async with anyio.create_task_group() as tg:
        tg.start_soon(long_running)
        await anyio.sleep(1)
        tg.cancel_scope.cancel()
    print('Task group exited')

anyio.run(main)
Output
Cancelled!
Task group exited
🔥Timeout with fail_after
📊 Production Insight
In production, always set timeouts on external I/O operations to prevent hanging. Use fail_after() as a context manager for fine-grained control.
🎯 Key Takeaway
Use CancelScope for manual cancellation and fail_after() for timeouts. Always catch get_cancelled_exc_class() if you need cleanup.

Async Synchronization Primitives

anyio provides backend-agnostic synchronization primitives like Lock, Event, Semaphore, and Queue. These work identically across backends. Here's an example using anyio.Queue for a producer-consumer pattern:

queue.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import anyio

async def producer(queue, n):
    for i in range(n):
        await queue.put(i)
        print(f'Produced {i}')
        await anyio.sleep(0.5)
    await queue.put(None)  # sentinel

async def consumer(queue):
    while True:
        item = await queue.get()
        if item is None:
            break
        print(f'Consumed {item}')

async def main():
    queue = anyio.Queue(maxsize=5)
    async with anyio.create_task_group() as tg:
        tg.start_soon(producer, queue, 10)
        tg.start_soon(consumer, queue)

anyio.run(main)
Output
Produced 0
Consumed 0
Produced 1
Consumed 1
... (truncated)
💡Queue Capacity
📊 Production Insight
For high-throughput systems, consider using anyio.create_memory_object_stream() which provides a more efficient channel-based communication.
🎯 Key Takeaway
anyio's synchronization primitives are drop-in replacements for asyncio's, but they work with any backend.

Async Networking with anyio

anyio provides high-level networking APIs for TCP and UDP. The anyio.connect_tcp() and anyio.create_tcp_listener() functions are backend-agnostic. Here's a simple echo server and client:

echo_client.pyPYTHON
1
2
3
4
5
6
7
8
9
10
import anyio

async def main():
    stream = await anyio.connect_tcp('127.0.0.1', 12345)
    async with stream:
        await stream.send(b'Hello')
        response = await stream.receive(1024)
        print(f'Received: {response}')

anyio.run(main)
Output
Received: b'Hello'
🔥Stream API
📊 Production Insight
For production servers, always handle connection errors and use timeouts. anyio's networking is built on top of the backend's event loop, so it's as performant as native code.
🎯 Key Takeaway
Use anyio.connect_tcp() and anyio.create_tcp_listener() for backend-agnostic networking.

Running Blocking Code with anyio

anyio provides to_thread.run_sync() to run blocking code in a separate thread without blocking the event loop. This is essential for integrating with synchronous libraries. Here's an example:

blocking.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
import anyio
import time

def blocking_io():
    time.sleep(2)
    return 'result'

async def main():
    result = await anyio.to_thread.run_sync(blocking_io)
    print(f'Got {result}')

anyio.run(main)
Output
Got result
⚠ Thread Safety
📊 Production Insight
For CPU-bound tasks, consider using anyio.to_process.run_sync() to run in a separate process. This avoids the GIL and leverages multiple cores.
🎯 Key Takeaway
Use anyio.to_thread.run_sync() to offload blocking operations to a thread pool.
● Production incidentPOST-MORTEMseverity: high

The Great Backend Migration: How anyio Prevented a Rewrite

Symptom
The service would occasionally hang on shutdown, leaving connections open and causing resource leaks.
Assumption
The team assumed the issue was in their custom cancellation logic and spent weeks debugging.
Root cause
asyncio's cancellation model is cooperative and can be tricky to get right, especially with third-party libraries that don't handle CancelledError properly.
Fix
The team migrated to anyio, which provides a unified cancellation API. They replaced asyncio-specific calls with anyio equivalents, and the service ran on trio without issues.
Key lesson
  • Use anyio for backend-agnostic async code to avoid vendor lock-in.
  • anyio's structured concurrency simplifies cancellation and error handling.
  • Test your async code with multiple backends to catch backend-specific bugs.
  • anyio's TaskGroup ensures all tasks are cleaned up on cancellation.
  • Adopt anyio early to future-proof your async code.
Production debug guideSymptom to Action4 entries
Symptom · 01
Task hangs indefinitely
Fix
Check for missing cancellation points; use anyio.fail_after() to set timeouts.
Symptom · 02
Unhandled exception in task group
Fix
Wrap task group in try-except; exceptions are propagated to the parent.
Symptom · 03
Deadlock with anyio.Queue
Fix
Ensure queue size is finite and use anyio.create_queue(10) to limit capacity.
Symptom · 04
Backend-specific behavior
Fix
Run the same code with anyio.run(backend='trio') and compare.
★ Quick Debug Cheat SheetCommon async issues and their anyio solutions.
Task not cancelled
Immediate action
Use `anyio.CancelScope`
Commands
async with anyio.CancelScope() as scope:
scope.cancel()
Fix now
Wrap the blocking operation in a cancel scope.
Timeout not working+
Immediate action
Use `anyio.fail_after()`
Commands
async with anyio.fail_after(5):
await long_operation()
Fix now
Replace asyncio.wait_for with anyio.fail_after.
Queue full+
Immediate action
Increase maxsize or use `anyio.Queue(maxsize=0)` for unbounded
Commands
queue = anyio.Queue(100)
await queue.put(item)
Fix now
Set an appropriate maxsize for your workload.
Backend mismatch+
Immediate action
Check `anyio.get_backend()`
Commands
print(anyio.get_backend())
anyio.run(main, backend='trio')
Fix now
Explicitly specify backend when running.
Featureasynciotrioanyio
Entry pointasyncio.run()trio.run()anyio.run()
Task creationasyncio.create_task()nursery.start_soon()task_group.start_soon()
CancellationCancelledErrorCancelledCancelScope
Timeoutasyncio.wait_for()trio.fail_after()anyio.fail_after()
Queueasyncio.Queuetrio.Queueanyio.Queue
Backend supportOnly asyncioOnly trioasyncio, trio, curio
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
hello_anyio.pyasync def main():Getting Started with anyio
task_group.pyasync def worker(name, delay):Structured Concurrency with Task Groups
cancellation.pyasync def long_running():Cancellation and Timeouts
queue.pyasync def producer(queue, n):Async Synchronization Primitives
echo_client.pyasync def main():Async Networking with anyio
blocking.pydef blocking_io():Running Blocking Code with anyio

Key takeaways

1
anyio provides a unified async API that works with asyncio, trio, and curio backends.
2
Use anyio.run() as the entry point and anyio.create_task_group() for structured concurrency.
3
Leverage CancelScope and fail_after() for cancellation and timeouts.
4
Use anyio's synchronization primitives (Queue, Lock, Event) for portable async code.
5
Offload blocking code with anyio.to_thread.run_sync().
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is structured concurrency and how does anyio implement it?
Q02JUNIOR
How would you implement a timeout for an async function using anyio?
Q03SENIOR
Explain how anyio's cancellation differs from asyncio's.
Q01 of 03SENIOR

What is structured concurrency and how does anyio implement it?

ANSWER
Structured concurrency ensures that all child tasks are completed or cancelled before the parent scope exits. anyio implements it via TaskGroup, which cancels all tasks if one fails and propagates exceptions.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between anyio and asyncio?
02
Can I use anyio with existing asyncio code?
03
How do I choose between asyncio and trio backend?
04
Does anyio support Python 3.7+?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Advanced Python. Mark it forged?

3 min read · try the examples if you haven't

Previous
CI/CD for Python: GitHub Actions, Testing, and Deployment
29 / 35 · Advanced Python
Next
Publishing Python Packages to PyPI with pyproject.toml