11.4 Shadow Traffic: mirror
Ship v2 by running it in the dark: every production request duplicated to the new code, its responses thrown away, the client never the wiser.
Advanced
What you will learn
What
mirrordoes — and the three guarantees that make it safe.The config: mirror location,
$request_uriin the target, body duplication.Proving it worked: the sink's log vs the client's response (real captures).
The production plays: dark launches, replay for load tests, migration shadowing — and the traps (writes! costs! PII!).
1. Three guarantees
mirror /_mirror; in a location duplicates the request: NGINX fires a
background subrequest with the same method, URI, and (via
mirror's
mirror_request_body on, the default) the same body at the location you name, while the original request proceeds untouched. The contract has exactly three clauses:
The client cannot feel it. The mirror runs beside the main content handler, never in front of it. The client's response — status, headers, timing — comes solely from the main path.
The mirror's response is discarded. Bytes, status, headers — thrown away. The mirror is a question you ask without waiting for (or caring about) the answer.
Mirror failures are invisible. If the sink is slow, down, or on fire, the main request continues. (Caveat: a mirror that hangs keeps worker resources until
proxy_timeout— mirror to something fast, or set a shortproxy_connect_timeoutin the mirror location.)
That trio is what makes dark launches possible: run v2 as the mirror of v1's traffic, diff the sink's behavior against reality, and promote with evidence instead of hope.
2. The config
The
:9303 server, complete:
server {
listen 9303;
server_name mirror.example.test;
location / {
mirror /_mirror; # fire the shadow copy
proxy_pass http://127.0.0.1:9002; # the REAL backend (b-node-2)
}
location = /_mirror {
internal; # shadow door: clients 404
# $request_uri preserves the client's ORIGINAL path+args even
# though this location's own URI is /_mirror — that's the trick
# that makes the copy faithful:
proxy_pass http://127.0.0.1:9090/mirror$request_uri;
}
}Two details carry the pattern:
/mirror$request_uri— the sink receives/mirror/orders/42?flag=1for a client request to/orders/42?flag=1. The fixed prefix is this sink's convention (it logs the path it received); the$request_uripart is the faithful original, args included. Without$request_uri, every mirrored request would arrive as/_mirror— a useless shadow.The mirror location is a normal location — you can give it its own
proxy_set_header(injectX-Shadow: trueso the sink knows), its own timeouts, evenproxy_cache(replay without hammering the shadow target).
3. The receipts
One POST through
:9303, then both sides of the story (real captures):
What the client saw — the main backend's echo, nothing else:
main: "server": "b-node-2",
main: "path": "/orders/42?flag=1",
What the sink logged — same request, one line of JSON (the sink,
b-mirror, appends to its log and answers 202 to nobody):
{"ts":"2026-09-23T01:07:37.932Z","method":"POST","path":"/mirror/orders/42?flag=1","body":"shadow-payload-123"}Read them together: same method (
POST), same path (
/orders/42? flag=1 inside the sink's
/mirror prefix),
same body (
shadow-payload-123 —
mirror_request_body on at work), and the client's response contained none of the sink's answer (
mirrored appears nowhere in what the client received — the capture above shows exactly that).
4. The production plays
Play | How mirror does it |
|---|---|
Dark launch | v2 deployed as the mirror target; run for hours/days; diff v2's responses (logged at the sink) against v1's actuals; promote v2 to primary when the diff is quiet |
Migration shadowing | new datastore fed the same writes if idempotent (see traps) or read-only traffic; compare read results |
Load rehearsal | mirror into a staging stack that mirrors production shape; you get real traffic patterns, not synthetic guesses |
Analytics without touching apps | tap the request stream at the edge (PII! see traps) |
The traps — this list is the lesson's fine print:
Never mirror non-idempotent writes blindly.
POST /paymentsmirrored = double charge. Mirror reads always; mirror writes only when the sink is explicitly designed for it (dedup keys, idempotency tokens) or when the "sink" only records (logs the request instead of executing it).Mirror traffic is real traffic — it costs the sink CPU, bandwidth, and money if the sink is a paid API.
rate-limit inside the mirror location, or sample (amapon a random variable → mirror only 10% — Lesson 7.4'ssplit_clientsis the sampler you want).PII multiplies. Every header/body you shadow is another copy of secrets to govern. Strip what the sink doesn't need (
proxy_set_header Authorization ""in the mirror location).Ordering is not guaranteed — the shadow may lag the main path. Diff-tolerant consumers only.
Bodies buffer in memory while duplicated — a 2 GB upload with
mirror_request_body onis read in full first (memory, or a temp file pastclient_body_buffer_size) and transmitted twice. For giant payloads, turnmirror_request_body offand shadow the metadata only.
Try it yourself
curl -si -X POST -d shadow-payload-123 http://127.0.0.1:9303/orders/42?flag=1— then check all four facts: the client saw the main backend, the client never saw the mirror's answer, the sink logged the request, the body was duplicated (the receipts above show each).Feel the invisibility:
killthe mirror sink, hit:9303— 200s keep flowing (watch the error log grumble about the mirror connect failure), then revive.Add
mirror /_mirror2;with a second target — mirrors compose; fan-out is one line each.
Q&A
Q: Does the mirror add latency to the client? A: The handoff itself is non-blocking — the main path doesn't wait. Second-order costs exist (worker cycles, bandwidth), which is why you sample or rate-limit the mirror, not the main path.
Q: Why is it called
mirror
and not
fork
? A: Marketing picked the right word: it's a
reflection — faithful, one-directional, and the mirror-world can never talk back.
Q: How is this different from the passive side of the house — like
access_log
tapping? A:
access_log records
what happened;
mirror
replays the request to a real service that can compute with it. One is observability (Lesson 12.1’s domain), the other is experimentation.
Q: Can the mirror target be HTTPS / another host? A: Any
proxy_pass target works — including TLS upstreams (Module 6's client-cert machinery if the sink demands mTLS).
Takeaways
mirrorduplicates method + URI + body as a background subrequest; the response is discarded and failures are invisible to the client — that's the whole safety contract.Faithfulness comes from
$request_uriin the mirror'sproxy_pass; the mirror location isinternaland fully configurable (headers, timeouts, caching, sampling).The canonical play is the dark launch: shadow v2, diff, then promote. The canonical sin is mirroring non-idempotent writes.
Mirror traffic is real traffic: costs, PII, and memory double — sample with
split_clients, strip secrets, mind the big bodies.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.