← AI + Backend
MLOps & Production ML

A/B Testing & Canary Deployments for Models

Watch: "A Tale of Two Worlds: Canary-Testing for Both ML Models and Microservices - Jörg Schad" — CNCF [Cloud Native Computing Foundation]

Shipping a new model, prompt, or retrieval setup is a deployment problem and a measurement problem at the same time. A model that wins on a held-out test set can still lose in production because real traffic has a different mix, users react to the output, latency and cost change under load, and the offline metric was only a proxy for what the business cares about. Interviewers probe this topic because it separates people who "deploy the model" from people who can answer "how do you know the new version is better, and how do you limit the damage if it is worse?". For a Backend or AI engineer the working vocabulary is a ladder of increasingly risky exposure (offline evaluation, shadow, canary, A/B test, full rollout), plus enough statistics to avoid the classic mistakes: peeking, underpowered tests, broken randomisation, and declaring victory on the wrong metric. It pairs directly with model monitoring and drift detection (the signals a canary watches) and the ML model lifecycle and CI/CD (the pipeline that promotes or rolls back a version).

What Is A/B Testing?

A/B testing is a way of comparing two versions of something —€” a webpage, a recommendation model, a checkout flow —€” by showing each version to a different group of real users at the same time and measuring which one performs better on a specific goal, like clicks, purchases, or task completion. Because the two groups are chosen randomly and see everything else the same, any consistent difference in outcomes can be credited to the change itself rather than luck or timing.

What Is a Canary Deployment?

A canary deployment is a cautious way of releasing a new version of software: instead of switching every user over at once, you first send it to a tiny slice of traffic and watch closely for errors, crashes, or slowdowns before gradually increasing that slice. The name comes from the old mining practice of bringing a caged canary into a coal mine —€” if the canary showed signs of trouble, miners knew to get out before anyone else was harmed.

Where This Is Used

A Day-to-Day Example

Imagine a restaurant chain testing a new recipe. Instead of changing the menu at every location overnight, it first serves the new dish at one or two restaurants and asks staff to watch closely for complaints or send-backs (the canary). Once it is clearly not a disaster, it runs the new recipe alongside the old one in several markets, tracking which one customers reorder more often (the A/B test), before finally putting it on the menu everywhere.

Deep Dive: The Rollout Ladder — Offline, Shadow, Canary, A/B

The rollout ladder and shadow trafficoffline evalfixed set better?shadowruns? fast?canaryis it BROKEN?A/B testbetter? (confident)requestrouterPROD modelNEW modelresponse → userlogged + compared, NEVER returnedshadow mode:copy
Each stage answers a different question with more real exposure: offline eval asks if it's better on a fixed set, shadow whether it runs, how fast and how its output differs, canary whether it's broken, and A/B whether it's better with confidence. In shadow mode the new model gets a copy of live requests but its answers are only logged. What each stage can't tell you is in the table below.
new modelA/B: user-visible
Stage            Users see new output?  Answers the question              Cannot tell you
---------------  ---------------------  --------------------------------  ---------------------------
Offline eval     no                     "Is it better on a fixed set?"    real traffic, user reaction
Shadow           no (response dropped)  "Does it run, how fast, how does  whether users prefer it
                                         its output differ on live input?"
Canary (1-50%)   yes, a small slice     "Is it BROKEN? errors, latency,   whether it is *better*
                                         guardrail metrics"               (sample too small/short)
A/B test         yes, randomised split  "Is it better on the metric we    why (needs analysis), and
                                         care about, with confidence?"     long-term effects if short

request ---> router ---> PROD model  ---> response returned to user
                 |
                 +--(copy)--> NEW model  ---> logged and compared, NEVER returned   (shadow)

Each rung answers a different question at a different risk. Offline evaluation is cheap and repeatable but only as good as the dataset and metric; it cannot see feedback loops or a shifted input distribution. Shadow deployment sends a copy of live requests to the candidate and discards its answer, so users carry zero risk while you observe latency, resource use, error rate and how much its outputs differ from production. Its limits are practical: it doubles the compute for the mirrored fraction, it must not perform side effects (writes, emails, tool calls with real consequences), and because nobody sees the answer it produces no user-behaviour signal. A canary exposes a small real slice to the new version and is fundamentally a safety check; an A/B test is a decision experiment. Teams commonly run them back to back: shadow for plumbing, a short canary for safety, then a properly sized A/B test for the ship decision.

Deep Dive: Canary Mechanics — Traffic Percent, Guardrails, Automatic Rollback

Canary steps and blast radiuscanary ladder (hold each step until checks pass)1%5%25%50%100%auto-rollback: error z-score > threshold (enough events) OR p95 ratio > limitblast radius: 200 req/s, bad model caught after 3 mincanary at 5%1,800 requestsshipped at 100%36,000 requests
Traffic moves up in steps, each held until guardrail metrics (errors, latency, quality proxies, cost) match the concurrent baseline. Catching a bad model at 5% affects 20× fewer requests than shipping it to everyone.
canaryfull rollout
Ladder:   1%  ->  5%  ->  25%  ->  50%  ->  100%       (each step held until checks pass)

Guardrail metrics (compared against the concurrent baseline, not last week):
  reliability : 5xx / timeout rate, malformed-output rate (JSON/schema failures)
  latency     : p50, p95, p99  (and time-to-first-token for LLMs)
  quality     : proxy signals - thumbs-down rate, retry rate, escalation-to-human rate
  cost        : tokens or GPU-seconds per request
  business    : conversion / task completion (usually too slow to gate a canary alone)

Rollback rule (automatic):  if (error z-score > threshold AND enough events)
                               OR (p95 ratio > limit)  =>  route 100% back to baseline, page a human

Blast radius arithmetic (ILLUSTRATIVE): 200 req/s, canary at 5%, bad model detected after 3 min
   = 200 * 0.05 * 180 = 1,800 affected requests   vs   36,000 if it had shipped at 100%

A canary works because the damage is proportional to the traffic share times the time to detect. Two design decisions matter most. First, compare the canary against the concurrent baseline over the same window, not a historical number, so a traffic spike or upstream outage does not look like a model regression. Second, route stickily (per user or per session) so one person does not bounce between versions, which also makes their experience interpretable. The statistics are the trap: at 1% traffic with a 1% baseline error rate, 600 canary requests contain only about six errors, so an error rate that is truly 2.5% cannot be distinguished from noise yet. The simulation below shows this: the buggy canary passes the 1% stage because it has too few error events, and is caught at 5%. That is why rollback rules require a minimum event count, why early stages exist to catch catastrophic failures (crashes, schema breakage, 10x latency) rather than subtle ones, and why stricter thresholds are used when you evaluate repeatedly, since every check is another chance for a false alarm. A rule that is too sensitive rolls back healthy releases and teaches the team to ignore or bypass it; too lax and it protects nobody.

Deep Dive: A/B Test Design — Unit, Sample Size, Power, SRM, Novelty

Sample size grows with the square of precisionusers per arm, baseline 10% conversion (α = 0.05, power 0.8)+1.00 pt14,749+0.50 pt57,760 (~4×)+0.25 pt228,552 (~16×)halving the detectable effect ≈ 4× the users (n ∝ 1 / effect²)assignment: bucket = hash(salt + user_id) mod 10000; < 5000 → A, else B (sticky)10,000 users/day, 57,760/arm → ~11.6 days → run whole weeks
Detecting a smaller lift needs quadratically more users. Users are assigned by a salted hash so each always sees the same arm. The formula is below.
+1 pt+0.5 pt+0.25 pt
Assignment:  bucket = hash(experiment_salt + user_id) mod 10000     # deterministic, sticky
             bucket < 5000 -> control (A)   else -> treatment (B)      # salt differs per experiment

Sample size per arm, two proportions (alpha = 0.05 two-sided, power = 0.80):
   n  ~=  (z_alpha/2 + z_power)^2 * [ p1(1-p1) + p2(1-p2) ]  /  (p2 - p1)^2
   z_alpha/2 = 1.96, z_power = 0.84  -> (2.80)^2 = 7.85

   baseline 10% conversion:   detect +1.00 pt -> ~14,749/arm
                              detect +0.50 pt -> ~57,760/arm     (half the effect = ~4x the users)
                              detect +0.25 pt -> ~228,552/arm    (numbers from the script below)

Duration (ILLUSTRATIVE): 10,000 eligible users/day, 50/50 split, need 57,760/arm
   -> 115,520 users total -> ~11.6 days; run whole weeks so weekday/weekend mix is balanced

The randomisation unit decides what "independent" means. Randomise by user when the experience should be consistent across visits and outcomes are per user; randomise by request only when requests really are independent and the treatment has no memory (a stateless classifier), otherwise a user sees both versions and effects contaminate each other. Watch for interference: users who share a marketplace, a shared cache, a rate-limited GPU pool or a social graph can affect each other, so a "win" for B may partly come at A's expense. Power is the probability of detecting a real effect of a chosen size (the minimum detectable effect, MDE); the sample-size formula shows the cost of sensitivity, because required n scales with 1/MDE squared. Sizing before you start is what stops the "let it run until it is significant" habit. Two guards should run on every experiment. A sample-ratio mismatch (SRM) check compares observed arm sizes with the intended split using a chi-square test; a tiny p-value (often below 0.001) means assignment or logging is broken (bots, a crashing variant dropping events, a redirect losing users), and the metric result should not be trusted. And beware novelty and primacy effects: a new UI or response style can get a temporary bump from curiosity, or a temporary dip while users adapt, so run long enough to see the effect settle and consider looking at only new versus only returning users separately.

Deep Dive: Peeking and Multiple Comparisons

Peeking inflates false positivesA/A tests (no real difference), 2,000 simulated experimentslook once at end4.9% false positivespeek 10×, stop early19.2% (~4× inflated)α = 5%20 metrics, no effect: P(≥1 false positive) = 1 − 0.95²⁰ = 64%fixes: fixed horizon · sequential tests · Bonferroni / BH · one primary metric
Checking results repeatedly and stopping at the first p < 0.05 quadruples false positives, and testing many metrics almost guarantees one looks significant by chance.
as designedinflated
A/A tests (both arms identical, so any "significant" result is a false positive)
2,000 simulated experiments, 5,000 users per arm, alpha = 0.05:

   look once at the end                 ->  4.9% declared significant   (as designed)
   peek 10 times, stop at first p<0.05   -> 19.2% declared significant   (~4x inflated)

Multiple comparisons: test 20 independent metrics at alpha = 0.05 with no real effect
   P(at least one false positive) = 1 - 0.95^20 = 64%

Fixes:  fixed horizon (decide n up front)      |  alpha-spending / sequential tests (SPRT, group-sequential)
        Bonferroni / Benjamini-Hochberg        |  one PRIMARY metric, the rest labelled secondary or guardrail

A p-value of 0.05 means "if there were no difference, a result this extreme would occur about 5% of the time for one pre-planned look". Each additional peek is another draw from the noise, and stopping at the first significant one turns a 5% error rate into the 19% the simulation measured. The same logic applies across metrics and segments: slice a flat experiment by ten countries and five devices and something will look like a win. The honest options are to fix the sample size and look once, to use a method designed for continuous monitoring (group-sequential or always-valid sequential tests), or to correct for the number of comparisons. Guardrail metrics are treated differently on purpose: for a guardrail such as latency you want sensitivity to harm, so you accept more false alarms, whereas the primary success metric is held to a stricter standard. Pre-registering the primary metric, the MDE, the duration and the decision rule before launch removes most of the temptation to rationalise afterwards.

Deep Dive: Interleaving, Bandits, and A/B for LLM Changes

Interleaving two rankersteam-draft interleaving: one list, items tagged by sourceranker Aa1a2a3a4ranker Bb1b2b3b4showna1b1a2b2…click = a vote for that rankerbandit (Thompson): sample Beta(s+1, f+1) per variant, send to the highest draw→ traffic drifts toward the winner
Instead of splitting users, interleaving shows every user one merged list and credits each click to the ranker that contributed the item, so each user is a paired comparison. Bandits shift traffic toward the better variant as evidence builds. LLM metrics are listed below.
ranker Aranker B
Team-draft interleaving (ranking): one result list, items from both rankers alternate
   ranker A: [a1 a2 a3 a4]     ranker B: [b1 b2 b3 b4]
   shown   : [a1 b1 a2 b2 ...] (duplicates removed; each item tagged with its source)
   click on a tagged item = a vote for that ranker -> every user is a paired comparison

Bandit (Thompson sampling): each variant has a Beta(success+1, fail+1) belief;
   sample from each, send the request to the highest draw  =>  traffic drifts to the winner

LLM change (prompt, model, retrieval, temperature)          Metric type
   task success / thumbs-up / escalation rate               primary (user or human-labelled)
   LLM-judge win rate on a fixed set                        offline gate (calibrate against humans)
   p95 latency, time-to-first-token                         guardrail
   cost per successful task, tokens per request             guardrail
   refusal rate, schema-valid rate, safety filter hits      guardrail

Interleaving suits ranking and recommendation: each user receives a merged list and the clicks are compared within the same user, which removes between-user variance, so it generally detects preferences with far fewer users than a split test. Its limits: it measures relative preference, not an absolute metric like revenue or latency, and it only applies where results can be merged. Bandits trade the clean answer for lower regret: they shift traffic toward the better arm as evidence accumulates, which is attractive for short-lived choices with many variants (headlines, prompt variants, which of several models to route to). But adaptive allocation complicates unbiased effect estimates, copes poorly with drifting rewards unless designed for it, and is a poor fit when you need a defensible effect size for a decision. For LLM changes the same discipline applies with extra hazards: outputs are stochastic (higher variance, so larger samples), the quality signal is often indirect, an LLM judge has known biases (position, length, preference for its own family's style) and should be calibrated against a human-labelled sample, and cost and latency belong in the decision as guardrails, not afterthoughts. A prompt edit is a model change and should be versioned and rolled out the same way; a slightly better answer that costs 40% more per successful task, or doubles p95 latency, may be a loss.

Key Pointers

How It Works

  1. Gate the candidate offline: run it on a frozen evaluation set and regression suite, and require it to beat or match the current model on the agreed metrics, including cost and latency.
  2. Deploy it beside production and mirror a fraction of live requests in shadow mode; check errors, latency, resource use and output differences, with side effects disabled.
  3. Start the canary at a small percentage with sticky routing; every few hundred requests compare error rate, latency percentiles and quality proxies with the concurrent baseline, and trigger automatic rollback if a guardrail rule fires.
  4. If the canary holds at each step (for example 1, 5, 25, 50 percent), run a sized A/B test: deterministic hash-based assignment, an SRM check on day one, and no early stopping unless a sequential method is in use.
  5. At the planned end, analyse the pre-registered primary metric with a confidence interval, check the guardrails and segment sanity, and only then decide to ship, iterate, or discard.
  6. Promote the winner to 100 percent, keep the old version warm for fast rollback for a defined period, and hand the ongoing signals to monitoring and drift detection.

Try It Yourself

# Part 1 - z-test, sample size, sample-ratio mismatch, and peeking inflation
# Python 3 stdlib only: z-test, sample size, SRM check, peeking inflation
import math, random
from statistics import NormalDist

N = NormalDist()

def two_prop_z(x1, n1, x2, n2):
    """Two-sided two-proportion z-test (pooled variance). Returns (diff, z, p)."""
    p1, p2 = x1 / n1, x2 / n2
    pool = (x1 + x2) / (n1 + n2)
    se = math.sqrt(pool * (1 - pool) * (1 / n1 + 1 / n2))
    z = (p2 - p1) / se
    return p2 - p1, z, 2 * (1 - N.cdf(abs(z)))

def sample_size_per_arm(p_base, mde_abs, alpha=0.05, power=0.80):
    """Approximate n per arm to detect an absolute lift `mde_abs` (two-sided)."""
    p2 = p_base + mde_abs
    za, zb = N.inv_cdf(1 - alpha / 2), N.inv_cdf(power)
    var = p_base * (1 - p_base) + p2 * (1 - p2)
    return math.ceil((za + zb) ** 2 * var / mde_abs ** 2)

# 1) A real difference: 10.0% vs 10.6% conversion, 20,000 users per arm
diff, z, p = two_prop_z(2000, 20000, 2120, 20000)
print(f"z-test: diff={diff:+.4f} z={z:.2f} p={p:.4f}")

# 2) Sample size: baseline 10%, detect +0.5 points absolute
for mde in (0.010, 0.005, 0.0025):
    print(f"baseline 10%, MDE +{mde*100:.2f} pts -> n/arm = {sample_size_per_arm(0.10, mde):,}")

# 3) Sample-ratio mismatch: intended 50/50, observed split
def srm_p(n_a, n_b):
    exp = (n_a + n_b) / 2
    chi2 = (n_a - exp) ** 2 / exp + (n_b - exp) ** 2 / exp   # df = 1
    return chi2, 2 * (1 - N.cdf(math.sqrt(chi2)))
for a, b in ((50_120, 49_880), (50_900, 49_100)):
    chi2, pv = srm_p(a, b)
    print(f"SRM check {a:,}/{b:,}: chi2={chi2:.2f} p={pv:.2g} -> {'SRM! do not trust' if pv < 0.001 else 'ok'}")

# 4) Peeking: A/A tests (NO true difference). Look 10 times vs once at the end.
random.seed(42)
def aa_test(n_final=5000, p=0.10, looks=10):
    a = b = 0
    step = n_final // looks
    hit_any = False
    for i in range(1, n_final + 1):
        a += random.random() < p
        b += random.random() < p
        if i % step == 0:
            _, _, pv = two_prop_z(a, i, b, i)
            if pv < 0.05:
                hit_any = True
    return hit_any, pv < 0.05        # (any peek significant, final-only significant)

runs = 2000
peek = final = 0
for _ in range(runs):
    any_hit, fin = aa_test()
    peek += any_hit; final += fin
print(f"A/A false positives over {runs} tests: single look {final/runs:.1%}, 10 peeks {peek/runs:.1%}")

# Real output from python3:
# z-test: diff=+0.0060 z=1.97 p=0.0484
# baseline 10%, MDE +1.00 pts -> n/arm = 14,749
# baseline 10%, MDE +0.50 pts -> n/arm = 57,760
# baseline 10%, MDE +0.25 pts -> n/arm = 228,552
# SRM check 50,120/49,880: chi2=0.58 p=0.45 -> ok
# SRM check 50,900/49,100: chi2=32.40 p=1.3e-08 -> SRM! do not trust
# A/A false positives over 2000 tests: single look 4.9%, 10 peeks 19.2%
# Part 2 - a canary with an automatic rollback rule (three seeded scenarios)
# Python 3 stdlib only: canary rollout with an automatic rollback rule
import math, random

STAGES = [1, 5, 25, 50]          # % to the canary (100% = promotion; no baseline left to compare)
REQS_PER_STAGE = 60_000          # total requests seen during each stage
CHECK_EVERY = 500                # evaluate guardrails every N canary requests
MIN_CANARY_ERRORS = 20           # do not judge an error rate from a handful of events
Z_ROLLBACK = 3.0                 # one-sided z on error rate (strict, because we look many times)
P95_LIMIT = 1.25                 # roll back if canary p95 latency > 1.25x baseline p95

def p95(xs):
    xs = sorted(xs)
    return xs[int(0.95 * (len(xs) - 1))]

def canary(base_err, can_err, base_mu, can_mu, seed, verbose=True):
    rng = random.Random(seed)
    base_lat = [rng.lognormvariate(base_mu, 0.4) for _ in range(2000)]  # baseline latency reference (ms)
    for pct in STAGES:
        ce = cn = be = bn = 0
        lat = []
        for _ in range(REQS_PER_STAGE):
            if rng.random() * 100 < pct:                                 # routed to canary
                cn += 1
                ce += rng.random() < can_err
                lat.append(rng.lognormvariate(can_mu, 0.4))
            else:
                bn += 1
                be += rng.random() < base_err
            if cn and cn % CHECK_EVERY == 0 and len(lat) == cn:          # just crossed a check boundary
                pc, pb = ce / cn, be / bn
                pool = (ce + be) / (cn + bn)
                se = math.sqrt(pool * (1 - pool) * (1 / cn + 1 / bn))
                z = (pc - pb) / se
                ratio = p95(lat) / p95(base_lat)
                bad_err = z > Z_ROLLBACK and ce >= MIN_CANARY_ERRORS
                if bad_err or ratio > P95_LIMIT:
                    if verbose:
                        why = f"error z={z:.1f}" if bad_err else f"p95 ratio={ratio:.2f}"
                        print(f"  stage {pct:>3}%: ROLLBACK after {cn} canary reqs ({why}); "
                              f"canary err {pc:.2%} vs base {pb:.2%}")
                    return False
        if verbose:
            print(f"  stage {pct:>3}%: ok  canary reqs={cn:>6}  err {ce/cn:.2%} vs {be/bn:.2%}  "
                  f"p95 ratio {p95(lat)/p95(base_lat):.2f}")
    if verbose:
        print("  all stages passed -> promote to 100%")
    return True

for name, args, seed in (("healthy canary",     (0.010, 0.010, 4.0, 4.00), 1),
                         ("error-rate bug",     (0.010, 0.025, 4.0, 4.00), 2),
                         ("latency regression", (0.010, 0.010, 4.0, 4.35), 3)):
    print(f"--- {name} ---")
    canary(*args, seed=seed)

# How often does the rule wrongly kill a HEALTHY canary? (100 seeded runs)
false_rb = sum(not canary(0.010, 0.010, 4.0, 4.0, seed=1000 + s, verbose=False) for s in range(100))
print(f"healthy canary rolled back by mistake: {false_rb}/100 runs")

# Real output from python3:
# --- healthy canary ---
#   stage   1%: ok  canary reqs=   610  err 1.48% vs 1.01%  p95 ratio 1.02
#   stage   5%: ok  canary reqs=  3010  err 1.13% vs 0.95%  p95 ratio 0.99
#   stage  25%: ok  canary reqs= 14881  err 1.01% vs 1.00%  p95 ratio 1.03
#   stage  50%: ok  canary reqs= 29974  err 1.00% vs 1.00%  p95 ratio 1.03
#   all stages passed -> promote to 100%
# --- error-rate bug ---
#   stage   1%: ok  canary reqs=   608  err 2.14% vs 0.99%  p95 ratio 1.03
#   stage   5%: ROLLBACK after 1000 canary reqs (error z=3.8); canary err 2.50% vs base 1.16%
# --- latency regression ---
#   stage   1%: ROLLBACK after 500 canary reqs (p95 ratio=1.43); canary err 0.60% vs base 1.10%
# healthy canary rolled back by mistake: 1/100 runs

Read the output closely. The first experiment is "significant" only barely (p = 0.048), which is the normal shape of a real but small effect. The peeking result is the important one: identical arms, and yet ten looks flag a false winner about one time in five. In the canary run, the error-rate bug is invisible at 1% (under the minimum event count) and caught at 5%; the latency regression is caught immediately because latency is measured on every request, so it accumulates evidence far faster than a rare error. The final line is honest about the cost of automation: even a healthy canary is occasionally rolled back by mistake, and that false-alarm rate is a tunable trade-off against detection speed.

Interview Q&A

Q: Why is offline evaluation not enough to ship a model?

A: The offline set is a frozen sample with a proxy metric, so it cannot capture the live input mix, feedback loops, user reaction, or how latency and cost behave under load. A ranking model with better offline nDCG can still lower engagement because the metric did not reflect what users value. Offline is a cheap filter that stops obviously bad candidates; only live exposure tells you whether the business metric moved.

Q: Shadow, canary and A/B: how do they differ and when do you use each?

A: Shadow mirrors live requests to the candidate and discards its output, so it tests plumbing, latency and output differences at zero user risk but yields no user feedback. A canary gives a small real slice the new version to catch breakage quickly and roll back automatically. An A/B test gives a statistically sized randomised split to decide whether it is actually better. I normally chain them because each is cheap insurance for the next.

Q: What guardrail metrics and rollback rule would you set for a canary of a new fraud model?

A: Reliability (error and timeout rate), latency percentiles (p95, p99) against the concurrent baseline, decision-rate drift (approval and decline rate versus baseline), and downstream signals like manual-review volume; chargebacks are too delayed to gate a canary. Rollback triggers if the error-rate z-score exceeds a strict threshold with a minimum event count, or p95 exceeds, say, 1.25x baseline, or the decline rate moves outside a band. The trade-off is sensitivity versus false rollbacks, so thresholds are tuned on historical A/A comparisons.

Q: How do you decide the sample size for an A/B test?

A: Pick the baseline rate, the smallest effect worth acting on (MDE), alpha (commonly 0.05) and power (commonly 0.80), then use the two-proportion formula: n per arm is roughly (1.96 + 0.84)^2 times the summed variances divided by MDE squared. For a 10% baseline, detecting +1 point needs about 15k users per arm and +0.5 point about 58k, since halving the MDE roughly quadruples the sample. Then convert to days using eligible traffic and round to whole weeks.

Q: What is a sample-ratio mismatch and why does it invalidate a result?

A: It is when the observed split differs from the designed one by more than chance, detected with a chi-square test; 50,900 versus 49,100 on a 50/50 design gives p around 1e-8. It means users are being lost or misassigned non-randomly (bots, a variant that crashes before logging, a redirect), so the two groups are no longer comparable and the metric difference may be a selection artefact. You fix the pipeline and rerun rather than analyse the biased data.

Q: Why is peeking at an A/B test dangerous?

A: The 5% false-positive guarantee holds for a single pre-planned look; every extra look is another chance for noise to cross the threshold. In an A/A simulation of ten looks per test, about 19% of tests declared a winner versus about 5% for one look. If you need early stopping, use a sequential design such as group-sequential or SPRT-style boundaries, or spend alpha across planned looks.

Q: When would you use a bandit rather than an A/B test?

A: When the cost of showing a bad variant is high, the decision is short-lived, and there are many variants, such as picking among several prompt templates or headlines: Thompson sampling shifts traffic to better arms and lowers regret. When you need an unbiased effect size, a defensible decision record, or the reward drifts, a fixed-split A/B test is cleaner because adaptive allocation complicates inference.

Q: How would you A/B test a new prompt for an LLM support assistant?

A: Version the prompt, first gate it offline with a regression set and an LLM judge calibrated against a human-labelled sample, then canary it and split users by user id. Primary metric is task resolution or escalation rate; guardrails are p95 latency, time-to-first-token, cost per resolved conversation, refusal rate and schema validity. Outputs are stochastic so I size for higher variance and read per-segment results carefully rather than trusting one aggregate.

Follow-up: Your canary shows no problems at 5% but the model fails at 100%. How is that possible?

A: Several ways: load-dependent failures (GPU memory pressure, queueing, or a KV cache or connection pool that only saturates at full traffic), a shared cache or rate limit that the small slice never stressed, rare inputs that only appear at volume, or effects that take time (drift, data lag). Mitigations are load-testing the candidate at full scale in shadow, holding each canary step long enough to span peak periods, and keeping a fast automatic path back to the old version.

Follow-up: The test shows a significant win in one country and nothing overall. Ship to that country?

A: Not on that evidence alone. Slicing by many segments creates false positives (20 slices at 5% gives a 64% chance of at least one), so I would treat it as a hypothesis, then run a new pre-registered test in that market, or apply a multiple-comparison correction and check that the segment has a plausible mechanism. If it replicates, ship it there with monitoring.

Follow-up: How do you A/B test when users interact with each other, or when both arms share one GPU pool?

A: Interference breaks the independence assumption: if B consumes more GPU, A's latency worsens, so the measured gap can be exaggerated or hidden. Options are randomising by cluster (a region, a tenant, a graph community), time-slicing the treatment (switchback tests), or giving each arm reserved capacity. The trade-off is fewer independent units, so lower power, and you need more duration.

System Design Tie-in

In an ML platform design, the deployment path is usually a model registry, a router (service mesh, API gateway or feature-flag service) that assigns traffic by hashed user id, per-version metrics (error rate, latency percentiles, quality proxies, cost) tagged by model version, and a controller that promotes or rolls back automatically; the CI/CD pipeline owns the stages and the monitoring layer supplies the guardrail signals. State the estimation: at, say, 200 requests per second, a 5% canary is 10 requests per second, so a 1% error rate yields about 0.1 errors per second, which is why detecting subtle regressions takes minutes to hours rather than seconds, and why an A/B test on a 10% baseline metric needing about 58k users per arm takes roughly two weeks at 10k eligible users per day (all ILLUSTRATIVE assumptions). Call out infrastructure constraints an interviewer likes: keeping both model versions warm doubles GPU memory during the rollout, shadow traffic adds real inference cost, routing must be sticky and idempotent, and rollback must be a config flip rather than a redeploy. For LLM products, add prompt and model versions to every log line so a regression can be attributed, and gate on cost per successful task alongside quality.

Trade-offs

Every step of the ladder trades speed for confidence. Slow, staged rollouts limit blast radius but delay improvements and leave two versions running (more infrastructure, more operational complexity); fast rollouts ship value sooner but move failure risk to every user. Strict automatic rollback rules protect users but produce false alarms that erode trust, while permissive ones protect nobody. A/B tests give trustworthy causal answers but need traffic, time and discipline, and low-traffic products often cannot power them for small effects. Shadowing is safe but doubles compute and gives no behavioural feedback. Bandits reduce regret at the cost of clean inference; interleaving is efficient but only for ranking. The process is the wrong choice when the decision is trivially reversible and low stakes (just flip a flag and watch), when traffic is too small to reach any meaningful power (rely on offline evaluation, human review and staged rollout with strong monitoring instead), or when the metric you can measure quickly is a poor proxy for the outcome that matters, in which case a rigorous test of the wrong metric gives false confidence.