Problem
During load testing and in production traffic spikes, the RAG reranker component of LlamaIndex running inside a Docker container consistently throws timeout errors. The failure manifests as:
2024-09-28 12:34:56,789 ERROR reranker - RerankerTimeoutError: Reranking request timed out after 30.0 seconds
Traceback (most recent call last):
File "/app/llama_index/reranker.py", line 212, in rank
response = self.client.post(payload, timeout=self.timeout)
File "/usr/local/lib/python3.11/site-packages/requests/models.py", line 975, in request
resp = session.request(method=method, url=url, **kwargs)
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='reranker-service', port=443): Read timed out. (read timeout=30)
Symptoms observed under load:
- Increased 5xx responses from the API endpoint that forwards queries to LlamaIndex.
- CPU throttling in the container (observed via
docker stats). - Spike in
RerankerTimeoutErrorlogs while other LlamaIndex components (retriever, generator) continue to respond.
Root Cause
The timeout originates from three interacting factors:
- Insufficient container CPU quota: The Docker run command limits the container to
0.5 CPUs. Under load the PythonThreadPoolExecutorused by the reranker cannot schedule new tasks, causing request queuing and eventual timeout. - Low default reranker timeout: LlamaIndex sets
reranker_timeout_seconds=30by default. The external reranker service (a hosted transformer model) exhibits 20‑25 s latency when the GPU is saturated, leaving a narrow margin for queuing delays. - Blocking I/O in the request path: The reranker client uses
requestssynchronously. When the container hits theulimit -nfile‑descriptor ceiling (default 1024), new outbound connections are blocked, further extending latency.
Combined, these conditions cause the reranker request to exceed the 30 s deadline, triggering the observed timeout.
Debug
Below is the step‑by‑step investigation performed in the failing environment.
1. Verify container resource limits
$ docker inspect rag_container --format='{{json .HostConfig}}' | jq .CpuQuota
# Output
100000 # 0.1 CPU (100 ms of CPU time per 100 ms period)
Result: the container was unintentionally limited to 0.1 CPU.
2. Observe live CPU usage
$ docker stats rag_container --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT
a1b2c3d4e5f6 rag_container 98.73% 512Mi / 1Gi
CPU saturation correlates with the spike in timeout errors.
3. Capture request latency to the reranker service
$ curl -w "\n%{time_total}\n" -o /dev/null -s https://reranker-service/v1/rank \
-d '{"queries":["test"]}' -H "Content-Type: application/json"
# Example output during load
0.00
24.73
Latency approaches 25 s, leaving only 5 s for queuing before the 30 s deadline.
4. Check file‑descriptor limits
$ docker exec rag_container sh -c 'ulimit -n'
1024
During load the process reaches the limit:
2024-09-28 12:35:02,112 WARNING reranker - ResourceWarning: too many open files
5. Review LlamaIndex reranker configuration
# settings.py (original)
RERANKER_TIMEOUT_SECONDS = 30
MAX_WORKERS = 4
Solution
The fix consists of three coordinated changes: increase container CPU, raise the reranker timeout, and adjust the client to use a non‑blocking HTTP pool.
1. Update Docker run configuration
Before:
docker run -d --name rag_container \
-p 8080:8080 \
--cpu-quota=100000 \ # 0.1 CPU
--memory=1g \
myorg/llamaindex-rag:latest
After (allocate 2 full CPUs and raise file‑descriptor limit):
docker run -d --name rag_container \
-p 8080:8080 \
--cpus="2.0" \ # 2 CPUs
--memory=4g \
--ulimit nofile=65535:65535 \
myorg/llamaindex-rag:latest
2. Extend reranker timeout in LlamaIndex configuration
Before:
# config.yaml
reranker:
timeout_seconds: 30
max_workers: 4
After (increase to 60 s and double workers):
# config.yaml
reranker:
timeout_seconds: 60
max_workers: 8
3. Switch to an asynchronous HTTP client
Replace the synchronous requests call with httpx.AsyncClient to avoid blocking the thread pool.
Before:
import requests
def rank(payload):
resp = requests.post(
url=self.endpoint,
json=payload,
timeout=self.timeout,
)
resp.raise_for_status()
return resp.json()
After:
import httpx
import asyncio
class AsyncReranker:
def __init__(self, endpoint, timeout):
self.endpoint = endpoint
self.timeout = timeout
self.client = httpx.AsyncClient(timeout=timeout)
async def rank(self, payload):
resp = await self.client.post(self.endpoint, json=payload)
resp.raise_for_status()
return resp.json()
# Usage in async context
async def rank_query(payload):
reranker = AsyncReranker("https://reranker-service/v1/rank", 60)
return await reranker.rank(payload)
Deploy the updated code and rebuild the Docker image.
Verify
After applying the changes, run a controlled load test (e.g., wrk with 200 concurrent connections for 2 minutes) and observe the following:
1. No timeout logs
2024-09-28 13:12:04,321 INFO reranker - Ranking completed in 31.4 seconds
2. CPU usage stays below 80 %
$ docker stats rag_container --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT
b7c8d9e0f1a2 rag_container 45.12% 1.2Gi / 4Gi
3. File‑descriptor count remains stable
$ docker exec rag_container sh -c 'lsof | wc -l'
342
4. End‑to‑end latency meets SLA
$ wrk -t12 -c200 -d120s http://localhost:8080/query
Latency 512ms 95% <= 1.2s
Requests/sec: 845.23
Prevent
- Monitoring: Export
reranker_request_duration_secondsandreranker_timeout_totalto Prometheus; set alerts on 95th‑percentile > 45 s. - Resource Guardrails: Define Kubernetes
resources.limits.cpuandresources.requests.cputhat match the observed load profile; useHorizontalPodAutoscalerto scale out whencpu_utilizationexceeds 70 %. - Connection Pooling: Keep the
httpx.AsyncClientinstance alive for the container lifetime to reuse TCP connections. - Timeout Margin: Configure the reranker timeout to be at least 2× the 99th‑percentile latency of the external model service.
- File‑Descriptor Limits: Set
nofileulimit to ≥ 65535 in production containers.
Related Topic Hub: RAG Systems Troubleshooting Hub
FAQ
- Why does the reranker timeout only under load and not in development?
Development runs with unlimited host resources and a low query rate, so the synchronousrequestsclient never queues. Production containers are CPU‑capped and hit the file‑descriptor ceiling, causing queuing delays that exceed the 30 s timeout. - Can I keep the original 30 s timeout and still avoid errors?
Only if you guarantee the external reranker service stays below ~15 s latency and the container has enough CPU to process requests without queuing. In practice, increasing the timeout and adding async I/O is more robust. - Is switching to
httpx.AsyncClientmandatory?
Not mandatory, but it eliminates thread‑pool blockage caused by synchronous network I/O. If you retainrequests, you must increasemax_workersand CPU, which may still lead to scaling limits. - How do I know the appropriate
max_workersvalue?
Start withmax_workers = min(2 * CPU_cores, 8). Adjust upward while monitoringThreadPoolExecutor._work_queue.qsize()and CPU usage; avoid exceeding the container’s CPU quota. - What alert thresholds should I set for reranker latency?
- Warning: 95th‑percentile latency > 40 s
- Critical: timeout count > 5 per minute or 95th‑percentile latency > 55 s