Introduction to asyncio.TaskGroup for Python scanning
This article targets Python developers and security engineers building concurrent network and port scanners using asyncio.TaskGroup. We focus on Python 3.11 and later patterns for cancellable, robust scanning tasks that integrate with SOC automation and toolchains.
Expect concrete code patterns for batching, rate limiting, exception group handling, graceful shutdown, and resource control. Examples are practical and intended for defensive automation and monitoring workflows.
Why choose asyncio.TaskGroup for network scanning
asyncio.TaskGroup provides structured concurrency, letting you start many tasks but handle cancellation and exceptions as a single unit. This is essential for scanners that must abort quickly on operator signal or cascade failures.
Compared with manually managing task lists and wait calls, TaskGroup reduces race conditions and simplifies cleanup. That leads to safer long running scanners that play well with async I O libraries and connection pools.
Basic cancellable scanner pattern
Start with a cancellable worker pattern using TaskGroup, where each worker attempts a connect or probe and returns status. Use asyncio.open_connection or asyncio streams for TCP probes, or integrate with asyncio sockets for raw probes.
Example pattern for simple port probe using TaskGroup and cancellation.
import asyncio
async def probe(host, port, timeout=2):
try:
reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout)
writer.close()
await writer.wait_closed()
return True
except Exception:
return False
async def scan_targets(targets):
async with asyncio.TaskGroup() as tg:
for host, port in targets:
tg.create_task(probe(host, port))
Batching and rate limiting patterns
Large IP ranges need batching and rate limiting to avoid saturating networks or triggering throttles. Implement batching by splitting the target list and awaiting each batch sequentially while using TaskGroup for concurrency inside each batch.
For rate control use asyncio.Semaphore to bound concurrent connections and an optional sleep between batches. This keeps resource use predictable and simplifies integration with upstream rate rules.
- Use batch sizes that match your network and pool limits
- Use semaphores to limit concurrent sockets and avoid overload
Handling exception groups and partial failures
TaskGroup raises an exception group when multiple tasks fail. Inspect the ExceptionGroup to decide if failures are transient or fatal. Use grouping to retry specific failures without restarting the entire scan.
Structured handling example: catch ExceptionGroup, iterate exceptions, log by type, and schedule targeted retries for recoverable errors. This gives granular resilience while keeping the main runner simple.

Graceful shutdown and signal handling
Integrate signal handlers to cancel the outer TaskGroup on SIGINT or SIGTERM. Cancel the runner TaskGroup to propagate cancellation to all active probes, let tasks perform cleanup, then await shutdown of connection pools.
Use asyncio.create_task for the main runner and register loop.add_signal_handler on supported platforms, or rely on asyncio.run which handles cancellation propagation on exit. Ensure file descriptors and writers are closed in finally blocks.
Resource control and connection pooling
Connection pools and semaphores limit per host and global concurrency. For HTTP scanning use async HTTP clients with connection pool controls. For raw TCP, maintain a controlled number of concurrent sockets to avoid ephemeral port exhaustion.
Keep a small set of shared resources, for example a single session object from an async library and a semaphore to gate access. This reduces overhead and avoids per probe expense that hurts throughput.
- Pool size tuned to CPU and network latency
- Semaphore limits for global and per host concurrency
Integrating with async I O libraries
Common integrations include httpx AsyncClient for HTTP probes and aiomultiprocess for CPU bound tasks. Use TaskGroup to wrap calls to these libraries and ensure cancellation reaches into their sessions and connectors.
When using third party async libraries make sure to close clients in finally blocks. For example, call await client.aclose inside the TaskGroup cancellation flow to avoid leaked sockets.
Performance tuning and benchmarking
Measure latency per probe, max concurrent sockets, and throughput in targets per second. Use synthetic ranges and real targets to tune batch sizes, timeouts, and retry logic. Profiling will expose where await points block the event loop unexpectedly.
Keep benchmarking simple: vary TaskGroup concurrency and semaphore limits, capture CPU, socket, and system call metrics, then pick parameters that maximize throughput without error growth. Automate runs and compare percentiles not just averages.
FAQ
Q1: Does asyncio.TaskGroup replace asyncio.gather A1: TaskGroup provides structured concurrency and better cancellation semantics than gather. Use TaskGroup when you need clear cancellation and exception aggregation control. It is not a direct performance feature, it is a correctness and lifecycle tool.
Q2: How do I limit per host concurrency A2: Use a mapping of host keyed semaphores. Acquire the host semaphore before probing and release after. Combine with a global semaphore to control total load. This avoids overwhelming a single target.
Conclusion
Using asyncio.TaskGroup for concurrent network scanning in Python yields safer, more maintainable scanners. TaskGroup simplifies cancellation, groups exceptions, and composes neatly with semaphores and pooled async clients. With batching, rate limiting, and careful resource control you can scale scanning tasks for SOC automation while avoiding overload and noisy failures.
Apply the patterns shown here: wrap workers in TaskGroup, gate concurrency with semaphores, handle ExceptionGroup for targeted retries, and integrate clean shutdown paths tied to signals. Benchmarking and gradual tuning are essential, start with conservative settings and iterate based on telemetry. These practices deliver deterministic scanner behavior that fits enterprise monitoring, alerting, and remediation pipelines.
