Problem
The retrieval‑augmented generation (RAG) service deployed on a Kubernetes cluster fails during the performance benchmark phase with a prompt template rendering error. The error appears only under load and aborts the benchmark run, causing the following observable symptoms:
- Benchmark client receives HTTP 500 responses with body
{"error":"PromptTemplateError: variable 'context' is undefined"}. - Pod logs contain stack traces from the Jinja2 renderer.
- CPU usage spikes to 90 % on the
rag-workerpods, while memory remains stable. - Other endpoints (e.g., health check, simple echo) continue to respond normally.
Root Cause
Under load the RAG service processes multiple concurrent requests that share a singleton PromptTemplateRenderer instance. The renderer caches the compiled Jinja2 template and re‑uses the same Context dictionary for each request. When the benchmark driver fires hundreds of parallel queries, the shared context is mutated by one request before another request finishes rendering, leading to missing keys such as context or retrieved_docs. The resulting jinja2.exceptions.UndefinedError propagates as a 500 error.
Two intertwined factors make the problem surface only during benchmarking:
- Singleton pattern in the container image. The Python module creates the renderer at import time, so all worker threads share mutable state.
- Kubernetes pod concurrency settings. The deployment uses
replicas: 3withresources.limits.cpu: "2"andrequests.cpu: "1". The horizontal pod autoscaler (HPA) does not scale out because the benchmark runs for a short burst, so the three pods become the bottleneck.
Debug
Step‑by‑step investigation that isolated the issue:
- Collect pod logs during benchmark.
kubectl logs -l app=rag-service -c rag-worker --since=5m > benchmark.log
Sample excerpt from benchmark.log:
2026-10-19T12:34:56.123Z ERROR rag_worker.renderer - PromptTemplateError: variable 'context' is undefined
Traceback (most recent call last):
File "/app/renderer.py", line 87, in render
rendered = self.template.render(context)
File "/usr/local/lib/python3.11/site-packages/jinja2/environment.py", line 1085, in render
return self.environment.handle_exception(exc_info, True)
jinja2.exceptions.UndefinedError: 'context' is undefined
- Inspect the renderer singleton. Attach to a running pod and dump the object’s state.
kubectl exec -it $(kubectl get pod -l app=rag-service -o name | head -n1) -- bash
python - <<'PY'
from rag.renderer import PromptTemplateRenderer
print(PromptTemplateRenderer._instance) # shows shared instance
PY
Output shows a single instance with a mutable self.last_context attribute.
- Reproduce locally with threading. Running the same template rendering in a multi‑threaded script reproduces the
UndefinedErrorafter ~150 concurrent threads.
python -m rag.tests.concurrent_render_test
- Check Kubernetes resource limits. Verify that the pod CPU limit is not being throttled.
kubectl top pod -l app=rag-service
NAME CPU(cores) MEMORY(bytes)
rag-service-abc123 1900m 512Mi
rag-service-def456 1850m 508Mi
rag-service-ghi789 1920m 511Mi
CPU usage is near the limit, confirming contention.
Solution
The fix consists of two parts: make the renderer stateless per request and adjust the deployment to allow scaling under burst load.
Code changes
Before (singleton renderer with mutable state):
# renderer.py
from jinja2 import Environment, FileSystemLoader
class PromptTemplateRenderer:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls)
env = Environment(loader=FileSystemLoader("/templates"))
cls._instance.template = env.get_template("rag_prompt.jinja2")
cls._instance.last_context = {}
return cls._instance
def render(self, context: dict) -> str:
# Mutates shared state
self.last_context = context
return self.template.render(context)
After (stateless renderer, instantiated per request):
# renderer.py
from jinja2 import Environment, FileSystemLoader, StrictUndefined
class PromptTemplateRenderer:
def __init__(self, template_name: str = "rag_prompt.jinja2"):
env = Environment(
loader=FileSystemLoader("/templates"),
undefined=StrictUndefined, # fail fast on missing keys
autoescape=False,
)
self.template = env.get_template(template_name)
def render(self, context: dict) -> str:
# No shared mutable state
return self.template.render(context)
Update the request handler to create a fresh renderer instance:
# handler.py
from .renderer import PromptTemplateRenderer
def handle_rag_request(request):
payload = request.json()
renderer = PromptTemplateRenderer()
try:
prompt = renderer.render({
"question": payload["question"],
"retrieved_docs": payload["docs"],
"context": payload.get("context", ""),
})
except Exception as e:
logger.error("PromptTemplateError: %s", e)
raise
# continue with LLM call...
Deployment changes
Increase burst capacity by allowing the HPA to scale out quickly and by adding a podDisruptionBudget to keep at least one replica healthy.
# rag-deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-service
spec:
replicas: 3
selector:
matchLabels:
app: rag-service
template:
metadata:
labels:
app: rag-service
spec:
containers:
- name: rag-worker
image: myregistry.com/rag-service:1.4.0
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "2"
memory: "1Gi"
env:
- name: MAX_CONCURRENCY
value: "50" # limit per pod
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: rag-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: rag-service
minReplicas: 3
maxReplicas: 12
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 4
periodSeconds: 60
Verify
After applying the changes, run the benchmark again and confirm that the error disappears.
- Redeploy the updated image and HPA configuration:
kubectl apply -f rag-deployment.yaml
kubectl rollout status deployment/rag-service
- Execute the benchmark script (e.g.,
bench/run.sh) and monitor logs:
kubectl logs -l app=rag-service -c rag-worker -f | grep PromptTemplateError
No output should appear.
- Validate that the HPA scaled out during the burst:
kubectl get hpa rag-service-hpa -w
# Expected: replicas increase from 3 to 8‑10 during the test, then scale back.
- Run a functional check on a single request to ensure the template renders correctly:
curl -X POST http://rag-service.default.svc.cluster.local/v1/generate \
-H "Content-Type: application/json" \
-d '{"question":"What is RAG?","docs":["doc1","doc2"]}' | jq .
Response should contain a generated_text field without error.
Prevent
Implement the following guardrails to avoid regression:
- Stateless components. Ensure any object that holds request‑specific data is instantiated per request or uses thread‑local storage.
- Strict template rendering. Use
StrictUndefinedin Jinja2 to catch missing variables early. - Load testing in CI. Include a benchmark stage that simulates at least 200 concurrent requests and asserts zero 5xx responses.
- Resource alerts. Configure Prometheus alerts on
container_cpu_usage_seconds_totaland on the custom metricrag_prompt_render_errors_total(incremented in the exception handler). - HPA tuning. Set
maxReplicasandscaleUppolicies to handle short‑lived spikes.
Related Topic Hub: Distributed Systems Troubleshooting Hub
FAQ
- Why does the error appear only under load and not during normal traffic?
Because the singleton renderer shares mutable state across concurrent requests. Under low traffic the race condition rarely manifests, but high concurrency creates overlapping renders that overwrite the shared context. - Can I keep a singleton renderer for performance reasons?
Only if the renderer is truly immutable. Compile the template once (which is safe) but do not store request‑specific data on the instance. Pass a fresh context dictionary torender()each time. - Do I need to increase the pod CPU limit after fixing the code?
Not necessarily. The stateless renderer reduces CPU spikes, but you may still want a modest headroom (e.g., 2 CPU) to accommodate bursty LLM inference workloads. - How can I detect similar race conditions in other services?
EnableStrictUndefinedor equivalent strict mode in templating engines, and add unit tests that run the rendering function in parallel threads, asserting noUndefinedErroroccurs. - Is there a way to make the HPA react faster to short bursts?
Configurebehavior.scaleUpwith a lowstabilizationWindowSecondsand a highervalueperperiodSeconds. Also consider usingexternalmetrics that reflect request latency or queue length for more immediate scaling signals.