Skip to content

Async execution model

Cortex nodes are asyncio-native: one event loop per process drives all publishers, subscribers, and timers for that node. On Linux/macOS, cortex.run uses uvloop when available.

Node task graph

flowchart TB
    Loop(((asyncio event loop)))
    Loop --> T1[Timer 1<br/>RateExecutor]
    Loop --> T2[Timer 2<br/>RateExecutor]
    Loop --> S1[Subscriber 1<br/>AsyncExecutor]
    Loop --> S2[Subscriber 2<br/>AsyncExecutor]

Node.run() spawns one task per timer (RateExecutor) and one per callback-bearing subscriber (AsyncExecutor), then asyncio.gathers them until cancelled.

RateExecutor cadence

sequenceDiagram
    participant L as Event loop
    participant R as RateExecutor
    participant CB as callback

    loop while running
        L->>R: resume
        R->>R: now = perf_counter()
        alt now >= next_exec_time
            R->>CB: await callback()
            R->>R: next_exec_time += interval
        end
        R->>L: sleep(max(0, next_exec_time - now))
    end

next_exec_time is set once at start and advances by exactly one interval per callback fire. If a callback overruns, the next sleep is zero-length until the clock catches up — ticks are never skipped, they fire back-to-back until aligned with the grid.

AsyncExecutor receive loop

sequenceDiagram
    participant L as Event loop
    participant A as AsyncExecutor
    participant S as SUB socket
    participant CB as user callback

    loop while running
        L->>A: resume
        A->>S: await recv_multipart(copy=False)
        S-->>A: frames
        A->>A: decode message
        A->>CB: await callback(msg, header)
        A->>L: sleep(0)  (yield)
    end

Head-of-line blocking

A slow callback stalls the receive loop. Messages pile up on the SUB HWM and get evicted. Offload variable-latency work to asyncio.create_task(...) or a thread pool.

Publish is sync-inside-async

Publisher holds a plain zmq.Context (shadowed onto the node's async context). publish() is a regular function call — no await — to skip the async zmq send overhead.

Not thread-safe

zmq.PUB sockets are not safe to call from multiple threads or tasks concurrently. Serialize calls to publish() per publisher.

uvloop

On Unix, cortex.run uses uvloop if it's installed. Lower p99 latency on high-rate small messages.

See also