StackPractices
intermediate By Mathias Paulenko

A/B Testing Guide: Design, Statistics and Pitfalls

A practical guide to A/B testing: hypothesis design, sample sizing, statistical analysis, tooling, and the failure modes that invalidate experiments.

Overview

A/B testing (split testing) is a controlled experiment: you split traffic between the current version (control) and a candidate (treatment), measure a pre-defined metric, and let statistics decide. Done well, it replaces opinion-based product debates with evidence. Done badly — underpowered samples, peeking, contaminated groups — it produces confident wrong answers, which is worse than guessing.

The hard part isn’t the statistics — it’s the discipline: pre-registered hypotheses, honest sample sizes, clean randomization, and resisting the urge to call a winner on day three. What follows walks the full lifecycle, from hypothesis to decision, plus the tooling options and the failure modes that quietly invalidate results.

When to Use

A/B testing fits when:

  • You want to validate a hypothesis about user behavior
  • You need to measure the impact of a UI, copy, pricing or algorithm change
  • You’re choosing between two or more implementations and need data to decide
  • You want to optimize conversion funnels or engagement metrics
  • Your organization wants to move from opinion-based to data-driven decisions

It’s the wrong tool when:

  • You don’t have enough traffic. If you can’t reach the required sample size within about four weeks, the test either runs forever or ends underpowered. Use qualitative research, session replay or usability testing instead.
  • You can’t randomize cleanly. Changes users can compare directly (public pricing pages) or features that leak between users (shared carts, messaging) break the independence assumption. Use geo-experiments or switchback designs.
  • The effect you want to detect is smaller than your noise floor. If the minimum detectable effect is below what normal metric variance allows, fix measurement before experimenting — CUPED and triggered analysis help (see below).
  • The decision is irreversible. Don’t experiment on changes you can’t roll back; measure them with staged rollouts instead.

For progressive releases and experiment delivery rather than hypothesis testing, see Feature Flags: Progressive Release and Safe Experimentation.

Core Concepts

ConceptDescription
Control (A)The existing version, the baseline for comparison
Treatment (B)The new version being tested
Primary MetricThe key outcome measure that determines success
Guardrail MetricA metric that must not degrade (latency, error rate, revenue)
Statistical SignificanceProbability that the observed difference isn’t due to chance
P-ValueProbability of seeing the observed result if there’s no real difference
PowerProbability of detecting a true effect when it exists
Minimum Detectable Effect (MDE)Smallest meaningful difference you want to detect
Sample Ratio Mismatch (SRM)When observed group sizes diverge from the expected split — almost always a bug

Designing Experiment Metrics

Metric choice decides whether the experiment can answer the question at all. A good primary metric has four properties:

  • Sensitive: it actually moves when the treatment works. “Checkout completion rate” responds to a shorter checkout; “total revenue per month” responds to everything and is too noisy for most tests.
  • Attributable: the change can plausibly affect it. A homepage redesign can’t move retention 30 days out — don’t measure it there.
  • Measurable at scale: you get clean, complete data for both groups without manual work.
  • Aligned with the business: the metric proxies something the company cares about. Click-through on a misleading button is easy to lift and worth nothing.

Rates and means behave differently. Proportion metrics (conversion rate, click-through) fit the z-test shown below. Continuous metrics (revenue per user, session duration) are usually right-skewed — a few whales dominate the mean — so use a two-sample t-test only if the distribution is roughly normal, or Mann-Whitney U / bootstrap methods otherwise. Ratio metrics (revenue per session, items per order) need delta-method variance or ratio-aware test support in your platform.

Guardrail metrics exist to veto. Pick the small set that would make you stop the experiment regardless of the primary result: error rate, p95 latency, refund rate, unsubscribe rate. Three to five is typical; a guardrail that never fires is fine, a missing guardrail that should have fired is an incident.

Teams running many experiments often consolidate into an Overall Evaluation Criterion (OEC) — a weighted composite of the metrics that matter — so every experiment rolls up to one decision number instead of negotiating metric trade-offs test by test.

How an A/B Test Works

Every experiment follows the same loop: form a hypothesis, size the sample, randomize users, collect data, analyze, and decide. The randomization step is where most experiments silently break — the statistics only work if control and treatment are truly comparable groups.

Lifecycle of an A/B test: define the hypothesis and MDE, size the sample, randomize users, run full weekly cycles, analyze with a z-test and guardrails, then ship or iterate.

Two things to notice about the loop. First, the sample size calculation happens before launch — it’s what makes “run until significant” invalid as a stopping rule. Second, the final gate isn’t just statistical significance: a real lift that’s too small to justify implementation cost is still a “don’t ship”.

Step-by-Step A/B Testing

1. Define the Hypothesis

A good hypothesis is specific, measurable, and falsifiable. If you can’t state what evidence would prove you wrong, you don’t have a hypothesis — you have a hope.

# Hypothesis Template

**We believe** that [change]
**will result in** [metric improvement]
**for** [user segment]
**because** [reasoning based on data/observation]

# Example
We believe that reducing checkout steps from 5 to 3
will result in a 5% increase in checkout completion rate
for mobile users
because analytics show 40% drop-off at step 4 on mobile.

Before launch, write down:

  • Primary metric — the single metric that decides the experiment. One, not three.
  • Guardrail metrics — things that must not degrade: error rate, p95 latency, refund rate, revenue per user. Define kill thresholds upfront (“stop the test if p95 grows >10%”).
  • Target population — which users are eligible, and how you’ll handle logged-out traffic.
  • MDE — the smallest lift worth shipping. If a 0.5% improvement wouldn’t pay for the implementation, don’t size the experiment to detect it.
  • Duration — a fixed end date covering full weekly cycles, decided before you look at any data.

2. Calculate Sample Size

Sample size is determined by four inputs: baseline rate, MDE, significance level (alpha), and power. Get this wrong and everything downstream is noise.

# Example: Sample size calculation for A/B test
import scipy.stats as stats

def calculate_sample_size(
    baseline_rate: float,     # Current conversion rate
    mde: float,               # Minimum detectable effect (absolute)
    alpha: float = 0.05,      # Significance level
    power: float = 0.80,      # Statistical power
    ratio: float = 1.0        # Ratio of treatment to control
) -> int:
    """
    Calculate required sample size per group for a two-proportion test.
    """
    p1 = baseline_rate
    p2 = baseline_rate + mde

    z_alpha = stats.norm.ppf(1 - alpha / 2)
    z_beta = stats.norm.ppf(power)

    pooled_p = (p1 + ratio * p2) / (1 + ratio)

    numerator = (
        z_alpha * (pooled_p * (1 - pooled_p) * (1 + 1/ratio)) ** 0.5 +
        z_beta * (p1 * (1 - p1) + p2 * (1 - p2) / ratio) ** 0.5
    ) ** 2

    denominator = (p1 - p2) ** 2

    return int(numerator / denominator) + 1

# Example: 20% baseline conversion, want to detect 2% absolute improvement
sample_size = calculate_sample_size(
    baseline_rate=0.20,
    mde=0.02,
    alpha=0.05,
    power=0.80
)
print(f"Required sample size per group: {sample_size}")
# Output: ~6,500 users per group

What moves the number:

  • Baseline rate: rates near 50% have maximum variance; very low rates (a 1% conversion) need huge samples to detect small absolute changes.
  • MDE: the sample size grows with the square of precision — halving the MDE roughly quadruples the required sample.
  • Alpha: 0.05 is standard; use 0.01 for high-stakes, hard-to-reverse changes.
  • Power: 0.80 means you’d still miss a real effect 20% of the time. Don’t go below it.

If the required sample exceeds your traffic, don’t shrink the MDE to make it fit — increase the MDE by testing a bolder change, or accept that A/B testing isn’t viable on that surface yet. For quick checks, Evan Miller’s calculator is the standard reference.

3. Implement Assignment

Assign users deterministically so each user sees the same version for the whole experiment:

# Example: Consistent user assignment
import hashlib

def get_experiment_group(user_id: str, experiment_name: str, num_groups: int = 2) -> str:
    """
    Deterministically assign user to experiment group based on hash.
    Ensures the same user always gets the same assignment.
    """
    hash_input = f"{experiment_name}:{user_id}"
    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
    bucket = hash_value % num_groups

    groups = ["control", "treatment"] if num_groups == 2 else [f"group_{i}" for i in range(num_groups)]
    return groups[bucket]

# Usage
user_id = "user-12345"
experiment = "checkout-redesign"
group = get_experiment_group(user_id, experiment)
print(f"User assigned to: {group}")

# Render appropriate UI
if group == "treatment":
    render_new_checkout()
else:
    render_old_checkout()

Assignment has four requirements:

  • Random: every eligible user has an equal chance of each group.
  • Consistent: the same user always sees the same version — including across sessions and devices, which means keying on a stable user ID, not a cookie.
  • Independent: assignment to experiment A can’t correlate with assignment to experiment B; include the experiment name in the hash input, as above.
  • Sticky: the assignment persists for the whole experiment. Re-randomizing mid-flight contaminates both groups.

Before trusting your bucketing, run an A/A test: split users into two identical groups and confirm that group sizes and all metrics match within noise. If an A/A test shows “significant” differences, your randomization or instrumentation is broken — fix it before running real tests.

4. Run the Experiment

Collect data while maintaining experiment integrity. The two rules that matter most: don’t peek, and don’t touch a running experiment.

CheckpointAction
Day 1Verify randomization (group sizes match the expected split) and that events fire for both variants
Day 3Check instrumentation health and guardrail metrics, not primary-metric significance
MidpointResist peeking at p-values; only check that data quality holds
End dateRun the analysis on the pre-committed sample
Post-analysisSegment by device, geography, and user type before deciding
# Example: Experiment result analysis (two-proportion z-test)
from statsmodels.stats.proportion import proportions_ztest

def analyze_experiment(control_converted: int, control_total: int,
                       treatment_converted: int, treatment_total: int) -> dict:
    control_rate = control_converted / control_total
    treatment_rate = treatment_converted / treatment_total

    _, p_value = proportions_ztest(
        [control_converted, treatment_converted],
        [control_total, treatment_total]
    )

    relative_lift = (treatment_rate - control_rate) / control_rate

    return {
        'control_rate': control_rate,
        'treatment_rate': treatment_rate,
        'relative_lift': relative_lift,
        'p_value': p_value,
        'significant': p_value < 0.05
    }

proportions_ztest lives in statsmodels, not scipy — the wrong import is the most common bug in homegrown analysis scripts. For continuous metrics (revenue, time-on-page) use a two-sample t-test or Mann-Whitney U for skewed distributions; see the statsmodels docs.

Monitoring during the run is about health, not results — error rates, latency, and event volume are covered well by the practices in the observability guide.

5. Interpret Results

Decide on statistical and practical significance:

# Result Interpretation Framework

## Statistical Significance
- p-value < 0.05: result is statistically significant
- p-value >= 0.05: not enough evidence to reject the null hypothesis

## Practical Significance
- Is the lift large enough to justify implementation cost?
- Does the lift persist across segments (new vs returning, mobile vs desktop)?
- Are guardrail metrics healthy?

## Decision Matrix
| Statistically Significant | Practically Significant | Decision |
|---------------------------|-------------------------|----------|
| Yes | Yes | Ship it |
| Yes | No | Don't ship — the cost exceeds the benefit |
| No | Yes | Run longer or increase the sample next time |
| No | No | Don't ship; document the learning |

Two habits that separate mature experiment programs from cargo-cult ones. First, report confidence intervals, not just the p-value — “between +0.3% and +4.1% conversion” is a decision input; “p=0.03” isn’t. Second, write down the decision and rationale for every experiment, including inconclusive ones — that’s the institutional memory that prevents re-running the same test a year later.

Choosing Experimentation Tooling

The build-vs-buy decision hinges on where your data lives and who runs the experiments:

ToolTypeBest for
DIY (hash + warehouse)CustomSmall teams, few experiments, full control. The code above is most of what you need — the cost is building trustworthy analysis and an experiment registry.
GrowthBookOpen source, warehouse-nativeTeams that want self-hosted or warehouse-connected experimentation with SQL-based metrics and Bayesian/frequentist engines.
StatsigSaaS or warehouse-connectedProduct experiments at scale; built-in sequential testing (mSPRT), feature gates, and strong diagnostics like automatic SRM detection.
EppoWarehouse-native SaaSOrganizations whose metrics already live in the warehouse and want analysis to run there too.
Optimizely / VWOSaaS with visual editorMarketing and website experiments where non-engineers need to create variants without deploys.

A few guidelines from teams that have made this choice:

  • Start simple if your experiment volume is low — a hash function plus a query beats a platform nobody logs into.
  • If you buy, prioritize automatic SRM detection and sequential testing support; those two features prevent the two most common ways experiments go wrong.
  • Warehouse-native tools keep analysis next to your real metrics and avoid maintaining a parallel event pipeline.
  • Whatever you choose, the experiment should be documented in one registry: hypothesis, owner, dates, decision. Experimentation without a registry becomes folklore within months.

When Experiments Go Wrong

These are the failure modes that invalidate results without throwing an error:

  • Sample ratio mismatch (SRM): you configured a 50/50 split but observe 51.2/48.8 with thousands of users. That gap is almost never chance — it signals a randomization bug, bot traffic, or telemetry loss in one variant. Check it with a chi-square test on group counts; Statsig and similar tools alert on it automatically. If you see SRM, stop and fix the plumbing — nothing downstream is valid.
  • Peeking: checking significance daily turns a 5% false-positive rate into roughly 20-30%. Either commit to a fixed end date or use sequential testing methods designed for continuous monitoring.
  • Instrumentation bugs: the treatment “loses” because its events never fired. Validate tracking in an A/A test before launch, and compare event volume per user across groups, not just totals.
  • Interaction effects: two simultaneous experiments on the same surface contaminate each other. Use mutually exclusive layers, or accept the noise only if the experiments touch genuinely independent metrics.
  • Novelty and primacy effects: new features get an engagement spike from curiosity; returning users punish unfamiliar changes for a week. Segment new vs returning users — if the lift only exists in one group, wait.
  • Guardrail degradation: conversion went up 3% while error rate doubled. Without pre-defined guardrails you ship a regression. If any guardrail breaches its kill threshold, end the test regardless of the primary metric.
  • Seasonality and external events: a test running over Black Friday or an outage measures the event, not your change. Run full weekly cycles and record external events in the experiment log.

Reducing Variance and Running Faster Tests

Once the basics are solid, these techniques buy you sensitivity without more traffic:

  • CUPED (Controlled-experiment Using Pre-Experiment Data): adjust each user’s metric by their pre-experiment behavior to remove between-user variance. Microsoft reported ~50% variance reduction on many metrics, which is like doubling your traffic for free. Every serious platform supports it; see the original Microsoft Research publication.
  • Triggered analysis: only include users who could actually see the change. Analyzing your full user base when the treatment only affects the checkout page dilutes the effect into noise.
  • Stratified assignment: force balance on key covariates (country, plan tier, device) at assignment time instead of hoping randomization produces comparable groups.
  • Cluster randomization: when users interact (social features, shared workspaces), randomize groups rather than individuals so treatment can’t leak through user-to-user contact.
  • Switchback designs: for marketplaces where treatment affects shared supply (pricing, dispatch algorithms), randomize time windows instead of users — the approach Uber and DoorDash use because user-level randomization is impossible there.

What Works

  • Run experiments for full weeks — a test that skips a weekend or starts mid-week inherits day-of-week bias that no sample size fixes.
  • Avoid peeking. Checking significance daily turns a 5% false-positive rate into roughly 20-30%.
  • Use one primary metric. The moment you allow three, every outcome can be framed as a win.
  • Pre-register the decision criteria — hypothesis, MDE, duration and kill thresholds written down before the first data point.
  • Run an A/A test on new infrastructure. It costs a week and catches broken bucketing before it produces fake wins.
  • Document every experiment — hypothesis, design, decision and reasoning. A year later, the registry is the only thing stopping you from re-running the same test.
  • Segment results before deciding. An aggregate win can hide a loss in a segment big enough to matter (new users, mobile, a specific market).

Common Mistakes

  • Testing more than two variants without correcting for multiple comparisons — use Bonferroni or sequential testing methods.
  • Underpowered experiments. Small samples can’t detect small but meaningful effects.
  • Reading a “no effect” result as proof there’s none. An underpowered null means the test couldn’t detect the effect — check the confidence interval’s upper bound before calling the change neutral.
  • Declaring victory on a post-hoc segment. If “mobile users in Canada” wasn’t a pre-registered hypothesis, it’s a lead for the next test, not a result.
  • Confusing novelty with preference. New capabilities get initial engagement spikes that fade.
  • Changing running experiments. Never modify the treatment mid-experiment — redeploy as a new test instead.
  • Rolling out to 100% the day a test ends. A lift measured on 5% of traffic can hide costs that only appear at scale — ship through a staged rollout.
  • Shipping on an inconclusive result because “it looked positive”. Flat results are also information; they mean the change wasn’t worth it.

Variants

  • A/B/n testing: test more than two variants (A/B/C/D). Requires correcting for multiple comparisons and much larger samples.
  • Multivariate testing (MVT): test combinations of changes to several elements at once (headline × image × button) to estimate interaction effects — different from simply adding more variants.
  • Sequential testing: analyze continuously without inflating the false-positive rate, using alpha-spending or mSPRT methods.
  • Bandit algorithms: dynamically shift traffic toward better-performing variants — good for short-lived optimization (headlines), weaker for clean causal measurement.
  • Holdout groups: keep a long-term control group to measure the cumulative impact of many shipped changes.
  • Geo-experiments: randomize by geography for changes that can’t be user-randomized, like pricing or offline marketing.

Further Reading

  • Evan Miller’s A/B testing tools: the sample size calculator and related articles are the standard reference for experiment math.
  • statsmodels documentation: the proportions_ztest reference for analyzing conversion metrics in Python.
  • CUPED paper: Microsoft’s Improving the Sensitivity of Online Controlled Experiments explains the variance-reduction technique every major platform uses.
  • Trustworthy Online Controlled Experiments (Kohavi, Tang, Xu — Cambridge University Press): the field’s reference book, drawn from experiments at Bing, Google and LinkedIn.
  • GrowthBook docs: the open-source platform’s documentation is a practical reference for experiment configuration and metric design.
  • Companion files: the a-b-testing-guide companion repo packages the sample-size calculator, assignment function, analysis script and hypothesis template from this guide as standalone files.

Conclusion

A/B testing turns product development from opinion-driven to evidence-driven — but only when the discipline holds. The methodology is simple; the rigor isn’t. Pre-register your hypothesis and decision criteria, size the sample honestly, verify the plumbing with A/A tests and SRM checks, and treat every inconclusive result as a documented answer, not a failed experiment. Teams that do this compound their learnings; teams that peek and ship on vibes get confidently wrong answers.

Frequently Asked Questions

How long should an A/B test run?

Minimum 1-2 full weeks to capture weekly cycles, and until you reach the pre-calculated sample size — whichever comes later. Ending early because you hit significance mid-week is the most common way experiments produce fake wins.

What if my treatment shows a 50% improvement?

Assume a bug until proven otherwise. Lifts that large usually mean double-counted events, a broken control, or SRM — verify the instrumentation and group split before celebrating.

Can I run two or more A/B tests at the same time?

Yes, if the experiments are independent: different surfaces, different metrics, or mutually exclusive assignment layers. Overlapping tests on the same funnel create interaction effects that make both results unreadable.

What p-value threshold should I use?

0.05 for most product decisions, 0.01 for high-stakes or hard-to-reverse changes (pricing, core algorithms). Whatever you choose, commit to it before the experiment starts — picking the threshold after seeing results is p-hacking.

What is a sample ratio mismatch?

When observed group sizes diverge from the configured split beyond chance — say 51/49 instead of 50/50 on a large sample. It's almost always a symptom of broken randomization, bots, or lost telemetry, and it invalidates the whole experiment until fixed.

What if I don't have enough traffic for the required sample size?

Don't shrink the MDE to fit your traffic — increase it by testing a bolder change, extend the duration (up to ~4 weeks max before cohort drift becomes a problem), reduce variance with CUPED, or use qualitative methods on that surface until traffic grows.

Should I build or buy an experimentation platform?

Build if your volume is low and your metrics live in a warehouse — a hash function plus SQL gets you surprisingly far. Buy when experiment volume grows, non-engineers need to launch tests, or you want SRM detection and sequential testing without maintaining them.