Skip to main content
Modal provides full support for Python’s async/await syntax, allowing you to write asynchronous functions that run efficiently in the cloud.

Async functions

Define async functions using the async def syntax:
import modal
import asyncio

app = modal.App("async-example")

@app.function()
async def async_hello(name: str):
    await asyncio.sleep(1)  # Simulate async work
    return f"Hello, {name}!"

Calling async functions

From async context

Use .remote.aio() to call functions asynchronously:
@app.local_entrypoint()
async def main():
    result = await async_hello.remote.aio("Alice")
    print(result)

From sync context

Use .remote() to call async functions synchronously:
@app.local_entrypoint()
def main():
    result = async_hello.remote("Bob")
    print(result)

Parallel execution

Using map with async

Process multiple inputs concurrently:
@app.local_entrypoint()
async def main():
    names = ["Alice", "Bob", "Charlie"]
    
    # Map returns async results
    async for result in async_hello.map.aio(names):
        print(result)

Manual concurrency with asyncio

For more control, use asyncio.gather():
@app.local_entrypoint()
async def main():
    tasks = [
        async_hello.remote.aio("Alice"),
        async_hello.remote.aio("Bob"),
        async_hello.remote.aio("Charlie")
    ]
    results = await asyncio.gather(*tasks)
    print(results)

Async generators

Modal supports async generators for streaming results:
@app.function()
async def generate_numbers(n: int):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

@app.local_entrypoint()
async def main():
    async for number in generate_numbers.remote_gen.aio(10):
        print(f"Received: {number}")

Mixing sync and async

You can call synchronous functions from async code and vice versa:
@app.function()
def sync_function(x: int):
    return x * 2

@app.function()
async def async_function(x: int):
    # Call sync function from async context
    result = await sync_function.remote.aio(x)
    return result + 1

@app.local_entrypoint()
async def main():
    result = await async_function.remote.aio(5)
    print(result)  # 11

Async HTTP requests

Use async HTTP libraries for concurrent API calls:
import aiohttp

image = modal.Image.debian_slim().pip_install("aiohttp")

@app.function(image=image)
async def fetch_url(url: str):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

@app.local_entrypoint()
async def main():
    urls = [
        "https://api.example.com/1",
        "https://api.example.com/2",
        "https://api.example.com/3",
    ]
    
    tasks = [fetch_url.remote.aio(url) for url in urls]
    results = await asyncio.gather(*tasks)
    print(f"Fetched {len(results)} pages")

Async context managers

Modal supports async context managers for resource management:
from contextlib import asynccontextmanager

@app.function()
async def process_with_cleanup():
    @asynccontextmanager
    async def database_connection():
        # Setup
        conn = await connect_to_db()
        try:
            yield conn
        finally:
            # Cleanup
            await conn.close()
    
    async with database_connection() as conn:
        result = await conn.query("SELECT * FROM users")
        return result

Client API

Modal’s Client class supports async operations:
from modal import Client

@app.local_entrypoint()
async def main():
    async with Client.from_env() as client:
        # Use client for low-level operations
        pass

Best practices

Async functions are ideal for I/O-bound tasks like HTTP requests, database queries, and file operations. For CPU-bound tasks, regular synchronous functions often perform better.
When calling Modal functions from async code, use .remote.aio() instead of .remote() to avoid blocking the event loop:
# Good
result = await my_function.remote.aio(arg)

# Bad - blocks event loop
result = my_function.remote(arg)
When using asyncio.gather(), handle exceptions appropriately:
results = await asyncio.gather(
    *tasks,
    return_exceptions=True  # Don't fail on first exception
)

for result in results:
    if isinstance(result, Exception):
        print(f"Task failed: {result}")
For large datasets or real-time processing, use async generators to stream results:
@app.function()
async def stream_data():
    for item in large_dataset:
        await asyncio.sleep(0)  # Yield control
        yield process(item)

Complete async example

Here’s a complete example demonstrating async patterns:
import modal
import asyncio
import aiohttp

app = modal.App("async-web-scraper")

image = modal.Image.debian_slim().pip_install(
    "aiohttp",
    "beautifulsoup4"
)

@app.function(image=image)
async def fetch_and_parse(url: str):
    """Fetch a URL and extract its title."""
    from bs4 import BeautifulSoup
    
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            html = await response.text()
            soup = BeautifulSoup(html, 'html.parser')
            title = soup.find('title')
            return {
                'url': url,
                'title': title.string if title else 'No title',
                'status': response.status
            }

@app.function()
async def process_batch(urls: list[str]):
    """Process multiple URLs concurrently."""
    tasks = [fetch_and_parse.remote.aio(url) for url in urls]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    successful = [r for r in results if not isinstance(r, Exception)]
    failed = [r for r in results if isinstance(r, Exception)]
    
    return {
        'successful': len(successful),
        'failed': len(failed),
        'results': successful
    }

@app.local_entrypoint()
async def main():
    urls = [
        "https://example.com",
        "https://python.org",
        "https://modal.com",
    ]
    
    print("Starting async web scraping...")
    summary = await process_batch.remote.aio(urls)
    
    print(f"\nProcessed {summary['successful']} URLs successfully")
    print(f"Failed: {summary['failed']}")
    
    for result in summary['results']:
        print(f"  {result['url']}: {result['title']}")

Synchronicity library

Modal uses the synchronicity library internally to provide both sync and async APIs from a single implementation. This is why you can call functions with both .remote() and .remote.aio().
The synchronicity library (version ~0.11.1) is listed in Modal’s dependencies and handles the automatic generation of sync wrappers for async code.

Next steps

Basic usage

Review fundamental Modal patterns

Web endpoints

Create async web endpoints

API reference

Explore the complete API reference

Build docs developers (and LLMs) love