Shipping prompt or model changes directly to production is reckless when the failure mode is silent quality degradation. A staging environment mirroring production LLM traffic lets you replay real inputs against candidate builds before users see them. This guide walks through a concrete pipeline to capture, sanitize, and replay traffic using standard OpenAI-compatible clients.
Step 1: Capture production requests without adding latency
Instrument the client you already use. Wrap the completion call so the request payload is pushed to a non-blocking queue and then to durable storage. Do not await the write in the request path; use a separate thread or async task.
import openai, redis, json, threading
r = redis.Redis(decode_responses=True)
def logged_create(client: openai.OpenAI, **kwargs):
# fire-and-forget: Redis writes are sub-millisecond on localhost
threading.Thread(target=r.rpush, args=("llm_prod_log", json.dumps(kwargs, default=str))).start()
return client.chat.completions.create(**kwargs)
The payload includes model, messages, temperature, and any provider-specific keys. You will replay these exactly, so preserve the raw dict.
Step 2: Sanitize and store traffic for replay
Production logs contain PII and routing hints you do not want in staging. Define a strict schema and drop everything else before persisting.
from pydantic import BaseModel, ConfigDict
from typing import List, Dict, Any, Optional
class CapturedRequest(BaseModel):
model: str
messages: List[Dict[str, Any]]
temperature: float = 1.0
max_tokens: Optional[int] = None
stream: bool = False
model_config = ConfigDict(extra="ignore") # drop user_id, client_ip, etc.
Run a worker that pops from llm_prod_log, parses into CapturedRequest, and writes to an object store or a replay queue. Keep the original model string; your staging gateway must understand it.
Step 3: Stand up a staging deployment with isolated credentials
Create a separate API key and, if possible, a separate endpoint. This isolates spend and rate limits from production. If you route through an OpenAI-compatible gateway such as n4n.ai, you get per-token usage metering that separates staging cost and automatic fallback when a provider is degraded, so replay storms don’t page you.
Set environment variables:
export STAGING_LLM_BASE_URL="https://gateway.example.com/v1"
export STAGING_LLM_API_KEY="sk-staging-xxx"
Point your staging client at these. Never reuse the production key; a bug in replay could otherwise exhaust your live quota.
Step 4: Build a replay worker that mirrors traffic
The worker consumes captured requests and calls staging with the same parameters. Disable streaming unless you specifically test streaming logic, to simplify validation.
import os, openai, redis, json
from step2 import CapturedRequest
client = openai.OpenAI(
base_url=os.environ["STAGING_LLM_BASE_URL"],
api_key=os.environ["STAGING_LLM_API_KEY"],
)
r = redis.Redis(decode_responses=True)
def replay_once():
_, raw = r.blpop("llm_prod_log", timeout=5)
if not raw:
return
req = CapturedRequest.model_validate_json(raw)
payload = req.model_dump(exclude_none=True)
try:
resp = client.chat.completions.create(**payload)
return resp.usage.total_tokens
except openai.APIError as e:
# log and continue; staging may have tighter limits
print("replay failed:", e.status_code)
Run this in a loop or a distributed consumer group so you can replay at production scale without overwhelming staging.
Step 5: Validate responses and measure divergence
A staging environment mirroring production LLM traffic is useless if you do not check the outputs. At minimum, assert the call succeeded and the response shape is correct. For deeper confidence, compute a similarity score against the production response captured earlier (store both).
def compare(prod_resp: str, stage_resp: str) -> float:
# trivial length-based heuristic; replace with embedding cosine or LLM judge
return 1.0 - abs(len(prod_resp) - len(stage_resp)) / max(len(prod_resp), 1)
Track the divergence rate per model. If a prompt change pushes the score below a threshold (e.g., 0.8), fail the deploy.
What to compare
- Token counts: large deviations hint at broken truncation.
- Finish reason:
lengthin staging butstopin prod means a param mismatch. - Tool call schemas: if your app uses function calling, validate the JSON parses.
Step 6: Automate continuous mirroring with a shadow fork
For live coverage, duplicate traffic at the edge. If you already use an OpenAI-compatible proxy, add a fork rule that sends a copy to staging with a header x-shadow: true. The proxy must not block on the shadow response.
{
"route": {
"match": { "path": "/v1/chat/completions" },
"mirror": [
{ "upstream": "staging-gateway", "headers": { "x-shadow": "true" } }
]
}
}
This gives you a staging environment mirroring production LLM traffic in real time, not just from a backlog.
Verifying success
You have a working setup when:
- Production latency is unchanged after adding capture (measure p99 before/after).
- The replay worker reports zero
APIErrorexceptions for a 10k-request sample. - Divergence score for unchanged prompts stays above your threshold (should be ~1.0).
- Staging cost appears as a distinct line item in your metering dashboard.
Run a canary: deploy a no-op prompt change to staging, replay a day of traffic, and confirm the pipeline surfaces no false positives. Only then trust it to catch real regressions.
Keep the capture schema versioned. When you add a new parameter (like response_format), extend CapturedRequest and migrate old logs or ignore the field. The goal is reproducibility: same input, same model, same config—different code path.
If you follow these steps, your staging environment mirroring production LLM traffic becomes a real safety net rather than a toy dataset of hand-written examples.