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)
| Pattern | What for |
|---|---|
await | a single I/O operation |
asyncio.gather | many operations concurrently |
Semaphore | concurrency limit |
asyncio.to_thread | pushing blocking code off the loop |
asyncio.timeout | a 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
202and ajob_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.