How Holehe Uses Trio and httpx for Asynchronous, Concurrent Module Checking: A Deep Dive

Holehe achieves fast, concurrent account enumeration by running every platform checker as an async coroutine under Trio's nursery scheduler, sharing a single httpx.AsyncClient for connection pooling across all requests.

Holehe is an open-source OSINT tool that checks whether an email address is registered on hundreds of websites. Its speed comes from a carefully designed asynchronous architecture built on Python's trio runtime and httpx library. The source code in megadose/holehe demonstrates production-grade patterns for concurrent HTTP scraping without thread overhead.

Entry Point: Bootstrapping the Async Runtime

The async lifecycle begins in holehe/core.py. The synchronous main() function hands control to Trio's event loop:


# holehe/core.py (lines 33-34)

def main():
    trio.run(maincore)

maincore() is the root async function. It bridges CLI arguments to the concurrent execution engine, handling everything from timeout configuration to progress visualization.

Creating a Shared HTTP Client with httpx

Before spawning any workers, maincore() instantiates a single shared async HTTP client:


# holehe/core.py (lines 13-14)

client = httpx.AsyncClient(timeout=args.timeout)

Using one AsyncClient instance is critical. httpx automatically pools connections and reuses TCP sessions via HTTP keep-alive, eliminating the overhead of establishing hundreds of separate SSL handshakes.

Real-Time Progress with a Custom Trio Instrument

Holehe integrates tqdm progress bars through Trio's instrumentation API. The custom TrioProgress class hooks into task lifecycle events:


# holehe/instruments.py (lines 4-10)

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 is registered before work begins:


# holehe/core.py

instrument = TrioProgress(len(websites))
trio.lowlevel.add_instrument(instrument)

This design decouples progress tracking from business logic. The progress bar advances automatically whenever any launch_module task completes—no manual counter increments scattered through module code.

Concurrent Module Execution with Trio Nurseries

The core concurrency primitive is Trio's nursery, which acts as a task supervisor:


# holehe/core.py (lines 18-21)

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

Key characteristics of this pattern:

  • Structured concurrency: The nursery async with block only exits after every spawned task completes or fails
  • No manual task tracking: Trio handles cancellation, exception propagation, and cleanup
  • True parallelism: While Python code runs single-threaded, all HTTP I/O overlaps—when one module awaits a response, others execute

Module Implementation Pattern

Every platform checker follows a standard coroutine signature. Here's the Twitter module:


# holehe/modules/social_media/twitter.py

async def twitter(email: str, client: httpx.AsyncClient, out: list):
    resp = await client.get(
        f"https://api.twitter.com/users/by?usernames={email}",
        headers={"Authorization": "Bearer …"}
    )
    exists = resp.status_code == 200
    out.append({"name": "twitter", "exists": exists, ...})

Modules never create their own HTTP clients. They receive the shared client parameter, ensuring connection reuse across all 200+ platforms. The out list is thread-safe for appends because Trio's scheduling guarantees only one coroutine runs Python bytecode at any instant.

Error Isolation and Graceful Degradation

launch_module() wraps each module call in exception handling:


# holehe/core.py (lines 66-78)

async def launch_module(module, email, client, out):
    try:
        await module(email, client, out)
    except Exception:
        # Log error without crashing other modules

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

This per-module fault isolation prevents a single broken website or network timeout from aborting the entire scan.

Cleanup and Result Aggregation

After the nursery closes, maincore() performs graceful shutdown:


# holehe/core.py (lines 24-31)

trio.lowlevel.remove_instrument(instrument)
await client.aclose()

# Sort, print, and optionally export to CSV

Closing the AsyncClient explicitly releases connection pool resources. Results are then sorted by platform name for consistent CLI output.

Performance Characteristics

Aspect Implementation Benefit
Connection reuse Single AsyncClient pools HTTP/2 and keep-alive connections
Memory efficiency Coroutines use ~1KB vs. ~8MB per thread
Scalability Hundreds of concurrent checks with no thread switch overhead
Responsiveness Trio nursery + instrumentation yields smooth progress updates

Running Holehe with Custom Async Parameters

From the command line, users control timeout behavior that feeds directly into the httpx.AsyncClient:

holehe user@example.com --timeout 15 --no-color

A shorter timeout reduces total scan duration at the risk of false negatives on slow platforms.

Summary

  • Trio provides structured concurrency through nurseries, eliminating callback complexity and ensuring clean task lifecycles
  • httpx.AsyncClient delivers efficient HTTP with automatic connection pooling when shared across coroutines
  • TrioProgress instruments the runtime for real-time feedback without polluting module code
  • Per-module error handling keeps the full scan resilient against individual platform failures
  • Single-client architecture maximizes connection reuse for 200+ concurrent website checks

Frequently Asked Questions

Why does Holehe use trio instead of asyncio?

Trio's nursery model enforces structured concurrency—tasks cannot outlive their parent context, preventing resource leaks. The instrumentation API also enables clean progress bar integration that would require more boilerplate in asyncio. According to the holehe source code, the maintainers prioritized these safety guarantees for a tool handling hundreds of external network calls.

Can I adjust the concurrency level in Holehe?

Holehe does not expose a semaphore or concurrency limit flag. Trio schedules all module coroutines simultaneously, relying on httpx's internal connection pool limits (default 100) to throttle outbound requests. For rate-limited environments, you would need to modify holehe/core.py to add trio.CapacityLimiter around nursery.start_soon().

What happens if one module hangs indefinitely?

The timeout parameter passed to httpx.AsyncClient applies per request. If a module's overall logic hangs beyond individual HTTP timeouts, Trio's cancellation system can abort it—though Holehe's current implementation lets exceptions propagate to launch_module()'s error handler rather than implementing explicit task timeouts.

How does connection pooling work across different domains?

httpx.AsyncClient maintains separate connection pools per (host, port, scheme) tuple. When Holehe checks 200 different websites, the client reuses connections aggressively for repeated hits to the same platform (e.g., multiple Google services) while establishing new connections for unique domains—all without explicit management in module code.

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 →