Skip to content
NLEN
Illustration: Shadow deployments and dark launching of LLM API updates

Shadow deployments and dark launching of LLM API updates

By Ivo Donker — compiled with AI assistance (Claude & Gemini)

Updating an LLM component in a production infrastructure carries substantial risk. Where traditional microservices fail with clear HTTP status codes or stack traces, language models often fail silently: a new model checkpoint introduces subtle hallucinations, breaks JSON schemas, or responds with unpredictable tail latency. To mitigate these risks within the quality assurance of API environments, we look at advanced testing patterns. A comprehensive overview of measurement and logging can be found in the article on observability and logging for LLM applications, which covers the fundamentals of traceability.

Traditional offline evaluation sets and synthetic benchmarks rarely give a complete picture of the dynamic, often messy input from real end users. By mirroring production traffic (shadowing) or quietly running new models in parallel without the user seeing the output (dark launching), we test changes against real-world production data without any errors affecting the user experience.

The fundamental difference between shadow deployments and dark launching

Although the terms are often used interchangeably, shadow deployments and dark launching solve two different technical challenges in an LLM architecture.

In a shadow deployment (also called traffic mirroring) duplicates incoming HTTP requests at the infrastructure level. The primary request goes to the current production version of the model (the baseline). An exact copy of the request is forwarded asynchronously to the candidate version (the shadow). The shadow's response is never returned to the user, but stored in an audit or evaluation database. The purpose is purely observational: how does the candidate perform in terms of token usage, JSON conformance, semantic drift, and latency?

In a dark launch the new functionality or model already runs actively in the application's primary execution path, but the resulting data stays hidden from the user interface. Consider a scenario where a new summarization model generates the summary and writes it to the database, while the frontend still shows the summary from the old model. Dark launching lets engineers test the full chain, including downstream systems, cache invalidation, and database writes, under real load.

Property Shadow Deployment (Traffic Mirroring) Dark Launching
Calling layer Gateway / Proxy level (fire-and-forget) Application logic / Feature flag level
Impact on downstream systems None (evaluated in isolation) Full (databases, caches, dependencies)
User impact on failure Zero (errors are ignored) Low to medium (can strain backend resources)
Purpose Regression testing of model behavior and latency Validation of end-to-end system integration

Architecture of a shadow pipeline in an API gateway

The most robust place to set up shadow traffic is at the gateway layer. This way, individual microservices don't need to know anything about the evaluation experiments. For an in-depth analysis of setting up such a central hub, see the guide on hosting your own LLM gateway to keep routing and failover under your own control.

In a scalable architecture, the gateway receives a request from the client. The gateway forwards the synchronous request directly to the primary LLM provider. At the same time, a non-blocking dispatcher places a copy of the payload on an internal message broker (such as Redis Streams, Apache Kafka, or RabbitMQ). A dedicated worker pool picks up the messages and executes the call against the candidate API. This prevents delays at the shadow provider from affecting the Time To First Byte (TTFB) for the primary user.

// Voorbeeld: Express / Node.js non-blocking shadow proxy middleware
import { Request, Response, NextFunction } from 'express';
import { Queue } from 'bullmq';

const shadowQueue = new Queue('llm-shadow-evals', {
  connection: { host: 'localhost', port: 6379 }
});

export async function shadowTrafficMiddleware(req: Request, res: Response, next: NextFunction) {
  // Controleer of shadowing actief is voor dit endpoint
  const shadowTarget = req.headers['x-shadow-model'] || process.env.ACTIVE_SHADOW_MODEL;
  
  if (shadowTarget) {
    const shadowPayload = {
      originalUrl: req.originalUrl,
      headers: { ...req.headers, host: undefined },
      body: req.body,
      targetModel: shadowTarget,
      timestamp: Date.now(),
      requestId: req.headers['x-request-id'] || crypto.randomUUID()
    };

    // Plaats asynchroon op de wachtrij zonder op het resultaat te wachten
    shadowQueue.add('evaluate-shadow', shadowPayload, {
      removeOnComplete: true,
      attempts: 1 // Geen onnodige retries bij shadow traffic
    }).catch(err => {
      // Falen van schaduwverkeer mag NOOIT de hoofdthread blokkeren
      console.error('Fout bij enqueue van shadow payload:', err.message);
    });
  }

  next();
}

Handling mutating functions and idempotency

One of the biggest dangers when mirroring LLM calls arises when models use function calling or tool use. If a prompt contains an instruction to send an email, create an order, or mutate a record, blindly duplicating the request leads to duplicate transactions in external systems.

To prevent this, shadow workers must run strictly in a read-only sandbox or use mocked tool executions. When the candidate model generates a tool call (such as verstuur_factuur(klant_id, bedrag)), the shadow environment does not actually execute the action. Instead, the evaluation layer logs that the tool call was invoked with specific arguments, after which a synthetic success message is fed back to the model to validate the rest of the dialogue.

It's also essential to separate unique request identifiers. Anyone who wants to dig deeper into avoiding duplicate mutations and network errors should read the article on idempotency in LLM API calls to understand how deduplication keys work in distributed systems.

Evaluation metrics and automated regression detection

Once both the baseline and the shadow model have responded to the same production input record, the comparison phase begins. Because LLM output is inherently non-deterministic, a simple string comparison (such as an diff) is not sufficient. We use four categories of automated evaluation metrics:

1. Syntactic conformance: If the API requires structured JSON, a validator checks whether the shadow model's output validates against the required JSON schema. A 0.1% increase in schema validation errors is a direct block on promotion to production.

2. Latency and throughput: We measure P50, P95, and P99 latency and the number of tokens generated per second. Models that are cheaper on paper but show twice the P99 latency can cause SLA violations.

3. Semantic consistency: By computing embeddings of both responses and determining cosine similarity, we detect strong deviations in content. A low similarity score triggers further investigation.

4. LLM-as-a-Judge evaluation: For qualitative analysis, a referee model (for example, a larger model set to deterministic mode) can asynchronously judge both responses on accuracy, factuality, and tone. To see how this fits into a complete testing pyramid, read the guide on automated testing of LLM integrations for the right test setup.

For teams that want to structurally codify policy around non-deterministic responses and quality standards, the guide on setting up acceptance tests for non-deterministic output offers concrete methods for making acceptance criteria measurable.

# Voorbeeld: Asynchrone evaluatiescript voor semantische drift en schemaconformiteit
import json
import jsonschema
from sentence_transformers import util

def evalueer_shadow_paar(baseline_res: dict, shadow_res: dict, json_schema: dict, embedder) -> dict:
    rapport = {
        "schema_valide": False,
        "token_delta": shadow_res["usage"]["total_tokens"] - baseline_res["usage"]["total_tokens"],
        "latentie_delta_ms": shadow_res["latency_ms"] - baseline_res["latency_ms"],
        "semantische_overeenkomst": 0.0
    }
    
    # 1. Valideer schema
    try:
        shadow_data = json.loads(shadow_res["content"])
        jsonschema.validate(instance=shadow_data, schema=json_schema)
        rapport["schema_valide"] = True
    except (json.JSONDecodeError, jsonschema.ValidationError):
        rapport["schema_valide"] = False

    # 2. Bereken semantische vergelijkingsscore
    emb1 = embedder.encode(baseline_res["content"], convert_to_tensor=True)
    emb2 = embedder.encode(shadow_res["content"], convert_to_tensor=True)
    rapport["semantische_overeenkomst"] = float(util.cos_sim(emb1, emb2)[0][0])
    
    return rapport

Cost control and selective sampling

Duplicating production traffic theoretically doubles variable API costs, since two LLM calls are made for every incoming request. In large-scale systems processing millions of tokens per day, 100% traffic shadowing is not financially sustainable.

The solution lies in dynamic sampling. Instead of mirroring all requests, the gateway configures a sampling percentage (for example, 5% or 10% of total volume). Sampling can also be targeted at higher-risk traffic flows:

To prevent experimental shadow runs from unexpectedly blowing through budgets, strict monitoring is essential. See the overview on monitoring costs and budgets for guidelines on setting hard budget limits and alerts per API key.

Regression testing for prompt and model changes

When a provider updates a model version (for example, from a June checkpoint to an August checkpoint), prompts that worked flawlessly for months can suddenly fail. Systematic shadow runs catch this regression early.

In addition to live shadowing, it's advisable to periodically run regression tests on historical production data curated during earlier shadow sessions. To learn how to structurally prevent prompt optimizations in one area from causing quality loss in another, consult the article on regression testing for prompts to set up guardrails against silent quality decline.

Failure modes, bottlenecks, and mitigation strategies

Implementing shadow deployments introduces specific failure modes an engineer must recognize:

Failure mode 1: Rate limits at upstream providers. By doubling traffic, the organization risks hitting provider quotas (TPM/RPM), which can block production calls. Mitigation: Use separate API accounts or dedicated organization keys with their own rate limits for shadow workers.

Failure mode 2: Memory exhaustion in queues. When the shadow model is significantly slower than the production model, the message queue (broker) quickly fills up, leading to server crashes. Mitigation: Set a hard max_queue_size with a drop-oldest or drop-newest policy (lossy shadowing).

Failure mode 3: Data leaks in test environments. Production data regularly contains personal data (PII). If shadow logs are stored without restrictions for evaluation, a compliance risk arises. Mitigation: Apply inline pseudonymization to payloads before they are written to the evaluation database.

Bottleneck Impact Mitigation strategy Trade-off
Provider Rate Limits Production outage due to 429 status codes Separate API key pool and strictly separated quota Extra management of provider accounts
Queue congestion Server memory exhausted Drop policy when buffer limit is exceeded Loss of a percentage of evaluation data
Doubled costs Unexpected budget exhaustion Sampling based on hash of requestId (e.g. 5%) Longer time needed to reach statistical significance
Mutating Tool Calls Duplicate external actions (email, orders) Sandboxed mock handlers for shadow workers Extra mock logic required per tool definition

Phased rollout: From shadow to dark launch and canary

A robust release process for LLM updates combines the different techniques in a fixed step plan:

  1. Phase 1: Offline Evaluation. The new prompt or candidate model runs through a fixed test suite with curated test cases.
  2. Phase 2: Shadow Deployment (10% sampling). Traffic is duplicated asynchronously at the gateway level. Schema validation, semantic drift, and latency are measured over at least 10,000 requests.
  3. Phase 3: Dark Launch. The model is integrated into the backend application logic. Downstream storage and caching are verified without frontend display.
  4. Phase 4: Canary Release (5% → 25% → 100%). A small percentage of users actually see the output from the new model. Observability dashboards monitor user interactions, thumbs up/down, and error rates.
  5. Phase 5: Full Promotion & Archiving. The old model endpoint is phased out and the shadow pipeline is disabled.

By following this systematic approach, we transform LLM updates from risky, unpredictable operations into controlled, data-driven software releases.