Cua Docs

Run local sandboxes in parallel

Run many local sandbox jobs at once, limit concurrency, and handle failures.

This guide runs local sandboxes with local=True. Because Sandbox.ephemeral and Sandbox.create are async, you can run many local sandboxes at the same time using asyncio.

For hosted Fleet capacity, follow Create Fleet capacity to create a pool, then claim Sandboxes for your tasks.

Every local sandbox consumes host CPU, memory, and disk. Start with low concurrency and increase it only after observing host resource use.

Run a fixed set concurrently#

Use asyncio.gather when you already have the full list of sandbox jobs.

import asyncio
from cua import Sandbox, Image
 
async def run_task(task: str) -> str:
    async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
        result = await sb.shell.run(f"echo '{task}'")
        return result.stdout.strip()
 
async def main():
    tasks = ['task-1', 'task-2', 'task-3', 'task-4']
    results = await asyncio.gather(*[run_task(t) for t in tasks])
    print(results)
 
asyncio.run(main())

Cap the number of concurrent sandboxes#

Use an asyncio.Semaphore when you need rate limiting.

import asyncio
from cua import Sandbox, Image
 
MAX_CONCURRENT = 5
 
async def process_item(sem: asyncio.Semaphore, item: str) -> dict:
    async with sem:
        async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
            result = await sb.shell.run(f"python /app/process.py '{item}'")
            return {'item': item, 'output': result.stdout, 'ok': result.success}
 
async def main():
    items = [f'item-{i}' for i in range(20)]
    sem = asyncio.Semaphore(MAX_CONCURRENT)
    results = await asyncio.gather(*[process_item(sem, item) for item in items])
    print(f"{sum(r['ok'] for r in results)}/{len(results)} succeeded")
 
asyncio.run(main())

Process a dynamic queue of tasks#

Use asyncio.Queue when tasks arrive at runtime.

import asyncio
from cua import Sandbox, Image
 
async def worker(queue: asyncio.Queue, worker_id: int):
    while True:
        task = await queue.get()
        if task is None:
            break
        async with Sandbox.ephemeral(Image.linux(), local=True) as sb:
            result = await sb.shell.run(task)
            print(f'[worker-{worker_id}] {result.stdout.strip()}')
        queue.task_done()
 
async def main():
    queue: asyncio.Queue = asyncio.Queue()
    num_workers = 4
    workers = [asyncio.create_task(worker(queue, i)) for i in range(num_workers)]
    for i in range(10):
        await queue.put(f"echo 'job {i}'")
    for _ in range(num_workers):
        await queue.put(None)
    await asyncio.gather(*workers)
 
asyncio.run(main())

Handle failures#

Set return_exceptions=True when one failed sandbox job should not cancel the rest.

results = await asyncio.gather(
    *[run_task(t) for t in tasks],
    return_exceptions=True
)
for task, result in zip(tasks, results):
    if isinstance(result, Exception):
        print(f'{task}: FAILED — {result}')
    else:
        print(f'{task}: {result}')