Asynchrony: async/await, asyncio

The ZEUS backend is asynchronous from HTTP all the way down to the database and the ARQ queue. Asynchrony gives huge I/O throughput, but only when you don't block the event loop. This lesson is a set of rules we follow in practice.

What async/await does

async def creates a coroutine. await hands control back to the event loop for the duration of an I/O operation (network, database, disk) so it can serve other requests.

async def fetch_forest(forest: str) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"/api/forest/{forest}")
        return resp.json()

The most common mistake: blocking the loop

Calling a synchronous, slow operation inside a coroutine blocks the whole server — every other request waits.

# bad — blocks the event loop
async def bad():
    time.sleep(5)                 # stops the entire event loop
    data = requests.get(url)      # synchronous HTTP — also blocks

# good
async def good():
    await asyncio.sleep(5)
    async with httpx.AsyncClient() as c:
        data = await c.get(url)

ProfessNet standard: in async code we use async libraries (httpx, asyncpg/SQLAlchemy async, redis.asyncio). We don't call synchronous libraries directly.

Synchronous code in an async world

When you have to use a blocking library (e.g. CPU-bound parsing or an old SDK client), push it onto a thread pool:

import asyncio

result = await asyncio.to_thread(blocking_parse, raw_bytes)

Parallelism: asyncio.gather

We run several independent I/O operations concurrently, not sequentially.

# sequentially — slow (sum of the times)
a = await fetch_forest("corp")
b = await fetch_forest("dmz")

# concurrently — fast (time of the slowest one)
a, b = await asyncio.gather(
    fetch_forest("corp"),
    fetch_forest("dmz"),
)

Concurrency limit

With many probes we don't fire a thousand tasks at once — we cap it with a semaphore, so we don't saturate the network or the target server.

sem = asyncio.Semaphore(10)


async def guarded(forest: str) -> dict:
    async with sem:
        return await fetch_forest(forest)


results = await asyncio.gather(*(guarded(f) for f in forests))

Timeouts and cancellation

Every external call has a timeout. Without it, a hung peer hangs forever.

async with asyncio.timeout(30):
    data = await collect_inventory(forest)
PatternWhat for
awaita single I/O operation
asyncio.gathermany operations concurrently
Semaphoreconcurrency limit
asyncio.to_threadpushing blocking code off the loop
asyncio.timeouta hard time limit

Tip: long-running jobs (scans of a whole forest) don't live in an HTTP handler — they go to ARQ. The endpoint returns 202 and a job_id, and the worker does the rest.


The golden rule: in a coroutine you either await something asynchronous, or you push the blocking part to to_thread. Anything else stops the server. Stick to that, and async will give you performance without surprises.