# How Holehe Tracks Progress with tqdm: Async Instrumentation Explained

> Learn how Holehe uses Trio Instruments and tqdm for async progress tracking. Discover the async instrumentation technique explained with the megadose/holehe repository.

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

---

**Holehe tracks scan progress by attaching a custom Trio Instrument (`TrioProgress`) to the event loop, which calls `tqdm.update(1)` every time a website-checking coroutine finishes.**

Holehe is an open-source OSINT tool that checks email addresses against 120+ websites simultaneously. Because it runs all checks asynchronously using the Trio concurrency library, the developers needed a way to show users which sites have been processed without blocking the main execution. The solution pairs Trio's **Instrument API** with the **tqdm** progress bar library for lightweight, non-intrusive feedback.

## How tqdm Progress Tracking Works in Holehe

The progress system consists of two coordinated parts: the `TrioProgress` instrument that detects task completion, and the main loop in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) that registers it.

### The TrioProgress Instrument ([`holehe/instruments.py`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py))

The core logic lives in [`holehe/instruments.py`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py), where a small class inherits from `trio.abc.Instrument`:

```python
class TrioProgress(trio.abc.Instrument):
    def __init__(self, total):
        self.tqdm = tqdm(total=total)          # initialise tqdm with the number of sites

    
    def task_exited(self, task):
        # Trio names every started coroutine; the ones we care about end with

        # "launch_module". When such a task exits we advance the bar.

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

```

Three key design decisions make this work:

- **Constructor receives `total`**: The count of websites to check, passed from `len(websites)` in the caller
- **`task_exited` hook**: Trio's built-in callback that fires automatically when any task ends—no manual instrumentation needed in the coroutines themselves
- **Name filtering**: Only tasks ending with `"launch_module"` trigger updates, ignoring internal Trio tasks like the nursery manager

### Integration in the Main Scan Loop ([`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py))

The instrument is wired into the execution flow in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py):

```python
instrument = TrioProgress(len(websites))
trio.lowlevel.add_instrument(instrument)          # attach the instrument

async with trio.open_nursery() as nursery:
    for website in websites:
        nursery.start_soon(launch_module, website, email, client, out)
trio.lowlevel.remove_instrument(instrument)       # detach after all tasks finish

```

The sequence matters: register before opening the nursery, then clean up after all tasks complete. This ensures every `launch_module` task that starts also triggers the exit hook.

## Why This Architecture Works

**tqdm** operates independently of Trio. It's thread-safe and accepts manual `update()` calls, so it doesn't care whether the caller is synchronous or async.

**Trio's Instrument API** provides lifecycle hooks without code modification. Alternative approaches would require:

- **Manual progress calls inside each module** — breaks separation of concerns and risks missed updates on exceptions
- **Periodic polling of task status** — adds overhead and complicates cancellation handling
- **Wrapping coroutines with decorators** — requires metaprogramming that obscures stack traces

The chosen design keeps module code clean while guaranteeing accurate progress tracking even when individual site checks fail or timeout.

## Running Holehe with Progress Output

From the command line, the tqdm bar appears automatically:

```bash
$ holehe user@example.com
[##########################] 120/120

```

The counter increments live as each asynchronous check returns, regardless of whether that site reported account found, not found, or rate-limited. Users see steady feedback without Terminal flicker or log spam.

## Embedding Holehe's Progress Logic in Custom Code

You can reuse the same instrumentation pattern in your own Trio-based scripts:

```python
import trio
from holehe.core import import_submodules, get_functions, launch_module
from holehe.instruments import TrioProgress
import httpx

async def run_scan(email):
    modules = import_submodules("holehe.modules")
    websites = get_functions(modules)
    client = httpx.AsyncClient()
    out = []
    prog = TrioProgress(len(websites))
    trio.lowlevel.add_instrument(prog)

    async with trio.open_nursery() as nursery:
        for site in websites:
            nursery.start_soon(launch_module, site, email, client, out)

    trio.lowlevel.remove_instrument(prog)
    await client.aclose()
    return out

trio.run(run_scan, "user@example.com")

```

The `TrioProgress` class requires no modification—it automatically detects any `launch_module` task in the current Trio event loop. This makes it portable across different Holehe-based workflows.

## Summary

- **`TrioProgress` in [`holehe/instruments.py`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py)** — Custom `trio.abc.Instrument` subclass that wraps tqdm and increments on each `launch_module` task exit
- **`task_exited` filtering** — Uses task name suffix matching to ignore irrelevant Trio internals
- **Registration in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py)** — Instrument attached via `trio.lowlevel.add_instrument()` before the nursery opens, removed after completion
- **Non-blocking feedback** — tqdm updates without awaiting, preserving full async throughput
- **Reusable design** — Same pattern works in standalone scripts importing Holehe internals

## Frequently Asked Questions

### Does Holehe's tqdm progress bar work on Windows terminals?

Yes. tqdm detects the terminal capabilities automatically. On Windows, it falls back from ANSI escape codes to the Windows console API. The `tqdm` dependency declared in [`setup.py`](https://github.com/megadose/holehe/blob/main/setup.py) handles cross-platform rendering without code changes in Holehe.

### What happens if a site check hangs—does the progress bar stall?

The bar only advances when `task_exited` fires, so hung tasks don't increment the counter. However, Trio nurseries typically apply timeouts via `trio.move_on_after()` or `async_timeout`. When a timeout cancels a `launch_module` task, the `task_exited` hook still executes, ensuring the bar reaches the total even with failures.

### Can I disable the progress bar in Holehe?

The command-line interface doesn't expose a `--no-progress` flag in the current release. To suppress output programmatically, instantiate `TrioProgress` with `disable=True` passed to tqdm: modify `self.tqdm = tqdm(total=total, disable=True)` in [`holehe/instruments.py`](https://github.com/megadose/holehe/blob/main/holehe/instruments.py) or subclass the instrument in your own code.

### Why does Holehe use Trio's low-level Instrument API instead of higher-level patterns?

Alternative approaches like `anyio` adapters or manual `trio.Event` signaling would require either an abstraction layer that reduces performance visibility, or invasive changes to every module. The Instrument API provides **zero-cost** observation—if no instrument is registered, the runtime skips all hook overhead entirely. This preserves Holehe's efficiency for headless automation use cases that don't need progress display.