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

> Discover Holehe's concurrency model using Trio for simultaneous email exposure checks. Learn how Trio's structured concurrency efficiently manages parallel HTTP requests.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: architecture
- Published: 2026-08-25

---

**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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/holehe/core.py)

The `maincore()` function in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) implements the primary event loop. It initializes a shared HTTP client and registers a progress instrument before opening the nursery.

```python

# 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.

```python

# 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`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py). Trio instruments hook into low-level task lifecycle events, providing a cleaner alternative to callbacks or shared counters.

```python

# 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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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.