I was testing a RAG ingestion pipeline and noticed something painful: ingesting a single document took nearly 50 seconds.
Checking the metrics revealed that the CPU was doing almost nothing. The app wasn't compute-bound β it was simply sitting idle, waiting for sequential HTTP network round-trips to return.
By converting the blocking embedding module into an asynchronous workflow, processing time dropped from 49.61 seconds to 1.56 seconds with zero infrastructure changes.
us-east-1
| Approach | Time | Difference |
|---|---|---|
Sequential (requests blocking loop) |
||
| 49.61s | ||
| Baseline | ||
Concurrent (aiohttp + asyncio.gather ) |
||
| 1.56s | ||
| 31.8Γ faster |
Each request must complete before the next one starts.
chunk 1 ββ> request ββ> β³ wait (~1.5s) ββ> response
chunk 2 ββ> request ββ> β³ wait (~1.5s) ββ> response
chunk 3 ββ> request ββ> β³ wait (~1.5s) ββ> response
...
Total Time β (33 chunks Γ ~1.5s) β 49.6s
Since network latency dominates the runtime, the event loop sits completely unused.
Requests are dispatched concurrently; total time collapses down to the duration of the slowest single request.
chunk 1 ββ> request ββββββββββββββ> response
chunk 2 ββ> request ββββββββββββββ> response
chunk 3 ββ> request ββββββββββββββ> response
... (in flight concurrently)
Total Time β slowest single request β 1.56s
php
import requests
def generate_embeddings(texts: list[str]) -> list[list[float]]:
all_embeddings = []
for text in texts:
response = requests.post(url, headers=headers, json={"inputText": text})
vector = response.json().get("embedding", [])
all_embeddings.append(vector)
return all_embeddings
python
import asyncio
import aiohttp
async def fetch_embedding(session: aiohttp.ClientSession, text: str) -> list[float]:
async with session.post(url, headers=headers, json={"inputText": text}) as res:
data = await res.json()
return data.get("embedding", [])
async def generate_embeddings_async(texts: list[str]) -> list[list[float]]:
async with aiohttp.ClientSession() as session:
tasks = [fetch_embedding(session, text) for text in texts]
return await asyncio.gather(*tasks)
Note: If you're using boto3 instead of raw HTTP requests, look at aioboto3, or offload the blocking SDK calls with asyncio.to_thread() to get similar non-blocking behaviour.
| Chunks | Sequential (est.) | Concurrent (est.) |
|---|---|---|
| 31 | 49.61s | 1.56s |
| 100 | ~160s (2.7 min) | ~2β3s |
| 500 | ~800s (13 min) | ~3β5s |
| 1,000 | ~1,600s (26 min) | ~5β10s |
The more chunks you ingest, the more the concurrent approach pays off β batch ingestion jobs that took half an hour now finish in seconds.
asyncio.Semaphore
) before you hit them.embedder.py
) removed the embedding stage as a bottleneck in the pipeline.