Skip to the content.

ADR-0007: Async Client

Decision: AsyncBaseClient on httpx, not a wrapper around the sync client.

Why a separate implementation: Wrapping sync calls with asyncio.run_in_executor would work but defeats the purpose — you’d be running sync urllib3 on a thread pool rather than truly async I/O. The platforms that benefit most from async (ThreatQ, CrowdStrike) support proper HTTP/2 keep-alive which httpx uses natively.

authenticate() is async: Means token refresh can be non-blocking. Proofpoint and Netskope auth is header injection (synchronous in effect) but declared async for interface consistency.

**translation methods stay synchronous:** to_stix() and from_stix()` are CPU-bound JSON manipulation — there is no benefit to making them async, and it would complicate callers.

Concurrent multi-platform queries:

async with AsyncGNATClient() as tq, AsyncGNATClient() as rf:
    await asyncio.gather(tq.connect("threatq"), rf.connect("recordedfuture"))
    tq_res, rf_res = await asyncio.gather(
        tq.client.get_object("indicator", ioc_id),
        rf.client.get_object("indicator", ioc_id),
    )

This is the primary reason to use the async client — fan-out enrichment across 5 platforms takes the same wall-clock time as the slowest single platform.


Licensed under the Apache License, Version 2.0