Concurrency Model for Handling Multiple Simultaneous Module Checks in Holehe: Trio Architecture Explained

Holehe employs Trio’s structured concurrency to execute email exposure checks across hundreds of websites simultaneously, using a single Trio nursery to spawn lightweight async coroutines that share one httpx.AsyncClient instance for parallel HTTP requests.

The open-source reconnaissance tool Holehe (megadose/holehe) must query hundreds of platforms concurrently to determine if an email address has been exposed in data breaches. The concurrency model for handling multiple simultaneous module checks in Holehe relies entirely on asynchronous I/O through the Trio framework, eliminating the overhead of traditional thread pools while ensuring robust error isolation per module.

Trio Structured Concurrency Foundation

Unlike tools that rely on threading.Thread or multiprocessing.Pool, Holehe adopts structured concurrency via Trio. This approach treats concurrent task execution as a tree structure where parent tasks (the nursery) manage child lifecycles. If the parent closes, all child coroutines terminate cleanly. This eliminates resource leaks and race conditions common in free-floating thread pools.

In holehe/core.py, the orchestration logic creates a single Trio nursery that acts as the parent context for every website check. This nursery guarantees that no orphaned network requests continue running after the main workflow exits.

Orchestrating Concurrent Checks in holehe/core.py

The maincore() function in holehe/core.py implements the primary event loop. It initializes a shared HTTP client and registers a progress instrument before opening the nursery.


# holehe/core.py – launching all module checks concurrently

async def maincore():
    # ... setup code ...

    client = httpx.AsyncClient(timeout=timeout)          # single async HTTP client

    out = []
    instrument = TrioProgress(len(websites))             # progress bar instrument

    trio.lowlevel.add_instrument(instrument)

    async with trio.open_nursery() as nursery:           # Trio nursery = structured concurrency

        for website in websites:
            nursery.start_soon(launch_module, website, email, client, out)
    trio.lowlevel.remove_instrument(instrument)
    # ... teardown ...

The Nursery Pattern for Task Management

The async with trio.open_nursery() context manager creates a scope where each call to nursery.start_soon(launch_module, ...) schedules a new lightweight coroutine. Trio schedules these tasks on its event loop, switching between them whenever they hit an await point (typically during HTTP I/O). Because these are coroutines rather than OS threads, the memory overhead per task remains minimal, allowing hundreds of simultaneous checks.

Shared Async HTTP Client

Holehe instantiates one httpx.AsyncClient and passes it to every module. This design leverages HTTP/1.1 keep-alive and connection pooling, reducing TCP handshake overhead across the entire scan. Since httpx is Trio-compatible, it yields control back to the event loop during network latency, enabling true parallelism without blocking.

The launch_module Worker Function

Each website module runs inside the launch_module async function, which acts as a wrapper for exception isolation. This prevents a single malformed response or timeout from crashing the entire nursery.


# holehe/core.py – a single module check (run concurrently)

async def launch_module(module, email, client, out):
    try:
        await module(email, client, out)                # each module is an async function

    except Exception:
        # on error, record a failed result without crashing the nursery

        out.append({"module": module.__name__, "exists": False, "error": True})

The module parameter here refers to the imported async function from holehe/modules/. By awaiting the module directly, the event loop can interleave execution between hundreds of pending network requests.

Real-Time Progress with TrioProgress Instruments

To track completion status across concurrent tasks, Holehe implements a custom Trio instrument in holehe/instruments.py. Trio instruments hook into low-level task lifecycle events, providing a cleaner alternative to callbacks or shared counters.


# holehe/instruments.py – Trio instrument that updates a tqdm bar on task exit

class TrioProgress(trio.abc.Instrument):
    def __init__(self, total):
        self.tqdm = tqdm(total=total)

    def task_exited(self, task):
        if task.name.split(".")[-1] == "launch_module":
            self.tqdm.update(1)

The instrument monitors the task_exited event. When a launch_module coroutine finishes—whether successfully or with an exception—the instrument increments the tqdm progress bar. This approach avoids race conditions because Trio invokes the callback synchronously with respect to the event loop.

Module Implementation Pattern

Every checker module in holehe/modules/ defines an async function matching the signature async def module_name(email, client, out). These functions must be awaitable to integrate with the launch_module dispatcher. They perform HTTP requests via the injected client and append results to the shared out list, which is thread-safe here because Trio’s single-threaded event loop eliminates concurrent mutation risks.

Summary

  • Trio nursery in holehe/core.py provides structured concurrency for spawning hundreds of module checks without thread overhead.
  • Single httpx.AsyncClient enables connection reuse and shared configuration across all simultaneous HTTP requests.
  • launch_module wrapper isolates exceptions per website, ensuring one failed check does not abort the entire scan.
  • TrioProgress instrument hooks into Trio's task lifecycle to update the progress bar atomically as each coroutine exits.
  • Pure async I/O replaces thread pools; concurrency limits are dictated by system file descriptors and HTTP timeouts, not artificial pool sizes.

Frequently Asked Questions

Does Holehe use multithreading or multiprocessing for concurrent checks?

No. According to the holehe/core.py source, Holehe relies purely on asynchronous I/O via Trio. It does not spawn Python threads or processes; instead, it schedules lightweight coroutines on a single event loop, allowing for massive parallelism without GIL contention or memory-heavy thread stacks.

How does Holehe prevent one module failure from crashing the entire scan?

The launch_module function wraps each module execution in a try-except block. When a module raises an exception, the error is caught and recorded in the output list without propagating to the Trio nursery. This ensures that other launch_module tasks running concurrently inside the nursery continue executing uninterrupted.

Why does Holehe use Trio instead of Python’s standard asyncio?

Trio provides structured concurrency through its nursery concept, which guarantees that all spawned tasks complete before the nursery scope exits. Holehe leverages this for reliable cleanup and utilizes Trio's instrument API (see holehe/instruments.py) to monitor task completion, which offers cleaner hooks into task lifecycles than asyncio's lower-level protocols.

What limits the number of simultaneous checks in Holehe?

Concurrency is constrained primarily by the operating system's file descriptor limits and the HTTP client timeout configuration, not by an artificial thread pool size. The Trio event loop manages all pending I/O operations, while the shared httpx.AsyncClient handles DNS resolution caching and connection pooling, typically capping parallelism at a few hundred concurrent streams depending on network conditions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →