Header-Based Dark Launches and Shadow Traffic
Shadow traffic is the only way to exercise new code with real production requests at zero user risk, and dark launching is the only way to put a feature in front of a chosen set of people before anyone else. Both are cheap to configure and both have one sharp edge — writes for shadowing, header trust for dark launches. This page covers the configuration and the edges.
Prerequisite concepts
This assumes the mechanism overview in traffic splitting and progressive delivery and header-match semantics from path and header-based routing.
Shadowing: real requests, discarded responses
# Envoy 1.32+ — mirror a fraction, mark it, and keep it out of traces
route:
cluster: orders_stable
request_mirror_policies:
- cluster: orders_shadow
runtime_fraction: { default_value: { numerator: 20, denominator: HUNDRED } }
trace_sampled: false
request_headers_to_add:
- header: { key: x-shadow, value: "false" }
# and on the shadow cluster, the header the target uses to know what it is:
# Envoy appends -shadow to the Host header automatically, which most frameworks log
The shadow target should be able to tell that it is a shadow. Envoy appends a suffix to the authority header, which is enough for the service to log differently, skip side effects, or write to a separate store. A shadow that cannot distinguish itself will emit alerts, send emails and charge cards.
Writes are the constraint. Mirroring a POST /payments charges twice. Three approaches work: mirror only safe methods, point the shadow at a separate data store, or have the shadow detect itself and short-circuit its side effects. What does not work is remembering to be careful.
Comparing what the shadow produced
Dark launching by header
# Envoy 1.32+ — the specific rule first; routes match in order
routes:
- match:
prefix: "/v2/checkout"
headers:
- name: x-feature-ring # set by the gateway from a verified claim
string_match: { exact: "internal" }
route: { cluster: checkout_next }
- match: { prefix: "/v2/checkout" }
route: { cluster: checkout_stable }
The header must not be one the caller can set, or the dark launch is a public launch for anyone who reads a blog post. Derive it from a verified claim, from a consumer record, or from an internal allow-list the gateway resolves — and strip the inbound header at the edge so an assertion from outside cannot survive.
One practical note on ordering: put the dark-launch rule above the general one and keep the two rules adjacent in the configuration file. A specific rule that drifts away from the general rule it overrides is how a dark launch quietly becomes permanent.
Decision matrix
| Goal | Mechanism |
|---|---|
| Prove new code survives production load | shadow traffic |
| Compare outputs before exposing anyone | shadow with response comparison |
| Let staff try it first | header targeting from a verified claim |
| Give one tenant early access | header or claim match on the tenant |
| Test a write path safely | shadow with a separate data store |
| Gather statistics | weighted split, not either of these |
Gotchas and failure signals
Shadow load is real load on shared dependencies. A shadow at a hundred percent doubles the traffic reaching a database both versions share.
Mirrored requests inherit the original’s headers, including authentication. The shadow target must be inside the same trust boundary, or you have just forwarded credentials somewhere new.
Tracing a shadow doubles span volume and makes traces confusing. Keep trace_sampled: false unless you are specifically debugging the shadow.
A dark-launch header left in place after the launch becomes an undocumented way to reach a code path nobody tests any more. Remove it as part of finishing the rollout.
Validation
- Shadow target can identify itself and skips side effects accordingly
- Only safe methods mirrored, or the shadow writes to a separate store
- Shared dependency capacity checked against the added shadow load
- Response comparison excludes expected-difference fields, by explicit list
- Dark-launch header derived from a verified source and stripped inbound
- The header and the shadow policy both removed when the rollout completes
- Someone owns the exclusion list used by the response comparison, and a field appearing in it for the first time is treated as a question rather than as noise to be suppressed permanently
FAQ
Does shadow traffic slow down the user’s request?
No. The gateway forwards to the stable version and returns that response without waiting for the shadow, whose response is read and discarded. A shadow that is slow, failing or completely wrong has no user-visible effect, which is exactly what makes shadowing the safest way to exercise new code with real traffic.
How do I shadow a write endpoint safely?
Three approaches work: mirror only safe methods, point the shadow at a separate data store, or have the shadow detect that it is a shadow and short-circuit its side effects. Envoy appends a suffix to the authority header, which most frameworks log and can branch on. What does not work is intending to be careful — a mirrored payment is a duplicate charge.
How do I stop response comparison drowning in noise?
Build an exclusion list of fields expected to differ — timestamps, request identifiers, anything generated — from the first hour of comparison output, and sample rather than diffing every response. Without the list the expected differences swamp the real ones and the comparison gets switched off within a day.
Can the dark-launch header come from the client?
Only if you are content for anyone to opt into untested code, which is occasionally the intent and usually is not. Derive it from a verified claim or a consumer record the gateway resolves, and strip the inbound header at the edge so an assertion from outside cannot survive. Remove the header entirely once the rollout finishes, or it becomes an undocumented path to code nobody tests.
Parent: Traffic Splitting & Progressive Delivery
Related
- Traffic Splitting & Progressive Delivery — where these two mechanisms sit among the four.
- Weighted Canary Routing with Envoy and Kong — the statistical mechanism these two complement.
- Path & Header-Based Routing — header match semantics and why caller-set values cannot be trusted.