Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a known set of related agent calls, use asyncio.TaskGroup when they should share a lifetime and a failure boundary; use asyncio.gather() when you want results in input order and will manage errors yourself. Add a semaphore or fixed worker pool to limit concurrency, use as_completed() to handle early results, apply timeouts as cancellation, and move blocking synchronous I/O off the event loop with asyncio.to_thread().

The examples below target Python 3.11 or later unless noted. These are practical ways to combine documented asyncio APIs, not a formal Python taxonomy.

1. Use TaskGroup for related agent calls

A task group is a good default when several calls belong to one operation—for example, gathering independent research from multiple agents before producing a response. The async with block owns the child tasks: it waits for them before exiting. If a child raises an exception, the group cancels its remaining tasks and reports failures as an exception group.

import asyncio

async def ask_agent(name, prompt):
    return await call_agent(name, prompt)

async def run_agents(prompt):
    async with asyncio.TaskGroup() as tg:
        research = tg.create_task(ask_agent("research", prompt))
        critique = tg.create_task(ask_agent("critique", prompt))

    return research.result(), critique.result()

Read task results after the context exits, when the group has finished. Task groups were added in Python 3.11. Their explicit ownership is useful for nested work because child failures do not leave sibling tasks running in the background. See the Python documentation for coroutines and tasks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
Elebase USB to USB C Adapter for iPhone 18 Pro Max,USBC Car Charger Adapter
  • Read Before You Buy — No Video Output: These adapters support charging and USB 2.0 data transfer, but cannot transmit video signals. Except for standard USB webcams (which use USB data only), they are not compatible with HDMI/DisplayPort cables, video-capable USB-C hubs, or docking stations with video output.
  • Convert USB-A Ports to USB-C: Designed to connect USB-C earphones, cables, flash drives, card readers, and other USB-C accessories to standard USB-A ports. Plug-and-play with no drivers or software required.
  • Aluminum Alloy Housing: Built with a sturdy aluminum alloy shell that aids in heat dissipation and protects against daily wear and scratches. Designed to maintain a stable and secure connection.
  • Compact & Travel-Friendly: The ultra-compact design allows the adapter to stay plugged into your device without blocking adjacent ports or adding bulk, reducing wear and tear on your original USB ports.
  • 12-Month Warranty: Backed by a 12-month manufacturer warranty for peace of mind. Designed to meet strict quality control standards for reliable everyday performance.

2. Use gather for a compact fan-out with ordered results

asyncio.gather() is concise when you have a known collection of awaitables and want successful results in the same order as the inputs, regardless of which call finishes first.

async def run_agents(prompts):
    return await asyncio.gather(
        *(ask_agent(f"agent-{i}", prompt)
          for i, prompt in enumerate(prompts))
    )

Its default failure behavior differs from a task group: the first exception is propagated to the waiter, but other submitted awaitables continue running. A child failure therefore does not automatically stop its peers. Choose gather() when that behavior fits your error policy; cancelling the gather operation itself cancels unfinished submitted awaitables. For related work that should stop together on failure, use TaskGroup instead. These semantics are documented in the Python task reference.

3. Use a semaphore to cap access to a resource

When an external service or shared resource should only handle a limited number of in-flight calls, put an asyncio.Semaphore around the protected operation. This limits concurrent entries into that section, not the number of tasks you create overall.

Rank #2
Sale
Anker USB-C Hub, 5-in-1 USB Hub for Laptops, 4K HDMI Multiport Adapter
  • 5-in-1 USB-C Hub: Experience comprehensive connectivity featuring a Power Delivery input, two USB-A 2.0 ports, a USB-A 3.0 port, and an HDMI port. (Note: The USB-C power delivery input port is only for connecting an external wall charger to power your laptop and cannot power peripheral devices.)
  • 90W Pass-Through Charging: Achieve optimal charging with 90W pass-through power to your laptop, supported by a total input of 100W, with the hub reserving 10W for operational efficiency. (Note: Wall charger not included.)
  • Quick Data Transfers: Accelerate your productivity with rapid data transfers using a high-speed 5Gbps USB 3.0 port and two 480Mbps USB 2.0 ports.
  • 4K HDMI Display: Enhance your visual experience with a hub capable of delivering 4K resolution at 30Hz in both mirror and extend modes. Please note that this hub is compatible with MacBook (macOS 12 and newer), Windows 10 and 11, ChromeOS, and laptops equipped with DP Alt Mode and Power Delivery. Note: This device is not compatible with Linux.
  • What You Get: Anker USB-C Hub (5-in-1, 4K HDMI), welcome guide, 18-month warranty, and our friendly customer service.
async def run_agents(prompts, limit=5):
    semaphore = asyncio.Semaphore(limit)

    async def limited_call(prompt):
        async with semaphore:
            return await ask_agent("worker", prompt)

    return await asyncio.gather(
        *(limited_call(prompt) for prompt in prompts)
    )

Use a positive limit appropriate to the resource. A semaphore is a concurrency gate, not a rate-per-time-window limiter and not a worker lifecycle: tasks waiting to acquire it still exist. The asyncio API index lists both Semaphore and BoundedSemaphore among its synchronization primitives; check the reference for the Python version you deploy. See the asyncio high-level API index.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

4. Use a queue and fixed workers for incremental workloads

If jobs arrive over time, or the input is too large to create a task for every item at once, use an asyncio.Queue with a fixed number of worker tasks. A bounded queue applies backpressure: when it fills, producers wait until workers make room. Python’s asyncio API index describes queues as a way to distribute work among tasks, as well as for connection pools and pub/sub patterns.

import asyncio

async def worker(queue):
    while True:
        job = await queue.get()
        try:
            if job is None:  # shutdown signal
                return
            await process_agent_job(job)
        finally:
            queue.task_done()

async def process_jobs(jobs, worker_count=5):
    queue = asyncio.Queue(maxsize=worker_count * 2)

    async with asyncio.TaskGroup() as tg:
        for _ in range(worker_count):
            tg.create_task(worker(queue))

        for job in jobs:
            await queue.put(job)

        await queue.join()

        # One sentinel per worker, so each can exit cleanly.
        for _ in range(worker_count):
            await queue.put(None)

        await queue.join()

Each successful get() must be paired with task_done(), including for shutdown signals; join() waits until all queued items have been marked done. The TaskGroup keeps workers owned by the operation, and sentinels give them a normal exit after jobs drain. If a worker fails, the group cancels its peers and the failure surfaces from the group. Queue behavior and lifecycle details are in the asyncio API index.

Rank #3
Sale
Anker USB C Hub, 7in1 Multi-Port USB Adapter, 4K@60Hz USBC to HDMI Splitter
  • Sleek 7-in-1 USB-C Hub: Features an HDMI port, two USB-A 3.0 ports, and a USB-C data port, each providing 5Gbps transfer speeds. It also includes a USB-C PD input port for charging up to 100W and dual SD and TF card slots, all in a compact design.
  • Flawless 4K@60Hz Video with HDMI: Delivers exceptional clarity and smoothness with its 4K@60Hz HDMI port, making it ideal for high-definition presentations and entertainment. (Note: Only the HDMI port supports video projection; the USB-C port is for data transfer only.)
  • Double Up on Efficiency: The two USB-A 3.0 ports and a USB-C port support a fast 5Gbps data rate, significantly boosting your transfer speeds and improving productivity.
  • Fast and Reliable 85W Charging: Offers high-capacity, speedy charging for laptops up to 85W, so you spend less time tethered to an outlet and more time being productive.
  • What You Get: Anker USB-C Hub (7-in-1), welcome guide, 18-month warranty, and our friendly customer service.

5. Use as_completed when early results matter

asyncio.as_completed() lets you process each result as soon as its awaitable finishes. This is useful when agents have uneven response times and you can act on early answers without waiting for the slowest call.

async def collect_early(prompts):
    tasks = [asyncio.create_task(ask_agent(f"agent-{i}", p))
             for i, p in enumerate(prompts)]
    try:
        for completed in asyncio.as_completed(tasks):
            result = await completed
            handle_result(result)
    finally:
        for task in tasks:
            if not task.done():
                task.cancel()
        await asyncio.gather(*tasks, return_exceptions=True)

The cleanup here is deliberate: ending iteration early does not itself cancel the remaining work. Cancel unfinished tasks and await them if the caller no longer needs their results. In Python 3.13 and later, the object returned by as_completed() supports both asynchronous iteration and ordinary iteration; the example’s ordinary iteration form also works on earlier versions. Consult the Python task reference for version-specific details.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

6. Set a deadline and preserve cancellation

Use asyncio.timeout(seconds) to give a whole operation a deadline. If it expires, asyncio cancels the current task inside the context and raises TimeoutError outside it.

Rank #4
Sale
UGREEN USB to USB C Adapter Combo 4-Pack, 10Gbps USB C Converter Space Gray
  • Dual Converters, Infinite Potential:Includes 2× USB C male to USB A female adapters and 2× USB A male to USB C female adapters. Perfect for a wide range of uses—tablets with Bluetooth keyboards, expand USB ports on macbook, and more. Two different converters for all your daily needs
  • Next-Level 10Gbps & 3A Charging: No more slow 480Mbps, this usb to usb c adapter has a transfer speed of up to 10Gbps, allowing you to do more transferring in less time. This usb adapter fits both USB A and USB C charger, supporting up to 3A fast charging
  • Upgraded Exquisite Craftsmanship: With an aluminum alloy housing and metal connector, the usbc to usb adapter is extremely durable and sturdy. Rigorously tested to withstand more than 10,000 times of plugging and unplugging, ensuring long-lasting performance
  • Broad Compatible: The usb c to usb adapter widely supports all USB C/ USB A devices like laptops, tablets, cellphones, car chargers, and phone chargers. Such as compatible with MacBook Pro/Air 2023/2022, Thunderbolt 4/3 Devices,Apple MagSafe Watch 9/8/7/SE/Ultra, iPad Pro 2022/2021, Samsung Galaxy S23/S20/S10, and iPhone 17/16/15 Pro. Plug and play
  • Please Note: To reach 10Gbps speed, keep the cable under 3.3 ft. For USB A Male to USB C adapters, try flipping the USB C connector. USB C Male to USB A adapters support bidirectional 10Gbps transfer within 3.3 ft
async def run_with_deadline(prompts, seconds=30):
    try:
        async with asyncio.timeout(seconds):
            return await asyncio.gather(
                *(ask_agent(f"agent-{i}", p)
                  for i, p in enumerate(prompts))
            )
    except TimeoutError:
        return None

This example makes timeout handling explicit, but returning None is only suitable if the caller treats it as a meaningful outcome; otherwise let the exception propagate or translate it into an application-specific error. The timeout context manager was added in Python 3.11. asyncio.wait_for() is a related alternative for a deadline on one awaitable; on timeout it cancels that awaitable.

Coroutines that need cleanup should use try/finally and generally allow CancelledError to propagate after cleanup. Suppressing cancellation can interfere with timeout and TaskGroup behavior.

async def call_with_cleanup():
    resource = await acquire_resource()
    try:
        return await do_agent_work(resource)
    finally:
        await resource.close()

Timeout and cancellation behavior are described in the Python task reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
Sale
Anker USB C Hub, 5-in-1 USBC to HDMI Splitter with 4K Display
  • 5-in-1 Connectivity: Equipped with a 4K HDMI port, a 5 Gbps USB-C data port, two 5 Gbps USB-A ports, and a USB C 100W PD-IN port. Note: The USB C 100W PD-IN port supports only charging and does not support data transfer devices such as headphones or speakers.
  • Powerful Pass-Through Charging: Supports up to 85W pass-through charging so you can power up your laptop while you use the hub. Note: Pass-through charging requires a charger (not included). Note: To achieve full power for iPad, we recommend using a 45W wall charger.
  • Transfer Files in Seconds: Move files to and from your laptop at speeds of up to 5 Gbps via the USB-C and USB-A data ports. Note: The USB C 5Gbps Data port does not support video output.
  • HD Display: Connect to the HDMI port to stream or mirror content to an external monitor in resolutions of up to 4K@30Hz. Note: The USB-C ports do not support video output.
  • What You Get: Anker 332 USB-C Hub (5-in-1), welcome guide, our worry-free 18-month warranty, and friendly customer service.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

7. Move blocking synchronous I/O off the event loop

An async function still blocks the event loop if it calls a synchronous function that waits on network, disk, or another I/O operation. Use asyncio.to_thread() to run that blocking call in a thread so other asyncio tasks can continue while it waits.

def legacy_agent_call(prompt):
    return sync_client.complete(prompt)

async def ask_legacy_agent(prompt):
    return await asyncio.to_thread(legacy_agent_call, prompt)

to_thread() is primarily for I/O-bound functions that would otherwise block the event loop. Because of the GIL, it generally does not speed up pure-Python CPU-bound work, except where code releases the GIL or on Python implementations without one. This behavior is covered in the Python task reference.

Which pattern should you choose?

Need Pattern Key behavior
Related tasks should share a lifetime and stop together on failure TaskGroup Waits for children at context exit; cancels the remaining group tasks after a failure.
Known fan-out and results in input order gather() Returns successful results in submission order; by default, a child error propagates while peers continue.
Cap simultaneous access to an external service Semaphore Limits entry to a guarded section, not task creation or requests per time window.
Incremental or large workloads with controlled consumption Queue and fixed workers Caps worker count; a bounded queue can apply producer backpressure.
Use results as soon as they finish as_completed() Yields completion-oriented processing rather than input-order aggregation.
Put a maximum duration on an operation timeout() Applies a deadline through cancellation; cleanup should preserve cancellation semantics.
A synchronous I/O dependency blocks async work to_thread() Runs the blocking function in a thread, primarily to keep the event loop responsive for I/O-bound work.

Patterns can be combined. For example, a TaskGroup can own a set of agent tasks while each task acquires a semaphore before calling a rate-limited service. A TaskGroup can also supervise queue workers. Keep the ownership boundary clear: know which scope is responsible for waiting for tasks and what should happen to them when that scope fails or times out.

Keep created tasks owned

If you create tasks directly with asyncio.create_task(), retain references to them while they run and arrange to await or cancel them. The event loop keeps only weak references, so unreferenced tasks can disappear before completion. For child work that belongs to a surrounding operation, TaskGroup usually provides the clearer ownership boundary. See the Python documentation on tasks.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.