Prefect Logo
Open Source Love Letters

How AnyIO Powers Prefect's Async Architecture

Our love letter to AnyIO.

March 24, 2025
Adam Azzam, PhD
VP, Product

Prefect is built on a lot of great ideas, but even more importantly, it’s built on great tools. This post is part of a series of love letters to the libraries and technologies that make Prefect possible.

We’re kicking things off with AnyIO — the async concurrency library that brought sanity, structure, and elegance to our workflow engine. If you’ve ever tried to wrangle asyncio and lived to tell the tale, you’ll understand why this one deserves a bit more than just a passing mention. This is our thank-you, our technical deep dive, and maybe just a little bit of a love letter.

In large-scale data processing and workflow orchestration, concurrency is crucial for keeping systems responsive and utilizing resources efficiently. Yet managing concurrency with raw asyncio can quickly turn into a mind-bending maze of callbacks, race conditions, and partial cancellations. That’s where AnyIO comes in, bridging Python’s main async frameworks under one carefully designed API—and freeing developers (us included!) to focus on building reliable, high-scale applications.

Below, we’ll explore why Prefect relies so heavily on AnyIO, show how we use it to solve real-world concurrency challenges, and share the lessons we’ve learned that might save you from your own async pitfalls. Let’s dive in.

The Challenge of Asynchronous Programming in Python

Asynchronous programming has become indispensable for building responsive, scalable systems. By allowing tasks to run concurrently—whether you’re dealing with heavy network I/O, CPU-bound computations, or file reads—you can make sure your program isn’t just sitting idle waiting for one operation to finish. In other words, async code helps you squeeze the most out of your CPU cycles, memory, and network connections so your application can handle more work without grinding to a halt.

Python’s async journey started with asyncio in Python 3.4, standardizing cooperative multitasking but also introducing a fair bit of complexity. That complexity paved the way for Trio, designed with a focus on simplicity, better debugging, and correctness. However, it also meant developers had to pick sides between asyncio and Trio, leaving the ecosystem fragmented.

In short, as the ecosystem evolved, fragmentation and complexity became the norm. So how did AnyIO unify the landscape? Let’s look under the hood.

What is AnyIO? A Technical Overview

AnyIO is an asynchronous concurrency library that lets you seamlessly write code compatible with Python’s two main async frameworks: asyncio and trio. But it's more than just a compatibility layer—it’s designed to blend the best ideas from both frameworks into a clear, unified API that smooths out their quirks and differences.

Here’s why this matters in practice:

1import asyncio
2
3async def asyncio_example():
4    await asyncio.sleep(1)
5    return "completed with asyncio"
6
7# trio version
8import trio
9
10async def trio_example():
11    await trio.sleep(1)
12    return "completed with trio"

But with AnyIO, you only write your async code once and run it anywhere:

1import anyio
2
3async def anyio_example():
4    await anyio.sleep(1)
5    return "completed with AnyIO"
6
7# Run your code with the asyncio backend
8anyio.run(anyio_example, backend="asyncio")
9
10# Or swap to trio just as easily
11anyio.run(anyio_example, backend="trio")

Beyond just simplifying your choices, AnyIO provides clean and consistent interfaces for common async needs:

  • Task Groups: Manage groups of tasks easily, with automatic error handling and cleanup.
  • Cancellation Scopes: Cancel tasks gracefully and reliably, ensuring resources are cleaned up properly.
  • Networking and Streams: Use the same APIs for TCP/UDP sockets, pipes, and more across both frameworks.
  • Synchronization Tools: Get locks, events, semaphores, and conditions—everything you need to coordinate tasks without headaches.
  • File Operations: Perform file reads and writes asynchronously without blocking your event loop.
  • Subprocesses: Run external commands efficiently within your async workflows.

The beauty of AnyIO is how it guides you toward structured concurrency—helping you write async code that's predictable, easy to reason about, and resistant to subtle bugs like resource leaks or deadlocks.

AnyIO's Position in the Python Async Ecosystem

AnyIO sits at a unique intersection in Python's async ecosystem. Unlike most async libraries, which typically commit exclusively to either asyncio or trio, AnyIO bridges the gap by offering a universal abstraction that works seamlessly with both frameworks. This flexibility becomes even more crucial as the ecosystem grows and developers seek solutions that aren't locked into one specific tool.

AnyIO shines especially bright for middleware and framework developers, enabling them to build software that remains agnostic to their users' async framework preferences. This versatility is why many prominent projects have embraced AnyIO:

  • Starlette: To power backend-agnostic web services.
  • Prefect: For robust, async-powered workflow orchestration.
  • OpenAI, Anthropic, and Google Gemini Python SDKs: For reliable async file uploads in interactions with large language models.
  • DsPY: To efficiently handle concurrent calls to LLMs while managing resources effectively.

By providing a unified and easy-to-use abstraction, AnyIO helps maintain cohesion and compatibility across the diverse landscape of Python's asynchronous ecosystem.

But who is behind AnyIO’s success? Naturally, you might wonder who’s guiding and shaping its evolution.

The Minds Behind AnyIO: Maintainers and Community

AnyIO is primarily maintained by Alex Grönholm, an experienced software developer deeply involved in Python’s async community. Alex also created asphalt, an application framework, and actively contributed to the ASGI specification, which significantly shaped Python web frameworks.

The project thrives on contributions from developers throughout the Python community, including core maintainers from both asyncio and trio. This collaborative approach ensures that AnyIO embodies a consensus on asynchronous programming best practices rather than reflecting just one developer’s vision.

Why Prefect Chose AnyIO: Technical Decision Points

At Prefect, concurrency isn't just a feature—it's at the heart of our workflow engine. Here's what we needed:

  1. Parallel Task Execution: Running many tasks simultaneously while respecting their dependencies.
  2. Graceful Cancellation: Easily and reliably stopping tasks when users halt their workflows.
  3. Reliable Timeouts: Preventing runaway tasks from consuming resources indefinitely.
  4. Clear, Debuggable Execution Paths: Ensuring transparency and ease of debugging in complex concurrent scenarios.

We chose AnyIO for a few key reasons:

  • Structured Concurrency: AnyIO's structured concurrency model clearly defines task lifetimes and relationships. This aligns perfectly with our workflows, ensuring reliable cleanup even when things go wrong.
  • Backend Flexibility: Currently, we rely on asyncio, but the option to seamlessly switch to trio without rewriting code gives us strategic flexibility. It also allows us to leverage libraries from either ecosystem.
  • Robust Cancellation and Timeout Handling: AnyIO's powerful primitives for handling cancellations and timeouts surpass raw asyncio capabilities, essential for a workflow system where task cancellation needs to be dependable.
  • Clean and Thoughtful API Design: AnyIO’s APIs are carefully designed to avoid common async pitfalls. Its task groups, for example, handle exceptions cleanly and prevent resource leaks—critical features for running user-defined workflows safely and reliably.

AnyIO in Action: How Prefect Leverages the Library

Below, we’ll show two major ways AnyIO underpins Prefect’s concurrency story. First, let’s look at how Prefect’s worker architecture uses structured concurrency for reliable task execution. Then we’ll see how capacity limiters solve a common real-world problem—SQLite concurrency issues.

Flow Run Orchestration Using TaskGroups

In Prefect, workers form the backbone of our orchestration lifecycle, continuously polling for scheduled workflows and delegating their execution. At the core of this lifecycle is structured concurrency, a powerful pattern implemented via AnyIO’s TaskGroups.

Structured concurrency explicitly models task dependencies, creating a clear hierarchical structure of upstream and downstream relationships. This ensures that cancellations and errors propagate swiftly and coherently through the execution graph, gracefully terminating affected tasks while maintaining system integrity.

Early deployments revealed several concurrency challenges, including deadlocks and cancellation race conditions. TaskGroups addressed these issues elegantly by offering predictable cleanup, clear error propagation, resource management, and simpler concurrency overall.

Here’s how we integrate TaskGroups into our BaseWorker class:

1async def setup(self) -> None:
2    """Prepares the worker to run."""
3    self._logger.debug("Setting up worker...")
4    self._runs_task_group = anyio.create_task_group()
5
6    # ... other setup code ...
7
8    await self._exit_stack.enter_async_context(self._runs_task_group)
9
10    # ... additional setup code ...
11
12    self.is_setup = True

By initializing a task group within an exit stack during setup, we guarantee all tasks within the group are properly awaited or canceled upon teardown, eliminating orphaned processes.

When submitting flow runs concurrently, we use TaskGroup’s start_soon method:

1# From the _submit_scheduled_flow_runs method
2self._runs_task_group.start_soon(self._submit_run, flow_run)

For tasks returning values, we prefer the .start() method:

1# From the _submit_run method
2readiness_result = await self._runs_task_group.start(self._submit_run_and_capture_errors, flow_run)

Handling cancellations cleanly was among our most challenging problems. With structured concurrency, cancellations naturally propagate through task hierarchies. We use AnyIO’s cancellation scopes to define precise boundaries:

1async def _schedule_task(
2    self, __in_seconds: int, fn: Callable[..., Any], *args: Any, **kwargs: Any
3):
4    """Schedule a background task to start after a delay."""
5    async def wrapper(task_status: anyio.abc.TaskStatus[Any]):
6        if self.is_setup:
7            with anyio.CancelScope() as scope:
8                self._scheduled_task_scopes.add(scope)
9                task_status.started()
10                await anyio.sleep(__in_seconds)
11                self._scheduled_task_scopes.remove(scope)
12        else:
13            task_status.started()
14
15        result = fn(*args, **kwargs)
16        if asyncio.iscoroutine(result):
17            await result
18
19    await self._runs_task_group.start(wrapper)

During teardown, cancellation is straightforward:

1async def teardown(self, *exc_info: Any) -> None:
2    """Cleans up resources after the worker is stopped."""
3    self._logger.debug("Tearing down worker...")
4    self.is_setup = False
5    for scope in self._scheduled_task_scopes:
6        scope.cancel()
7
8    # ... other cleanup code ...

In a production setting, dozens or hundreds of tasks can spawn simultaneously. AnyIO’s structured concurrency ensures tasks are never orphaned if the parent is canceled. The predictable cleanup model spares developers from headaches like race conditions and stuck processes.

Database Protection via Capacity Limiters

Beyond orchestrating flow runs, concurrency also impacts our database usage. Prefect uses a database to track workflow states, initially prioritizing SQLite for simplicity. However, SQLite struggles with concurrent writes, quickly leading to "database is locked" errors under high concurrency.

In real-world scenarios, these resource bottlenecks can cripple applications. To protect SQLite from overload, we introduced a simple yet effective middleware using AnyIO’s CapacityLimiter:

1class RequestLimitMiddleware:
2    """
3    Middleware limiting concurrent API requests to protect SQLite.
4    """
5
6    def __init__(self, app: Any, limit: float):
7        self.app = app
8        self._limiter = anyio.CapacityLimiter(limit)
9
10    async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
11        async with self._limiter:
12            await self.app(scope, receive, send)

This middleware ensures:

  • A controlled maximum number of concurrent operations.
  • Fair request handling through FIFO queuing.
  • Requests waiting in a queue if the concurrency limit is reached, resuming as soon as capacity becomes available.

The result? SQLite remains responsive, and lock errors are drastically reduced. This simple pattern can work for any shared resource—like an external API—where too many concurrent requests can lead to throttling, rate limits, or outright failures.

Performance and Reliability Benefits for Prefect Users

From a performance perspective, AnyIO adds minimal overhead compared to direct asyncio usage, typically less than 1ms per operation. The library is also memory-efficient, avoiding unnecessary allocations and object churn that can impact systems running thousands of concurrent workflows.

Looking Forward: AnyIO's Future and Its Impact on Prefect

The future of AnyIO looks promising. The library continues to mature, with recent improvements focused on:

  • Better memory efficiency for large-scale concurrent operations.
  • Enhanced networking primitives for modern protocols.
  • More comprehensive support for structured concurrency patterns.

For Prefect, these improvements directly translate to a more capable and reliable workflow engine. As we scale to support more complex orchestration scenarios, AnyIO’s abstractions provide the foundation we need. Looking ahead, we see several exciting possibilities:

  1. Enhanced Cancellation Semantics: Future versions of AnyIO may provide even more nuanced cancellation controls, allowing for partial workflow cancellation with sophisticated cleanup logic.
  2. Backend Optimizations: As both asyncio and Trio evolve, AnyIO will provide access to performance improvements from either ecosystem without requiring code changes.
  3. Ecosystem Growth: As more libraries adopt AnyIO, Prefect will be able to integrate with a wider range of async tools regardless of their underlying implementation.

AnyIO may not be the most well-known Python library, but it’s a critical piece of infrastructure that enables Prefect to deliver a reliable, performant workflow orchestration engine. By providing unified abstractions over Python’s async landscape, it allows us to focus on building the best possible orchestration experience rather than wrestling with low-level concurrency management.

For data engineers already using Prefect, understanding AnyIO helps explain how we achieve reliable concurrent execution. For those building their own async systems, AnyIO represents a battle-tested approach to managing concurrency that’s worth consideration.

We’re grateful to Alex Grönholm and the AnyIO community for their dedication to improving Python’s async story. If you’re curious to try AnyIO for yourself, check out the official AnyIO documentation and Prefect’s own docs on concurrency and orchestration. You’ll be well on your way to writing clear, resilient async code.