Shadow Evals: The Test Layer That Catches Regressions Before Users Do
Regressions in LLM products don't announce themselves — they accumulate silently until a user screenshots something embarrassing or a support ticket volume spikes. By then you've already shipped the damage. Shadow evals break that loop entirely, and most teams aren't running them.
The idea is simple: before you promote a new model, a new prompt, or a new retrieval config to production, you run it against a sample of real traffic in parallel — same inputs, no user-facing output — and compare the responses against your current production baseline. You catch quality drift before users see it, not after.
This is distinct from a static eval suite. Static evals test known cases. Shadow evals test the long tail of what your actual users actually send.
Why Static Eval Suites Alone Aren't Enough
Every team I've worked with starts with a handcrafted golden set: 50–200 curated input/output pairs they run before deploying. It's necessary. It's not sufficient.
The problem is distributional drift. Your golden set was built from yesterday's traffic patterns. Users evolve: they ask questions you didn't anticipate, they combine features in unexpected ways, they write in three languages in a single message. A GPT-4o → GPT-4.1 upgrade might pass your golden set and still regress on the 15% of traffic that includes Arabic-English code-switching — because you never had those cases in your static suite.
At Etera AI, multi-agent travel planning surfaces exactly this failure mode: a prompt change that scores clean on curated itinerary examples can silently degrade on the long tail of conversational re-planning requests that real users actually send. Static evals don't catch that. Shadow evals do.
The directional rule: static evals prove you haven't broken what you know about; shadow evals prove you haven't broken what you've forgotten about.
The Architecture in Four Components
Here's what a minimal shadow eval setup looks like in practice:
codeLive Request │ ├──▶ Production Model (response served to user) │ └──▶ Shadow Model (response logged, never served) │ ▼ Comparison Layer │ ▼ Eval Store (scores, diffs, alerts)
1. Traffic mirroring — duplicate incoming requests (after stripping PII) to a shadow pipeline. At low traffic volumes, mirror 100%. At high volumes, sample 5–20% to keep shadow inference costs manageable. If your production system does 50k requests/day and each costs $0.002, a 10% shadow sample adds ~$100/day — almost always worth it for a staging gate.
2. The shadow model runner — call your candidate model with the same input. Keep it async and isolated: shadow latency must not bleed into production response time. A simple async queue (Redis-backed or SQS-backed) with a separate worker pool handles this cleanly.
3. The comparison layer — this is where most teams underinvest. You need at minimum three signal types:
- Structural checks: did the output maintain required JSON schema? Did it include mandatory fields? These are cheap and deterministic.
- LLM-as-judge scoring: use a cheap, fast model (GPT-4o mini, Gemini Flash) to score candidate vs. baseline on 3–5 dimensions relevant to your use case. Cost is typically $0.0001–0.0005 per comparison pair.
- Embedding similarity: compute cosine similarity between production and shadow response embeddings to catch semantic drift. For cosine similarity to be meaningful here, normalize your embeddings first — raw dot products aren't the same thing. A reasonable starting threshold is around 0.80, but you must calibrate this on your own traffic distribution; customer support responses and creative travel narratives have very different natural similarity ranges.
4. The alert and decision layer — aggregate scores over a rolling window (24–48 hours of shadow traffic) and compare against baseline. Set explicit thresholds: if mean judge score drops more than 0.05 or structural failure rate increases more than 1 percentage point, block the promotion and fire an alert.
The PII Stripping Problem You Must Solve First
Before you mirror a single live request, you need a PII handling policy. This is not optional and it's not just a compliance formality — in regulated industries (finance, health, anything touching EU users), mirroring raw user input to a shadow inference endpoint may constitute unauthorized data processing.
The minimum viable approach:
- Named entity recognition pass before mirroring (spaCy or a small fine-tuned model works fine; don't use the expensive frontier model for this)
- Replace entities with typed placeholders:
[PERSON_NAME],[EMAIL],[PHONE],[ACCOUNT_ID] - Log only the anonymized variant to your eval store
- Keep PII retention policy identical to your production logging policy
In MENA deployments specifically, you have UAE PDPL and Saudi PDPPL layering on top of any global policies. The compliance work here is real, but it's a few days of legal review and engineering, not months. Don't let it block you from starting — start with internal tooling or synthetic data and layer in live traffic mirroring once the data governance is clear.
Scoring Without Ground Truth
The uncomfortable truth about production evals is you rarely have ground truth labels at scale. Users don't tell you when an answer was subtly wrong — they just churn.
Three approaches that work without labels:
Reference-free LLM-as-judge — prompt a judge model to score the response on criteria like factual coherence, instruction following, and tone. The key discipline: score the shadow response AND the production response independently, then compare deltas. Don't just score the shadow response in isolation — you'll get absolute noise, not signal. The comparison is the signal.
Here's a prompt template that's close to what works in production. The critical details are explicit rubrics per criterion (so the judge isn't free-associating a number), JSON-only output to make parsing reliable, and randomized A/B labeling to suppress position bias:
pythonJUDGE_PROMPT_TEMPLATE = """ You are evaluating two AI responses to the same user prompt. Respond with valid JSON only — no explanation, no markdown. User prompt: {prompt} Response A: {response_a} Response B: {response_b} Score each response on the following criteria (integer 1–5): - instruction_following: Did the response do exactly what the prompt asked? 1=ignored instructions, 3=partial, 5=fully followed - factual_coherence: Are claims internally consistent and plausible? 1=contradictory/hallucinated, 3=mostly coherent, 5=fully coherent - tone_appropriateness: Does the tone match the context (formal/casual/helpful)? 1=clearly wrong tone, 3=acceptable, 5=spot-on Output format: {{"A": {{"instruction_following": int, "factual_coherence": int, "tone_appropriateness": int}}, "B": {{"instruction_following": int, "factual_coherence": int, "tone_appropriateness": int}}}} """ def judge_pair(prompt: str, prod_response: str, shadow_response: str) -> dict: # Randomize which is A and which is B to suppress position bias import random if random.random() > 0.5: a, b, flipped = prod_response, shadow_response, False else: a, b, flipped = shadow_response, prod_response, True result = call_judge_model( JUDGE_PROMPT_TEMPLATE.format(prompt=prompt, response_a=a, response_b=b) ) scores = parse_json_safely(result) # Re-orient so prod and shadow are always consistently labeled if flipped: scores["prod"], scores["shadow"] = scores.pop("B"), scores.pop("A") else: scores["prod"], scores["shadow"] = scores.pop("A"), scores.pop("B") return scores
Position bias in judge models is well-documented — if shadow is always "B", your scores will be systematically skewed toward production. The randomization plus re-labeling step above is not optional.
Behavioral consistency checks — if your system has deterministic postconditions (always returns a structured itinerary, always includes a price, always responds in the input language), check those mechanically. These are zero-cost signals that catch hard regressions instantly.
User behavior proxies — if you can tag shadow traffic back to sessions, watch for correlated signals: do sessions where production gave a high-similarity shadow response show better retention? This is a longer-horizon signal but directionally powerful.
Promotion Gates and the Decision Table
Don't make the promotion call subjectively. Wire explicit gates — but understand that the right thresholds depend heavily on your domain. A customer support bot and a creative travel planner have different natural score distributions; calibrate on your own baseline before treating any number here as universal.
| Signal | Starting Threshold | Action if Failed | Calibration note |
|---|---|---|---|
| Structural failure rate | ≤ prod baseline + 0.5% | Block + alert | Deterministic — threshold is stable across domains |
| LLM judge mean score delta | ≥ -0.05 vs baseline | Block + alert | Tighten to -0.03 for high-stakes or regulated outputs |
| Embedding similarity p10 | ≥ 0.78–0.85 (domain-dependent) | Investigate | Calibrate on 1–2 weeks of prod-vs-prod pairs first |
| Latency p95 (shadow) | ≤ 2× prod p95 | Warning only | Shadow includes queue overhead; don't hard-block on this |
| PII leak detection rate | 0 | Hard block | Non-negotiable regardless of domain |
The embedding similarity range is deliberately wide: factual Q&A responses that diverge semantically are almost always a regression, while open-ended creative outputs can legitimately vary more. Run your current production model against itself on a holdout sample to establish what "good" similarity looks like before you evaluate any candidate.
Once all gates pass over your rolling window, promotion is a flag flip — a 30-second operation with instant rollback if production behavior diverges from the shadow run.
How Long Does This Take to Build?
The honest answer: the core infrastructure is a week of engineering. Async mirroring queue, shadow runner, basic structural checks, a LLM-as-judge call, a dashboard. That's it. You don't need a dedicated MLOps platform to start.
What takes longer:
- PII policy and legal review: depends on your legal team's availability, not engineering hours
- Calibrating your judge prompts to your domain: 2–3 days of iteration
- Tuning thresholds on historical data: 1–2 days if you have logs, longer if you're starting from scratch
What's genuinely slow is accumulating enough shadow traffic to get statistically meaningful signal on a low-volume edge case. If a regression only affects 0.3% of traffic, you need enough samples to detect it with confidence. At 10k requests/day with 10% mirroring, you're looking at 1,000 shadow samples per day — enough to detect a 2% regression at 95% confidence in roughly two days. For rarer regressions, you need higher sample rates or longer windows.
The Contrarian Take
A shadow eval infrastructure is more valuable than another feature. One silent regression in a core user journey costs more in churn and trust than a month of feature velocity gains. Shadow evals let you ship fast because you've removed the anxiety from the promotion decision — the data tells you whether to ship, not your gut.
Teams that skip this layer aren't moving faster. They're just deferring the cost to their users.
What to Actually Do
-
This week: instrument your inference layer to duplicate requests to an async queue. Don't touch production response paths. Start logging shadow responses alongside production responses, even if you do nothing with them yet.
-
Next 3 days: wire a structural check for your most critical output format (JSON schema, required fields, language detection). Set up a simple dashboard counting failures. You now have your first regression signal.
-
Days 4–7: add LLM-as-judge scoring on sampled pairs using the prompt template above. Run it against current production output first — establish your baseline scores before you touch any candidate model.
-
Before your next model or prompt change: run the candidate through 24–48 hours of shadow traffic. Calibrate your embedding similarity threshold on prod-vs-prod pairs, then require all gates to pass before you promote. Make this a team norm, not a personal habit.
-
Once PII policy is clear: move from synthetic/internal traffic to live mirrored traffic. This is when shadow evals become genuinely powerful — and when the promotion decision stops feeling like a gamble.
The engineering to get started is trivial. The discipline to make it a gate — that's the actual work.
Working on something like this? I take on a few fractional-CTO and AI engagements at a time.
Get my AI playbooks — straight to your inbox
Practical notes on shipping production AI, scaling teams, and the calls a CTO actually has to make. A few times a month. No spam, no fluff.