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 withblock 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
TrioProgressinstruments 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →