---
title: "Concurrent Requests"
description: "Patterns for making concurrent requests with the TraceRTM Python SDK"
---

# Concurrent Requests

Learn how to efficiently make concurrent API requests using the TraceRTM Python SDK's async client.

## Basic Concurrency with asyncio.gather

The simplest way to make concurrent requests is with `asyncio.gather`:

```python
import asyncio
from tracertm import AsyncTraceRTMClient

async def fetch_dashboard_data(project_id: str):
    async with AsyncTraceRTMClient() as client:
        # All three requests execute concurrently
        project, items, coverage = await asyncio.gather(
            client.projects.get(project_id),
            client.items.list(project_id=project_id, limit=10),
            client.reports.coverage(project_id=project_id)
        )

        return {
            "project": project,
            "recent_items": items.data,
            "coverage_percent": coverage.coverage_percent
        }

# Run
data = asyncio.run(fetch_dashboard_data("proj_abc123"))
```

---

## Controlled Concurrency

### Using Semaphores

Limit concurrent requests to avoid overwhelming the API:

```python
import asyncio
from tracertm import AsyncTraceRTMClient

async def fetch_items_controlled(item_ids: list[str], max_concurrent: int = 10):
    """Fetch multiple items with controlled concurrency."""
    semaphore = asyncio.Semaphore(max_concurrent)

    async def fetch_one(client: AsyncTraceRTMClient, item_id: str):
        async with semaphore:
            return await client.items.get(item_id, include=["links"])

    async with AsyncTraceRTMClient() as client:
        tasks = [fetch_one(client, id) for id in item_ids]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        items = []
        errors = []

        for item_id, result in zip(item_ids, results):
            if isinstance(result, Exception):
                errors.append({"id": item_id, "error": str(result)})
            else:
                items.append(result)

        return {"items": items, "errors": errors}

# Fetch 100 items, max 10 concurrent
result = asyncio.run(fetch_items_controlled(
    [f"item_{i}" for i in range(100)],
    max_concurrent=10
))
```

### Using asyncio.BoundedSemaphore

Prevents accidental release beyond the initial count:

```python
async def process_with_bounded_semaphore(project_id: str, max_workers: int = 5):
    semaphore = asyncio.BoundedSemaphore(max_workers)

    async with AsyncTraceRTMClient() as client:
        async def process_item(item):
            async with semaphore:
                # Process item
                updated = await client.items.update(
                    item.id,
                    add_tags=["processed"]
                )
                return updated

        items = await client.items.list(project_id=project_id)
        tasks = [process_item(item) for item in items.data]
        return await asyncio.gather(*tasks)
```

---

## Task Groups (Python 3.11+)

Task groups provide structured concurrency with better error handling:

```python
import asyncio
from tracertm import AsyncTraceRTMClient

async def fetch_with_task_group(project_ids: list[str]):
    """Fetch multiple projects using TaskGroup."""
    async with AsyncTraceRTMClient() as client:
        results = {}

        async with asyncio.TaskGroup() as tg:
            for pid in project_ids:
                async def fetch(project_id=pid):
                    project = await client.projects.get(project_id)
                    results[project_id] = project

                tg.create_task(fetch())

        return results

# All tasks complete or all fail
projects = asyncio.run(fetch_with_task_group(["proj_1", "proj_2", "proj_3"]))
```

### Handling Partial Failures

```python
async def fetch_with_error_handling(item_ids: list[str]):
    """Handle individual task failures gracefully."""
    async with AsyncTraceRTMClient() as client:
        results = {"success": [], "failed": []}

        async def fetch_one(item_id: str):
            try:
                item = await client.items.get(item_id)
                results["success"].append(item)
            except Exception as e:
                results["failed"].append({"id": item_id, "error": str(e)})

        try:
            async with asyncio.TaskGroup() as tg:
                for item_id in item_ids:
                    tg.create_task(fetch_one(item_id))
        except ExceptionGroup:
            pass  # Individual errors already captured

        return results
```

---

## Batch Processing

### Process in Batches

```python
async def process_in_batches(
    items: list[dict],
    batch_size: int = 50,
    delay_between_batches: float = 0.5
):
    """Process items in batches with delay between batches."""
    async with AsyncTraceRTMClient() as client:
        all_results = []

        for i in range(0, len(items), batch_size):
            batch = items[i:i + batch_size]

            # Process batch concurrently
            results = await client.items.bulk_create(batch)
            all_results.extend(results)

            # Rate limiting delay
            if i + batch_size < len(items):
                await asyncio.sleep(delay_between_batches)

            print(f"Processed batch {i // batch_size + 1}")

        return all_results
```

### Parallel Batch Processing

```python
async def parallel_batch_processing(project_id: str, batch_size: int = 100):
    """Process all items in parallel batches."""
    async with AsyncTraceRTMClient() as client:
        # Get total count
        first_page = await client.items.list(project_id=project_id, limit=1)
        total = first_page.total

        # Create tasks for each batch
        async def process_batch(offset: int):
            items = await client.items.list(
                project_id=project_id,
                limit=batch_size,
                offset=offset
            )
            # Process items in this batch
            return [await process_item(item) for item in items.data]

        # Calculate offsets
        offsets = range(0, total, batch_size)

        # Process all batches concurrently (with limit)
        semaphore = asyncio.Semaphore(5)  # Max 5 concurrent batches

        async def limited_batch(offset: int):
            async with semaphore:
                return await process_batch(offset)

        tasks = [limited_batch(offset) for offset in offsets]
        results = await asyncio.gather(*tasks)

        # Flatten results
        return [item for batch in results for item in batch]
```

---

## Producer-Consumer Pattern

```python
import asyncio
from tracertm import AsyncTraceRTMClient

async def producer_consumer_pattern(project_id: str):
    """Use producer-consumer for continuous processing."""
    queue = asyncio.Queue(maxsize=100)

    async def producer(client: AsyncTraceRTMClient):
        """Fetch items and add to queue."""
        async for item in client.items.iterate(project_id=project_id):
            await queue.put(item)

        # Signal completion
        await queue.put(None)

    async def consumer(client: AsyncTraceRTMClient, consumer_id: int):
        """Process items from queue."""
        processed = 0
        while True:
            item = await queue.get()

            if item is None:
                # Pass the signal to next consumer
                await queue.put(None)
                break

            # Process item
            await client.items.update(item.id, add_tags=[f"processed-by-{consumer_id}"])
            processed += 1
            queue.task_done()

        return processed

    async with AsyncTraceRTMClient() as client:
        # Start producer and multiple consumers
        producer_task = asyncio.create_task(producer(client))
        consumer_tasks = [
            asyncio.create_task(consumer(client, i))
            for i in range(3)  # 3 consumers
        ]

        # Wait for completion
        await producer_task
        results = await asyncio.gather(*consumer_tasks)

        print(f"Consumers processed: {results}")
```

---

## Streaming Results

### Async Generator Pattern

```python
async def stream_search_results(
    client: AsyncTraceRTMClient,
    query: str,
    project_ids: list[str]
):
    """Stream search results as they arrive."""
    for project_id in project_ids:
        results = await client.search.items(
            query=query,
            project_ids=[project_id]
        )
        for hit in results.items:
            yield hit

# Usage
async def process_search():
    async with AsyncTraceRTMClient() as client:
        async for hit in stream_search_results(
            client,
            "authentication",
            ["proj_1", "proj_2", "proj_3"]
        ):
            print(f"Found: {hit.item.title}")
```

### Buffered Streaming

```python
async def buffered_stream(project_id: str, buffer_size: int = 10):
    """Stream items with buffering for smoother processing."""
    async with AsyncTraceRTMClient() as client:
        buffer = []

        async for item in client.items.iterate(project_id=project_id):
            buffer.append(item)

            if len(buffer) >= buffer_size:
                yield buffer
                buffer = []

        # Yield remaining items
        if buffer:
            yield buffer

# Process in chunks
async def process_buffered():
    async for chunk in buffered_stream("proj_abc123", buffer_size=20):
        print(f"Processing chunk of {len(chunk)} items")
        # Process chunk concurrently
        await asyncio.gather(*[process_item(item) for item in chunk])
```

---

## Error Recovery

### Retry with Exponential Backoff

```python
import asyncio
from tracertm import AsyncTraceRTMClient
from tracertm.exceptions import RateLimitError, ServerError

async def fetch_with_retry(
    client: AsyncTraceRTMClient,
    item_id: str,
    max_retries: int = 3,
    base_delay: float = 1.0
):
    """Fetch item with exponential backoff retry."""
    for attempt in range(max_retries):
        try:
            return await client.items.get(item_id)
        except RateLimitError as e:
            delay = e.retry_after or (base_delay * (2 ** attempt))
            print(f"Rate limited, waiting {delay}s")
            await asyncio.sleep(delay)
        except ServerError:
            if attempt == max_retries - 1:
                raise
            delay = base_delay * (2 ** attempt)
            print(f"Server error, retrying in {delay}s")
            await asyncio.sleep(delay)

    raise Exception(f"Failed after {max_retries} attempts")
```

### Graceful Degradation

```python
async def fetch_with_fallback(project_id: str):
    """Fetch data with fallback on partial failure."""
    async with AsyncTraceRTMClient() as client:
        results = {}

        # Critical data
        try:
            results["project"] = await client.projects.get(project_id)
        except Exception as e:
            # Project is critical, re-raise
            raise

        # Optional data with fallback
        try:
            results["coverage"] = await client.reports.coverage(project_id=project_id)
        except Exception:
            results["coverage"] = {"coverage_percent": None, "error": "Unavailable"}

        try:
            items = await client.items.list(project_id=project_id, limit=10)
            results["recent_items"] = items.data
        except Exception:
            results["recent_items"] = []

        return results
```

---

## Performance Monitoring

### Request Timing

```python
import asyncio
import time
from tracertm import AsyncTraceRTMClient

async def timed_requests(project_id: str):
    """Measure request timing."""
    async with AsyncTraceRTMClient() as client:
        timings = []

        async def timed_fetch(item_id: str):
            start = time.perf_counter()
            item = await client.items.get(item_id)
            elapsed = time.perf_counter() - start
            timings.append({"id": item_id, "time": elapsed})
            return item

        # Get some items to fetch
        items_page = await client.items.list(project_id=project_id, limit=20)
        item_ids = [item.id for item in items_page.data]

        # Fetch all concurrently
        start = time.perf_counter()
        await asyncio.gather(*[timed_fetch(id) for id in item_ids])
        total_time = time.perf_counter() - start

        # Report
        avg_time = sum(t["time"] for t in timings) / len(timings)
        print(f"Total: {total_time:.2f}s")
        print(f"Average per request: {avg_time:.3f}s")
        print(f"Effective throughput: {len(item_ids) / total_time:.1f} req/s")
```

---

## Best Practices

1. **Always use context managers** - Ensures proper connection cleanup
2. **Limit concurrency** - Use semaphores to avoid overwhelming the API
3. **Handle errors gracefully** - Use `return_exceptions=True` with gather
4. **Reuse client instances** - Don't create a new client for each request
5. **Use bulk operations** - Prefer bulk_create/bulk_update over many single requests
6. **Monitor rate limits** - Check `client.rate_limit_remaining` after requests
7. **Implement backoff** - Use exponential backoff for retries

---

## Related Documentation

- [Async Overview](/docs/api-reference/sdks/python/async) - Async concepts
- [Async Client](/docs/api-reference/sdks/python/async-client) - Client reference
- [Error Handling](/docs/api-reference/sdks/python/error-handling) - Error handling
