<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: David Sert</title>
    <description>The latest articles on DEV Community by David Sert (@david_sert).</description>
    <link>https://dev.to/david_sert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032860%2F1f02fb3e-9524-4845-b086-ffe7d7d0f77b.jpg</url>
      <title>DEV Community: David Sert</title>
      <link>https://dev.to/david_sert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david_sert"/>
    <language>en</language>
    <item>
      <title>Sample Ratio Mismatch: Is Your A/B Test Broken?</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:10:45 +0000</pubDate>
      <link>https://dev.to/david_sert/sample-ratio-mismatch-is-your-ab-test-broken-2fe8</link>
      <guid>https://dev.to/david_sert/sample-ratio-mismatch-is-your-ab-test-broken-2fe8</guid>
      <description>&lt;p&gt;Your experiment finished, the dashboard shows a winner, and something still feels off. The traffic split you set to 50/50 is sitting at 47/53. Before you write up the result or ship the change, you need to answer one question: is the split you observed close enough to the split you designed, or is the test itself broken? That question has a precise, statistical answer, and getting it wrong is one of the most common ways a team ships a change based on a number that was never real. This is sample ratio mismatch, and it is the first thing a senior experimenter checks before looking at any lift.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sample ratio mismatch is
&lt;/h2&gt;

&lt;p&gt;A sample ratio mismatch (SRM) occurs when the number of visitors actually assigned to each variation differs from the split you intended by more than random chance can explain. You designed a 50/50 test; the engine delivered something that a fair coin, flipped that many times, would essentially never produce.&lt;/p&gt;

&lt;p&gt;The important word is &lt;em&gt;chance&lt;/em&gt;. You should never expect a perfectly even split. If you assign 20,000 visitors with a fair 50/50 rule, landing on exactly 10,000/10,000 is rare — the count wobbles around the target every time, the same way 10,000 coin flips almost never give exactly 5,000 heads. SRM is not "the split isn't perfect." SRM is "the split is &lt;em&gt;so&lt;/em&gt; far off that randomness is no longer a believable explanation."&lt;/p&gt;

&lt;p&gt;That distinction is why you cannot judge SRM by eye. Looking at 47/53 tells you the direction and size of the gap but nothing about how surprising it is, because surprise depends on how many visitors produced that gap. The same 47/53 is unremarkable at 400 visitors and a five-alarm fire at 400,000. You need a test that accounts for &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt;. The tool for that is the chi-square goodness-of-fit test, and OptiPilot's free &lt;a href="https://optipilot.com/tools/srm-checker" rel="noopener noreferrer"&gt;SRM Checker&lt;/a&gt; runs it for you — paste in your observed counts and expected split, and it returns the p-value and verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an SRM invalidates your results
&lt;/h2&gt;

&lt;p&gt;Randomized assignment is the entire reason an A/B test can claim causality. When you split traffic randomly, the two groups are, on average, identical in every way — same mix of devices, same proportion of loyal customers, same intent — &lt;em&gt;except&lt;/em&gt; the change you are testing. That balance is what lets you attribute any difference in outcomes to the variation rather than to a difference in who was in each group.&lt;/p&gt;

&lt;p&gt;An SRM is direct evidence that the randomization did not work as designed. If a mechanism pushed extra visitors into one variation, that same mechanism almost certainly pushed a &lt;em&gt;non-random&lt;/em&gt; kind of visitor. Redirects that time out drop the impatient and the slow-connection users. Bot filters that fire unevenly strip out a specific traffic source. In every case the groups are no longer comparable, so the lift you measured is a mix of the real treatment effect and a selection bias you cannot separate out.&lt;/p&gt;

&lt;p&gt;This is the point that trips up teams under deadline pressure: &lt;strong&gt;you cannot adjust your way out of an SRM.&lt;/strong&gt; There is no correction factor, no reweighting, no "the split was 45/55 so I'll scale the numbers." You do not know which visitors are missing or why, so you cannot model the bias. An SRM means the data is untrustworthy at the source. The only valid responses are to find and fix the cause, then rerun — or, if you can prove the cause is benign and non-differential, to document exactly why. Reading the &lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;Optimizely results page&lt;/a&gt; is meaningless until the split checks out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SRM is detected with a chi-square test
&lt;/h2&gt;

&lt;p&gt;The standard test is Pearson's chi-square goodness-of-fit. It compares the visitor counts you &lt;em&gt;observed&lt;/em&gt; in each variation against the counts you &lt;em&gt;expected&lt;/em&gt; under your intended split, and returns a p-value: the probability of seeing a gap at least this large if assignment were truly random.&lt;/p&gt;

&lt;p&gt;The statistic is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chi-square = sum over each variation of (observed - expected)^2 / expected

degrees of freedom (df) = number of variations - 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a two-arm test, df = 1. For a three-arm 34/33/33 test, df = 2, and so on. You compute the statistic, look up the p-value for that value and df, and compare it to a threshold.&lt;/p&gt;

&lt;p&gt;The industry alarm threshold is &lt;strong&gt;p &amp;lt; 0.001&lt;/strong&gt;. If the p-value falls below it, you treat the experiment as having an SRM and stop trusting the results until you find the cause. Some teams run stricter: Microsoft's experimentation platform uses &lt;strong&gt;0.0005&lt;/strong&gt;, and a few use 0.0001. The threshold is deliberately far tighter than the 0.05 you use for a metric result, for two reasons. First, you run this check on every experiment, so a loose threshold would drown you in false alarms. Second, SRM is a binary data-integrity gate, not an effect you are trying to measure — you want it to fire only when something is genuinely wrong. Note that a real SRM usually produces a p-value orders of magnitude below any of these thresholds, not a marginal miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked chi-square example
&lt;/h2&gt;

&lt;p&gt;Take a 50/50 test. You intended an even split; you observed 10,200 visitors in A and 9,800 in B, for 20,000 total. Under a fair split you expect 10,000 in each.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observed:  A = 10,200   B = 9,800     Total = 20,000
Expected:  A = 10,000   B = 10,000    (50% of 20,000 each)

chi-square = (10,200 - 10,000)^2 / 10,000 + (9,800 - 10,000)^2 / 10,000
           = (200)^2 / 10,000 + (-200)^2 / 10,000
           = 40,000 / 10,000 + 40,000 / 10,000
           = 4 + 4
           = 8.0

df = 2 variations - 1 = 1
p-value for chi-square = 8.0, df = 1  -&amp;gt;  p ~= 0.0047
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read that: p ≈ 0.0047 means a gap this large would happen about 5 times in 1,000 by pure chance. That clears the conventional 0.05 bar, so a naive check would panic. But it is &lt;strong&gt;above&lt;/strong&gt; the 0.001 SRM threshold, so by the standard you do not declare an SRM. It sits in the uncomfortable middle — worth a glance, not worth halting for. This is exactly why the strict threshold exists: a 2% wobble at this sample size is not yet strong enough evidence of a broken split.&lt;/p&gt;

&lt;p&gt;Now keep the &lt;em&gt;same 51/49 ratio&lt;/em&gt; but run 10x the traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observed:  A = 102,000   B = 98,000    Total = 200,000
Expected:  A = 100,000   B = 100,000

chi-square = (2,000)^2 / 100,000 + (-2,000)^2 / 100,000
           = 40 + 40
           = 80.0

df = 1
p-value for chi-square = 80.0, df = 1  -&amp;gt;  p ~= 3.7e-19  (effectively zero)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical proportions, wildly different verdict. At 20,000 visitors the 51/49 split is plausibly noise; at 200,000 it is a certainty that something is wrong. This is the single most important intuition about SRM: &lt;strong&gt;the ratio alone tells you nothing — the mismatch is a function of the ratio and the sample size together.&lt;/strong&gt; It is also why staring at the percentages on a dashboard will never substitute for the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  What causes sample ratio mismatch
&lt;/h2&gt;

&lt;p&gt;An SRM is a symptom, like a fever — it tells you something is wrong without telling you what. The KDD 2019 taxonomy from Microsoft and Booking.com researchers organizes the causes by &lt;em&gt;where in the pipeline&lt;/em&gt; the visitor count gets distorted. Walking that pipeline is how you narrow the search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Assignment] --&amp;gt; B[Exposure]
    B --&amp;gt; C[Logging]
    C --&amp;gt; D[Analysis]
    A -.-&amp;gt;|"faulty hash, bad IDs,&amp;lt;br/&amp;gt;uneven ramp, carryover"| A
    B -.-&amp;gt;|"redirect timeouts, flicker,&amp;lt;br/&amp;gt;variation errors, latency"| B
    C -.-&amp;gt;|"bot filters, dropped events,&amp;lt;br/&amp;gt;caching, ad blockers"| C
    D -.-&amp;gt;|"wrong trigger point,&amp;lt;br/&amp;gt;bad joins, segment filters"| D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Assignment-stage causes
&lt;/h3&gt;

&lt;p&gt;The split is wrong at the moment of bucketing. A faulty randomization or hash function, corrupted or reassigned visitor IDs, or carryover effects from a previous experiment that still tag returning users. Uneven &lt;em&gt;ramping&lt;/em&gt; belongs here too: if you launch variation B a day after A, or ramp them to different allocations, the counts diverge for a completely mundane reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposure-stage causes
&lt;/h3&gt;

&lt;p&gt;Assignment was fair, but visitors are lost between being bucketed and being counted. &lt;strong&gt;Redirect tests are the classic offender&lt;/strong&gt; — a visitor sent to a separate URL can close the tab before the redirect completes, so the event never fires and that visitor is never counted. Because the delay is not symmetric across variations, the counts drift. Client-side flicker and load races drop a variation when the page renders before the experiment code runs, latency in one variation lets impatient users leave first, and a variation that errors for a subset of browsers silently loses those visitors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging and analysis-stage causes
&lt;/h3&gt;

&lt;p&gt;The split was fine; the &lt;em&gt;measurement&lt;/em&gt; distorts it. Bot and spam filtering applied unevenly strips a traffic source from one arm. Ad blockers and tracking-prevention block the logging beacon for some visitors more than others. Caching or CDN behavior serves stale assignments. On the analysis side, choosing the wrong starting point — counting from a downstream event that already differs between variations rather than from first exposure — manufactures an SRM out of clean data. Bad table joins and delayed log arrival do the same.&lt;/p&gt;

&lt;p&gt;The most useful directional clue: excess visitors in the &lt;em&gt;treatment&lt;/em&gt; arm often points to logging or engagement changes (the new variant generates more events, so more one-time visitors get recorded), while missing visitors in treatment points to exposure failures like redirects, errors, or latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to diagnose an SRM
&lt;/h2&gt;

&lt;p&gt;Once the check fails, resist the urge to guess. Diagnosis is a process of localization: find the segment or stage where the mismatch lives, and the cause usually becomes obvious.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    S[SRM detected] --&amp;gt; Q1{Is it in every segment&amp;lt;br/&amp;gt;or just some?}
    Q1 --&amp;gt;|Isolated to one&amp;lt;br/&amp;gt;browser/device/source| Seg[Suspect that segment:&amp;lt;br/&amp;gt;rendering bug, bot source,&amp;lt;br/&amp;gt;ad blocker, redirect on that platform]
    Q1 --&amp;gt;|Spread evenly&amp;lt;br/&amp;gt;across all segments| Q2{Does the split break&amp;lt;br/&amp;gt;at assignment or later?}
    Q2 --&amp;gt;|Wrong at first exposure| Assign[Assignment layer:&amp;lt;br/&amp;gt;hashing, IDs, ramp timing,&amp;lt;br/&amp;gt;overlapping experiments]
    Q2 --&amp;gt;|Fine at exposure,&amp;lt;br/&amp;gt;off downstream| Q3{Redirect or heavy&amp;lt;br/&amp;gt;client-side render?}
    Q3 --&amp;gt;|Yes| Exp[Exposure loss:&amp;lt;br/&amp;gt;redirect timeouts, flicker,&amp;lt;br/&amp;gt;variation errors]
    Q3 --&amp;gt;|No| Log[Logging/analysis:&amp;lt;br/&amp;gt;bot filter, wrong trigger point,&amp;lt;br/&amp;gt;join error, caching]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Work through it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Segment the mismatch.&lt;/strong&gt; Break the counts down by browser, device, operating system, day, traffic source, and new-vs-returning. If the SRM collapses into one segment — say, only Safari, or only mobile, or only the day of a deploy — you have found it. A rendering bug, a browser that rejects redirects, or a bot wave from one source will all show up as a single hot segment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the stage where the ratio breaks.&lt;/strong&gt; Compare the split at assignment, at exposure, and at analysis. If bucketing is even but exposure is skewed, the loss is happening after assignment (redirects, errors, latency). If assignment itself is off, the problem is upstream in the hashing or ID logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the analysis trigger.&lt;/strong&gt; Confirm you are counting from first exposure, not from a downstream event that the treatment itself changed. This alone explains a large share of "SRMs" that are really analysis artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line up the timeline.&lt;/strong&gt; Map the onset of the mismatch against deploys, ramp changes, and marketing sends. SRM that starts precisely at a code push has a precise cause.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One caution specific to how detection works: run the SRM check on the &lt;em&gt;whole&lt;/em&gt; experiment population, not on filtered segments as a decision input. Segmenting is a diagnostic tool for finding the cause once an SRM is flagged — it is not a place to hunt for fresh imbalances to act on, because slicing enough ways will always turn up a low p-value by chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent SRM
&lt;/h2&gt;

&lt;p&gt;Prevention is cheaper than diagnosis. The teams that rarely see SRM have built these habits in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assign server-side where you can.&lt;/strong&gt; Server-side or edge assignment removes the flicker, redirect, and client-render failure modes that cause most exposure-stage mismatches. Client-side tests are more exposed and need tighter QA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket at the point of exposure.&lt;/strong&gt; Assign the visitor at the moment they are actually eligible to see the change, not at some earlier step that a variation may reach at a different rate. Assign-at-exposure keeps the counted population aligned with the treated population.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run an &lt;strong&gt;[&lt;/strong&gt;A/A test**](&lt;a href="https://optipilot.com/data/aa-testing" rel="noopener noreferrer"&gt;https://optipilot.com/data/aa-testing&lt;/a&gt;)&lt;/strong&gt; first.** An A/A test — two identical variants — should show &lt;em&gt;no&lt;/em&gt; SRM. If it does, your instrumentation is broken before you have tested anything real. It is the cheapest possible way to catch a pipeline problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put an SRM check in CI and pre-analysis.&lt;/strong&gt; Make the chi-square check an automated gate that runs continuously, not a manual step someone remembers at the end. Add it to your pre-launch &lt;a href="https://optipilot.com/tools/experiment-qa-checklist" rel="noopener noreferrer"&gt;experiment QA checklist&lt;/a&gt; so it is verified every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be careful with redirects and uneven ramps.&lt;/strong&gt; Prefer single-page variation over redirects when you can; when you cannot, expect and monitor a small imbalance. Ramp all variations together, and never adjust traffic mid-flight to "rebalance" counts — that corrupts the results further.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Optimizely detects SRM automatically
&lt;/h2&gt;

&lt;p&gt;Optimizely does not make you run this check by hand. Its Stats Engine A/B tests include automatic SRM detection with an &lt;strong&gt;Experiment Health&lt;/strong&gt; indicator on the results page: a green &lt;em&gt;Good&lt;/em&gt; status means no imbalance, and an &lt;em&gt;SRM detected&lt;/em&gt; status means the test has failed a traffic check and needs investigation before you trust it.&lt;/p&gt;

&lt;p&gt;One design detail matters for practitioners. Optimizely's detector does &lt;strong&gt;not&lt;/strong&gt; use a one-shot chi-square test. It uses a &lt;em&gt;sequential&lt;/em&gt; SRM (SSRM) method that checks traffic counts continuously as data arrives, so it can flag a problem within the first days of a launch — when stopping the test actually limits the damage — rather than only at the end. This is the same reasoning behind &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing and the peeking problem&lt;/a&gt;: a fixed-horizon test is only valid if you look once, whereas a sequential test is built to be monitored continuously. A retroactive end-of-experiment chi-square check has the opposite problem — by the time the totals even out, a transient mid-experiment SRM can be hidden entirely.&lt;/p&gt;

&lt;p&gt;A few operational limits are worth knowing. Optimizely's automatic detection applies to Stats Engine A/B tests with &lt;strong&gt;Manual&lt;/strong&gt; traffic distribution (not Stats Accelerator), running 45 days or less, with at least 1,000 visitors, and it evaluates only first decisions. It does not run on segmented results, and it does not check paused or archived experiments. So the manual chi-square check and the &lt;a href="https://optipilot.com/tools/srm-checker" rel="noopener noreferrer"&gt;SRM Checker&lt;/a&gt; still earn their place: for Stats Accelerator tests, for segment-level diagnosis once an alarm fires, and for any experiment run on a platform without built-in detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust the split before you trust the lift
&lt;/h2&gt;

&lt;p&gt;Sample ratio mismatch is the smoke detector of experimentation. It does not tell you what is on fire, but it tells you — reliably, and before you make an expensive decision — that you should not trust the numbers yet. The discipline is simple and non-negotiable: check the split with a chi-square test against a strict threshold, and if it fails, find and fix the cause rather than reasoning around it. A test with an SRM is not a weaker result; it is not a result at all.&lt;/p&gt;

&lt;p&gt;Make it the first thing you look at, not the last. Before you read a single metric, run your observed counts through the &lt;a href="https://optipilot.com/tools/srm-checker" rel="noopener noreferrer"&gt;SRM Checker&lt;/a&gt;. If the split holds, everything downstream — your lift, your confidence interval, your &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;variance-reduced estimates&lt;/a&gt; — rests on solid ground. If it doesn't, you have saved yourself from shipping a decision built on a broken experiment.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>datascience</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Prevent Optimizely from being blocked by ad-blockers using AWS</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:10:10 +0000</pubDate>
      <link>https://dev.to/david_sert/prevent-optimizely-from-being-blocked-by-ad-blockers-using-aws-17jn</link>
      <guid>https://dev.to/david_sert/prevent-optimizely-from-being-blocked-by-ad-blockers-using-aws-17jn</guid>
      <description>&lt;p&gt;Category: Privacy&lt;/p&gt;

&lt;p&gt;Some ad-blockers might block Optimizely from running. In this tutorial, we’ll see how we can prevent this from happening.&lt;/p&gt;

&lt;p&gt;Ad-blockers will often look at the domain the request is originating from and if it happens to be from a list of known analytics tools (such as Optimizely.com), the ad blocker will block the network request from happening.&lt;/p&gt;

&lt;p&gt;The key solution is to proxy requests via an API gateway which will forward all requests back to Optimizely.com.&lt;/p&gt;

&lt;p&gt;Here’s how.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create an AWS API Gateway
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
    subgraph Browser
        A[Your Website] --&amp;gt; B[Proxy Request]
    end

    subgraph AWS["AWS CloudFront"]
        B --&amp;gt; C[Your Domain cdn.yourdomain.com]
        C --&amp;gt; D[CloudFront Distribution]
    end

    subgraph Optimizely
        D --&amp;gt; E[cdn.optimizely.com]
        E --&amp;gt; F[Optimizely Snippet]
    end

    F --&amp;gt; A

    style AWS fill:#ff9900,color:#fff
    style Optimizely fill:#0037ff,color:#fff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head the &lt;a href="https://aws.amazon.com/" rel="noopener noreferrer"&gt;AWS Management Console&lt;/a&gt; then head to the API gateway section&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuqye7y5b7t3u3tj2y00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuqye7y5b7t3u3tj2y00.png" alt="The AWS API Gateway Section" width="799" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, click on &lt;em&gt;Create New API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4crhmbceul2zh40pskj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4crhmbceul2zh40pskj.png" alt="Click to create a new API" width="799" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select HTTP API as the API type:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv27xww81gdbd0t1qvp22.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv27xww81gdbd0t1qvp22.png" alt="Select API type" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2: Create the required routes&lt;/p&gt;

&lt;p&gt;Now we are going to create our API routes and where the API will forward the requests to.&lt;/p&gt;

&lt;p&gt;We’ll need 3 routes, depending on the Optimizely product you use:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One that allows us to retrieve the Optimizely snippet (if you are using Web). The snippet is fetched via a &lt;code&gt;GET&lt;/code&gt; request done to &lt;a href="https://cdn.optimizely.com" rel="noopener noreferrer"&gt;https://cdn.optimizely.com&lt;/a&gt;. You can find the full snippet URL inside your Project settings.&lt;/li&gt;
&lt;li&gt;One that allows us to send decision &amp;amp; conversion events (logx.optimizely.com). This route will be fetched via a &lt;code&gt;POST&lt;/code&gt; request to &lt;a href="https://logx.optimizely.com/v1/event" rel="noopener noreferrer"&gt;https://logx.optimizely.com/v1/event&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;One that allows us to retrieve the Optimizely datafile (if you are using Full Stack). This route will be fetched via a &lt;code&gt;GET&lt;/code&gt; request to &lt;a href="https://cdn.optimizely.com/datafiles" rel="noopener noreferrer"&gt;https://cdn.optimizely.com/datafiles&lt;/a&gt;. You can find your datafile URL inside your Project settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2c6xyel81sesywntxifc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2c6xyel81sesywntxifc.png" alt="Add to these values your Optimizely snippet and/or datafile" width="799" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s how it should look like once properly filled out:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9rbq9pe958eeygjw7qkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9rbq9pe958eeygjw7qkz.png" alt="With snippet and/or datafile filled out" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS will ask you for a confirmation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19fqgx969dr8lov63xym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19fqgx969dr8lov63xym.png" alt="Final review" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Change the ANY to be exactly the same as the method on the right-hand side. (POST, GET &amp;amp; GET)&lt;/p&gt;

&lt;p&gt;Congrats you now have a working API which will proxy requests to Optimizely.com.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Fetch Optimizely from your newly-created API gateway
&lt;/h3&gt;

&lt;p&gt;Now that we’ve got a working API, it’s time to update our website to start fetching from this API.&lt;/p&gt;

&lt;h4&gt;
  
  
  Optimizely Web
&lt;/h4&gt;

&lt;p&gt;Update your script tag that contains Optimizely to no longer fetch the file from cdn.optimizely.com but from your AWS API Gateway. You’ll find the invoke URL on the main API page, as such:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52dtw7895p9vo9hawnrn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F52dtw7895p9vo9hawnrn.png" alt="The invoke URL for your API" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now to ensure the Optimizely snippet sends events to the API gateway instead of the default Optimizely endpoint, this is a custom snippet setting that can’t be configured by a customer. You’ll need to ask your account manager about it. They can amend your snippet to ensure the snippet dispatches events to your newly-created API.&lt;/p&gt;

&lt;h4&gt;
  
  
  Optimizely Full Stack
&lt;/h4&gt;

&lt;p&gt;For Full Stack, you’ll need to customize the SDK’s &lt;em&gt;createInstance&lt;/em&gt; method to include a new datafile URL pointing to your API, as such:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@optimizely/optimizely-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optimizelyClientInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createInstance&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sdkKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;datafileOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;autoUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;urlTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:///datafiles/%s.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll also need to provide a custom event dispatcher which will dispatch the events back to your newly AWS API Gateway. You can use &lt;a href="https://github.com/optimizely/javascript-sdk/blob/master/lib/event_processor/event_dispatcher/default_dispatcher.browser.ts" rel="noopener noreferrer"&gt;this built-in event dispatcher&lt;/a&gt; and change line 39 to indicate the POST endpoint of your newly created API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/resolve-mau-overages-optimizely" rel="noopener noreferrer"&gt;How to resolve MAU overages in Optimizely Experimentation (step-by-step)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/events-full-stack-to-web" rel="noopener noreferrer"&gt;Send Events from Optimizely Full Stack to Optimizely Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/overriding-variation-assignments-optimizely-web" rel="noopener noreferrer"&gt;Overriding Variation Assignments in Optimizely: Complete Guide for Web and Feature Experimentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;How to Read the Optimizely Results Page Correctly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abtesting</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Optimizely vs Amplitude: Experimentation Compared</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:09:34 +0000</pubDate>
      <link>https://dev.to/david_sert/optimizely-vs-amplitude-experimentation-compared-7on</link>
      <guid>https://dev.to/david_sert/optimizely-vs-amplitude-experimentation-compared-7on</guid>
      <description>&lt;p&gt;"Optimizely vs Amplitude" is a slightly unfair framing, because the two products start from opposite ends of the same problem. Optimizely is an experimentation platform that added analytics; Amplitude is a product analytics platform that added experimentation. Both can run an A/B test and tell you which variation won, but the path each takes — and the org each is built for — is different. This guide compares them honestly so you can pick the right tool for your situation, or decide to run both.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR of the difference
&lt;/h2&gt;

&lt;p&gt;Optimizely leads with experimentation. Its core products — Web Experimentation (visual, client-side testing) and Feature Experimentation (server-side feature flags and SDK-based tests) — are purpose-built to run a high volume of rigorous experiments across marketing pages, product surfaces, and backend logic, with the Stats Engine making continuous monitoring statistically safe.&lt;/p&gt;

&lt;p&gt;Amplitude leads with product analytics. Amplitude Experiment is an experimentation layer bolted onto a best-in-class behavioral analytics engine, so every test is automatically connected to deep funnel, retention, and cohort analysis on the same event data.&lt;/p&gt;

&lt;p&gt;If your primary question is "what should we test, where, and how do we govern a large testing program," Optimizely is the stronger center of gravity. If your primary question is "how do users behave, and can I experiment without leaving my analytics," Amplitude is compelling. They are not mutually exclusive — many teams run both.&lt;/p&gt;

&lt;p&gt;The decision comes down to two questions: what is the primary job, and who runs the tests?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A{What is the primary job?} --&amp;gt;|Run and govern experiments| B{Who builds most of the tests?}
  A --&amp;gt;|Deep behavioral analytics| C{Is Amplitude already your&amp;lt;br/&amp;gt;analytics system of record?}
  B --&amp;gt;|Marketers, in a visual editor| D[Optimizely Web Experimentation]
  B --&amp;gt;|Engineers, behind feature flags| E[Optimizely Feature Experimentation]
  C --&amp;gt;|Yes, flag-based product tests| F[Amplitude Experiment]
  C --&amp;gt;|No, and you test backend + marketing pages| G[Optimizely for experiments,&amp;lt;br/&amp;gt;Amplitude for analysis]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What each tool is built for
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Optimizely
&lt;/h3&gt;

&lt;p&gt;Optimizely splits experimentation into two complementary products. &lt;strong&gt;Web Experimentation&lt;/strong&gt; uses a WYSIWYG Visual Editor and a JavaScript snippet, letting marketers and optimization teams build A/B tests, redirect tests, and multivariate tests on a live site without engineering for most changes (custom HTML/JS is available when needed). &lt;strong&gt;Feature Experimentation&lt;/strong&gt; is developer-first: server-side and client-side SDKs (Java, Python, Go, C#, JavaScript/Node, Swift, Android, and more) wrap experiments in feature flags, so you can roll features out, roll them back instantly, and test backend logic such as algorithms, pricing, or APIs. Both share Optimizely's Stats Engine for results.&lt;/p&gt;

&lt;p&gt;The platform is built for &lt;strong&gt;experimentation breadth and program scale&lt;/strong&gt;: visual marketing tests and deep server-side tests under one roof, audience targeting, mutual exclusion groups, multi-armed bandits, and a free Rollouts tier for teams starting with feature flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  Amplitude
&lt;/h3&gt;

&lt;p&gt;Amplitude's foundation is its analytics graph: every event a user fires is stored and queryable through funnels, retention curves, pathfinder, and behavioral cohorts. &lt;strong&gt;Amplitude Experiment&lt;/strong&gt; sits on top of that data, offering feature flags, server-side and client-side evaluation, and both feature-flag-based experiments and a web experiment capability. Its defining advantage is that experiment results read from the same event stream as the rest of your analytics — so a winning variation can immediately be sliced by any cohort or downstream behavior you already track.&lt;/p&gt;

&lt;p&gt;Amplitude is built for &lt;strong&gt;product teams who live in their analytics&lt;/strong&gt; and want experimentation to be a natural extension of behavioral analysis rather than a separate discipline.&lt;/p&gt;

&lt;p&gt;A note on the moving market: OpenAI acquired the experimentation vendor Statsig in September 2025, and in May 2026 Amplitude announced it was taking over the Statsig brand, platform, and customer base. That leaves Amplitude with two experimentation lineages (its native Experiment product and the Statsig platform), and some analysts have flagged near-term uncertainty about how the overlapping capabilities will consolidate. Treat any roadmap promises in this area as provisional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Head-to-head comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Optimizely&lt;/th&gt;
&lt;th&gt;Amplitude&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary use case&lt;/td&gt;
&lt;td&gt;Experimentation across web and product/backend&lt;/td&gt;
&lt;td&gt;Product analytics, with experimentation layered on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Experimentation model&lt;/td&gt;
&lt;td&gt;Visual (Web Experimentation) + feature flags / server-side SDKs (Feature Experimentation)&lt;/td&gt;
&lt;td&gt;Feature-flag-based experiments + web experiment capability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual/no-code testing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes — mature WYSIWYG Visual Editor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited; beyond simple changes, work tends to need engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-side / SDK testing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Strong, broad SDK coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes, via Experiment SDKs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statistics engine&lt;/td&gt;
&lt;td&gt;Stats Engine (&lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;always-valid&lt;/a&gt;, sequential, controls false positives)&lt;/td&gt;
&lt;td&gt;Sequential testing, t-tests, &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;CUPED&lt;/a&gt;, multi-armed bandits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics depth&lt;/td&gt;
&lt;td&gt;Solid experiment analytics; warehouse-native analytics available&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best-in-class behavioral analytics, cohorts, retention, funnels&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration model&lt;/td&gt;
&lt;td&gt;Connects to external analytics (including Amplitude); CMS/DXP ecosystem&lt;/td&gt;
&lt;td&gt;Experimentation tied directly to native event data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing model&lt;/td&gt;
&lt;td&gt;MAU-based (free Rollouts tier for flags)&lt;/td&gt;
&lt;td&gt;Event-volume based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal team&lt;/td&gt;
&lt;td&gt;Optimization, growth, and engineering teams running a structured testing program&lt;/td&gt;
&lt;td&gt;Product teams who already standardize on Amplitude analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Experimentation capabilities compared
&lt;/h2&gt;

&lt;p&gt;The clearest difference is &lt;strong&gt;where and how you can test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Optimizely covers two distinct surfaces well. For marketing and front-end teams, the Web Experimentation Visual Editor makes it straightforward to change copy, layout, and styling and ship an A/B test without a deploy. For engineering and product teams, Feature Experimentation runs tests behind feature flags directly in application code — including server-side paths where there is no DOM to manipulate, such as recommendation algorithms, checkout logic, or API behavior. Flags double as a kill switch, so a bad variation can be turned off remotely without redeploying. Optimizely also supports targeted rollouts, mutual exclusion, and &lt;a href="https://optipilot.com/data/multi-armed-bandit" rel="noopener noreferrer"&gt;multi-armed bandit&lt;/a&gt; optimization.&lt;/p&gt;

&lt;p&gt;Amplitude Experiment is primarily &lt;strong&gt;flag-based&lt;/strong&gt;: you gate a feature behind a flag, evaluate it client- or server-side, and measure the result against your event data. This is a clean model for product experimentation and progressive rollouts. Its web experimentation capability exists, but for anything beyond simple changes, practitioners generally report that meaningful variations require engineering involvement rather than a marketer-friendly visual workflow. If a large share of your testing is on marketing pages built by non-engineers, that distinction matters.&lt;/p&gt;

&lt;p&gt;On the statistics, both are credible. Optimizely's &lt;strong&gt;Stats Engine&lt;/strong&gt; is designed for "always-valid" inference — you can monitor results continuously without inflating false-positive rates, which suits teams that watch dashboards daily. Amplitude likewise uses &lt;strong&gt;sequential testing&lt;/strong&gt; for valid-anytime results, and supports t-tests, CUPED for variance reduction, multi-armed bandits, mutual exclusion groups, and holdouts. Neither team should feel they are settling for weak statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics and data compared
&lt;/h2&gt;

&lt;p&gt;This is where Amplitude's genuine strength shows. Because Experiment runs on the same event pipeline as Amplitude Analytics, a result is never a dead end: you can immediately ask "did the winning variation help retention at day 30," "how did it perform for this behavioral cohort," or "where in the funnel did the lift come from" without exporting data or stitching IDs across tools. For teams whose core competency is behavioral analysis, that tight loop is the main reason to consider Amplitude Experiment over a standalone testing tool.&lt;/p&gt;

&lt;p&gt;Optimizely's analytics are good for reading experiment results and, increasingly, offers warehouse-native analytics that let you analyze decision and event data alongside the rest of your data in Snowflake, BigQuery, or Databricks. But product analytics is not its historical center of gravity the way it is Amplitude's. If your team's daily home is rich cohort and retention analysis, Amplitude will feel more native; if your team's daily home is running and governing experiments, Optimizely will.&lt;/p&gt;

&lt;p&gt;A practical caveat on cost: Amplitude's &lt;strong&gt;event-volume-based pricing&lt;/strong&gt; can scale quickly for high-traffic products, whereas Optimizely prices experimentation on monthly active users and offers a free Rollouts tier for feature flags. Model both against your actual traffic before deciding — the cheaper option depends heavily on your event volume versus user count.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to choose Optimizely
&lt;/h2&gt;

&lt;p&gt;Optimizely is the stronger choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Experimentation is the primary job&lt;/strong&gt;, not a feature of your analytics tool. You want one platform that handles both marketing-page tests and deep server-side product tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-engineers run a meaningful share of tests.&lt;/strong&gt; The Visual Editor lets growth and marketing teams ship experiments without a deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You test in the backend&lt;/strong&gt; — algorithms, pricing, infrastructure changes — where flag-based, server-side experimentation and an instant kill switch are essential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You run a high-volume, governed program&lt;/strong&gt; and want mutual exclusion, bandits, audience targeting, and always-valid statistics in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are starting with feature flags&lt;/strong&gt; and want a free on-ramp via Rollouts before scaling up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Amplitude makes sense
&lt;/h2&gt;

&lt;p&gt;Amplitude Experiment is the better fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amplitude is already your analytics system of record&lt;/strong&gt; and most of your team's decisions start in its funnels, cohorts, and retention reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your experimentation is product-led and flag-based&lt;/strong&gt; — gating features, progressive rollouts, and measuring impact on behavioral metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The analysis loop matters more than test authoring breadth.&lt;/strong&gt; You value being able to slice any result by behavioral cohort instantly, on the same data, without integration work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are not relying on a visual, marketer-driven editor&lt;/strong&gt; for the bulk of your tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be deliberate here given the current consolidation around the acquired Statsig platform: confirm which experimentation product a vendor is steering you toward and what its supported roadmap looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  How they work together
&lt;/h2&gt;

&lt;p&gt;These tools are not an either/or for many organizations, and the most pragmatic answer is often "both." A common pattern is to &lt;strong&gt;run experiments in Optimizely and analyze them in Amplitude&lt;/strong&gt;: Optimizely decides which variation a user sees and provides the statistical results, while the variation a user was bucketed into is sent into Amplitude as a user property or event, so you can analyze experiment impact against your full behavioral dataset.&lt;/p&gt;

&lt;p&gt;This is a supported, documented integration. Optimizely Feature Experimentation can forward decision data to Amplitude via a decision notification listener (setting an &lt;code&gt;[Optimizely] &amp;lt;flagKey&amp;gt;&lt;/code&gt; user property and an optional impression event), and Optimizely Web Experimentation integrates through custom analytics extensions, with the connector built and maintained by Amplitude. If your team has standardized on Amplitude for analytics but wants Optimizely's experimentation breadth and visual testing, this combination gives you the best of both rather than forcing a single choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;Pick based on where your center of gravity sits. If experimentation is the discipline you are investing in — across marketing, product, and backend — Optimizely is built for that job and gives non-engineers and engineers a shared platform with rigorous statistics. If best-in-class behavioral analytics is your foundation and you want experimentation as a tightly coupled extension of it, Amplitude Experiment earns its place. And if you have both needs, the supported integration means you do not have to choose: experiment in Optimizely, analyze in Amplitude.&lt;/p&gt;

&lt;p&gt;This site covers both Optimizely products in implementation depth. Browse the &lt;a href="https://optipilot.com/feature-experimentation" rel="noopener noreferrer"&gt;Feature Experimentation guides&lt;/a&gt; and the &lt;a href="https://optipilot.com/web-experimentation" rel="noopener noreferrer"&gt;Web Experimentation guides&lt;/a&gt;, or start from the &lt;a href="https://optipilot.com/feature-experimentation/integrations" rel="noopener noreferrer"&gt;integration catalog&lt;/a&gt;. Not sure which platform fits your stack? &lt;a href="https://optipilot.com/contact" rel="noopener noreferrer"&gt;Contact us&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/feature-experimentation/integrations/amplitude" rel="noopener noreferrer"&gt;Integrate Amplitude with Optimizely Feature Experimentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/web-experimentation/integrations/amplitude" rel="noopener noreferrer"&gt;Integrate Amplitude with Optimizely Web Experimentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/strategy/testing-ai-models-with-feature-flags" rel="noopener noreferrer"&gt;Testing AI Models with Feature Flags: LLM Prompt Optimization&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abtesting</category>
      <category>analytics</category>
      <category>optimizely</category>
      <category>saas</category>
    </item>
    <item>
      <title>How the Optimizely Stats Engine Works</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:08:58 +0000</pubDate>
      <link>https://dev.to/david_sert/how-the-optimizely-stats-engine-works-3j6a</link>
      <guid>https://dev.to/david_sert/how-the-optimizely-stats-engine-works-3j6a</guid>
      <description>&lt;p&gt;Optimizely's Stats Engine is the statistical framework behind every winner, loser, and "inconclusive" verdict on the Experiment Results page. It exists to solve a specific, expensive problem: practitioners watch experiments in real time and make decisions the moment a result "looks significant," and traditional statistics punishes that behavior with a flood of false positives. Understanding how Stats Engine works is the difference between trusting a result and acting on noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stats Engine Is and the Problem It Solves
&lt;/h2&gt;

&lt;p&gt;Stats Engine is Optimizely's proprietary statistical methodology for evaluating A/B tests. It is a &lt;strong&gt;frequentist&lt;/strong&gt; engine built on &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;&lt;strong&gt;sequential testing&lt;/strong&gt;&lt;/a&gt; combined with &lt;a href="https://optipilot.com/data/false-discovery-rate-control" rel="noopener noreferrer"&gt;&lt;strong&gt;false discovery rate&lt;/strong&gt;&lt;/a&gt;** (FDR) control**. Those two mechanisms are the whole story, and each addresses a distinct failure mode of classical A/B testing.&lt;/p&gt;

&lt;p&gt;The first failure mode is the &lt;strong&gt;peeking problem&lt;/strong&gt;. Traditional fixed-horizon statistics (the t-test being the canonical example) are only valid if you commit to a &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt; in advance, wait until you reach it, and look exactly once. Every time you check results early and react, you give random noise another chance to cross the significance threshold. A test that peeks repeatedly can report a false "winner" far more often than its stated error rate suggests. But waiting passively for a pre-computed sample size is exactly what real experimentation teams do not do.&lt;/p&gt;

&lt;p&gt;The second failure mode is the &lt;strong&gt;multiple comparisons problem&lt;/strong&gt;. Real experiments rarely track one metric on one variation. Add more variations and more metrics and the chance of at least one false positive climbs quickly, even though each individual test holds its error rate. Worse, the rate that actually matters to a decision-maker, the proportion of false positives &lt;em&gt;among the results you act on&lt;/em&gt;, is higher still.&lt;/p&gt;

&lt;p&gt;Stats Engine is engineered so that both problems are handled automatically. As Optimizely's documentation states, results are &lt;strong&gt;always valid&lt;/strong&gt;: you can monitor a test continuously and stop as soon as you have a clear winner, without invalidating it, because the engine controls the false discovery rate throughout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Sequential Testing Solves the Peeking Problem
&lt;/h2&gt;

&lt;p&gt;Sequential testing is the mechanism that makes continuous monitoring safe. Rather than computing a single p-value at one fixed endpoint, Stats Engine evaluates the experiment as evidence accumulates over time and produces inferences that remain valid no matter when you look.&lt;/p&gt;

&lt;p&gt;Optimizely's own framing uses a baking analogy that is worth keeping in mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fixed Horizon : Set a timer before baking. You may only open the oven
                when the timer ends. Open it early and the result is unreliable.
Sequential    : Put the cake in without committing to a time. Open the oven
                whenever you like to check; looking never ruins the result.
                When it looks done, it is done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mechanically, Stats Engine does not compute one confidence interval. It computes a &lt;strong&gt;series of 100 successive confidence intervals&lt;/strong&gt; across the experiment's lifetime, each with its own significance value. The numbers you see on the Results page are deliberately conservative summaries of that series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://optipilot.com/tools/statistical-significance-calculator" rel="noopener noreferrer"&gt;&lt;strong&gt;statistical significance&lt;/strong&gt;&lt;/a&gt; shown is the &lt;em&gt;smallest&lt;/em&gt; significance value observed across those sequential intervals, not the average and not the latest.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;confidence interval&lt;/strong&gt; shown is the &lt;em&gt;running intersection&lt;/em&gt; of all prior intervals: it tracks the smallest upper limit and the largest lower limit seen during the run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of this, the displayed significance and interval may not exactly match the currently observed conversion rates. That is intentional. It is what makes the result robust to having been observed many times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Significance Climbs (and Occasionally Drops)
&lt;/h3&gt;

&lt;p&gt;In a stable environment, significance should rise in a stepwise, generally increasing fashion as evidence accumulates. Two forms of evidence move it: &lt;strong&gt;larger differences&lt;/strong&gt; between conversion rates, and &lt;strong&gt;differences that persist across more visitors&lt;/strong&gt;. Early on, when the sample is small, large swings are treated conservatively, so you often see a flat line that later rises sharply once real evidence accrues.&lt;/p&gt;

&lt;p&gt;Significance can also fall, though Optimizely's analysis indicates this happens in only about 4% of experiments. Small dips of a few percentage points come from &lt;strong&gt;time bucketing&lt;/strong&gt;: Optimizely divides the experiment's runtime into 100 equal buckets that grow as the test runs, reshuffling visitors among them and recomputing as it goes, which produces minor fluctuations. Larger drops, potentially all the way to 0%, come from a &lt;strong&gt;stats reset&lt;/strong&gt;, a protective mechanism that triggers when the engine detects that the underlying environment has changed (the assumption that data is identically distributed has been violated). A reset is the engine refusing to stand behind a conclusion the new evidence no longer supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  False Discovery Rate Control vs Traditional Significance
&lt;/h2&gt;

&lt;p&gt;The second pillar is what makes Stats Engine trustworthy when you run many metrics and variations. The naive error metric is the &lt;strong&gt;false positive rate&lt;/strong&gt;: out of all the comparisons where there is truly no effect, what fraction are wrongly flagged? Optimizely controls something more decision-relevant, the &lt;strong&gt;false discovery rate&lt;/strong&gt;: out of the results you would actually act on (the declared winners and losers), what fraction are wrong?&lt;/p&gt;

&lt;p&gt;The distinction matters enormously. Optimizely's worked example: an experiment with ten comparison opportunities reports two winners, one of which is a false winner. Measured as a false positive rate, that is 1 in 10, about 10%, which sounds acceptable. But you do not implement the eight inconclusive results; you implement the two winners. Among those, your error rate is 1 in 2, or &lt;strong&gt;50%&lt;/strong&gt;. The false discovery rate captures the risk that actually reaches your roadmap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;False positive rate = false positives / all null comparisons   = 1/10 = 10%
False discovery rate = false positives / declared discoveries   = 1/2  = 50%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To control FDR across many hypotheses, Stats Engine uses a &lt;strong&gt;tiered version of the Benjamini-Hochberg procedure&lt;/strong&gt;. The tiering reflects that not all metrics deserve equal weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary metric (rank 1)&lt;/strong&gt; — evaluated independently of all others, so it reaches significance as fast as possible and is unaffected by how many other metrics you track.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary metrics (ranks 2-5)&lt;/strong&gt; — their significance threshold is adjusted for the number of metrics and variations. Adding more secondary metrics can slow each of them to significance, but never slows the primary metric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring metrics (rank 6+)&lt;/strong&gt; — each given a fractional weight of 1/n, so they have minimal impact on secondary metrics and none on the primary metric.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical payoff: Optimizely keeps the false discovery rate low (approximately 10%) while still letting your most important metric reach significance quickly. This is also why Stats Engine uses &lt;strong&gt;two-tailed tests&lt;/strong&gt;, which are required for FDR control.&lt;/p&gt;

&lt;p&gt;One caveat the documentation is explicit about: FDR control is &lt;strong&gt;not&lt;/strong&gt; maintained when you &lt;strong&gt;segment&lt;/strong&gt; results. The deeper you slice, the higher your chance of finding a spurious "significant" segment. Use segments for exploration, not for decisions, and only inspect the most meaningful ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Read Significance and Confidence Intervals
&lt;/h2&gt;

&lt;p&gt;Statistical significance answers a precise question: how unusual would these results be if the variation and baseline truly performed identically? At 90% significance, you are accepting roughly a 10% false-positive risk on that call. The confidence interval is the estimated range that likely contains the true effect (the true uplift), and Optimizely sets its confidence level to match your project's significance threshold (90% by default).&lt;/p&gt;

&lt;p&gt;The single most useful rule for reading the Results page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A variation reaches significance exactly when its confidence interval &lt;strong&gt;stops crossing zero&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidence interval entirely above 0%&lt;/strong&gt; means a winning variation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence interval includes 0%&lt;/strong&gt; means inconclusive (you cannot yet rule out "no effect").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence interval entirely below 0%&lt;/strong&gt; means a losing variation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before declaring anything, Stats Engine enforces minimum data thresholds. For &lt;strong&gt;binary metrics&lt;/strong&gt;, it requires at least 100 visitors or sessions and at least 25 conversions in both the baseline and the variation. For &lt;strong&gt;numeric metrics&lt;/strong&gt; such as revenue, it requires at least 100 visitors or sessions but no fixed conversion count. Until those are met, the page reports that more visitors are needed and estimates the wait.&lt;/p&gt;

&lt;p&gt;A useful judgment heuristic from the documentation: if the &lt;strong&gt;observed mean (the tick mark)&lt;/strong&gt; sits near the &lt;strong&gt;edge&lt;/strong&gt; of the confidence interval, the engine may be accumulating evidence against its own conclusion, so consider waiting. If the observed mean sits near the &lt;strong&gt;center&lt;/strong&gt;, you can be more confident the call will hold.&lt;/p&gt;

&lt;p&gt;For revenue-per-visitor goals, be aware that revenue distributions are heavily skewed, which reduces &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;statistical power&lt;/a&gt;. Stats Engine applies &lt;strong&gt;skew correction&lt;/strong&gt; to recover some of that power and to shape the confidence interval correctly, but skewed metrics still generally need more data than binary ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stats Accelerator: A Separate Feature, Not the Engine
&lt;/h2&gt;

&lt;p&gt;Stats Accelerator is frequently conflated with Stats Engine. They are different things. Stats Engine is the statistical methodology that evaluates results. &lt;strong&gt;Stats Accelerator is a traffic-allocation feature&lt;/strong&gt; that sits on top of it and uses a multi-armed-bandit-family algorithm (a variation on the Upper Confidence Bound strategy) to shorten the &lt;em&gt;time&lt;/em&gt; to statistical significance.&lt;/p&gt;

&lt;p&gt;Stats Accelerator monitors a running experiment and routes more traffic toward the variation showing the &lt;strong&gt;most significant difference from the baseline&lt;/strong&gt;, regardless of whether that difference is positive or negative, because its goal is to minimize &lt;em&gt;time&lt;/em&gt;, not &lt;em&gt;regret&lt;/em&gt;. Once a variation reaches significance, it is removed from consideration and traffic is redistributed to the rest. It requires at least three variations (a baseline plus two). It still produces statistical significance, because the underlying engine is still doing the inference.&lt;/p&gt;

&lt;p&gt;This is distinct from a true &lt;a href="https://optipilot.com/data/multi-armed-bandit" rel="noopener noreferrer"&gt;&lt;strong&gt;multi-armed bandit&lt;/strong&gt;&lt;/a&gt;** (MAB) optimization** (formerly "Accelerate Impact"), which minimizes &lt;em&gt;regret&lt;/em&gt; by funneling traffic to whichever variation currently performs best on the primary metric. MABs are for temporary, value-maximizing scenarios such as a Black Friday promotion, and crucially &lt;strong&gt;MAB optimizations do not generate statistical significance at all&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Produces significance?&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stats Accelerator&lt;/td&gt;
&lt;td&gt;Minimize time to significance&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;You want a reliable winner faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-armed bandit&lt;/td&gt;
&lt;td&gt;Maximize reward / minimize regret&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Short-lived campaigns; no permanent decision needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because Stats Accelerator changes traffic allocation mid-flight, it risks a sampling bias called &lt;strong&gt;Simpson's Paradox&lt;/strong&gt; when conversion rates vary over time (for example, weekday-vs-weekend seasonality). Optimizely addresses this with the &lt;strong&gt;Epoch Stats Engine&lt;/strong&gt;, which produces a stratified, &lt;strong&gt;weighted improvement&lt;/strong&gt; estimate, comparing baseline and variation &lt;em&gt;within each interval&lt;/em&gt; between allocation changes, then combining those intervals by visitor count. This is also why, with Stats Accelerator enabled, the Results page may report both absolute (in percentage points) and relative improvement. For Feature Experimentation, use a user profile service (sticky bucketing) so frequent reallocation does not expose one visitor to multiple variations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Misconceptions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the Optimizely Stats Engine Bayesian?
&lt;/h3&gt;

&lt;p&gt;No. This is the most common misconception, and it is worth correcting precisely. Stats Engine is a &lt;strong&gt;frequentist sequential&lt;/strong&gt; method. Optimizely does offer a separate, explicitly &lt;strong&gt;Bayesian&lt;/strong&gt; A/B testing mode (which expresses results as direct probabilities like "90% chance B beats A"), and a separate &lt;strong&gt;Frequentist Fixed Horizon&lt;/strong&gt; mode. But the classic Stats Engine, the one that powers sequential testing, is frequentist. It reports statistical significance and frequentist confidence intervals, not posterior probabilities. The reason a search for "Optimizely Bayesian" surfaces Stats Engine at all is that both Bayesian and sequential methods let you peek and stop early. That shared &lt;em&gt;behavior&lt;/em&gt; does not make them the same &lt;em&gt;methodology&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stats Engine results disagree with my t-test, so they must be wrong
&lt;/h3&gt;

&lt;p&gt;They can legitimately disagree, and Stats Engine is the more trustworthy of the two when you have been monitoring continuously. A t-test uses only the &lt;em&gt;currently observed&lt;/em&gt; mean and difference, so if evidence looked strong early and weakened later, a t-test can still report the stale, strong result. Stats Engine's intersected intervals are more conservative, less likely to declare a false winner, and less likely to reverse a call later.&lt;/p&gt;

&lt;h3&gt;
  
  
  I can keep slicing segments until something is significant
&lt;/h3&gt;

&lt;p&gt;You can, but you will be manufacturing false discoveries. FDR control does not extend across segments. Repeated segment-hunting inflates false positives exactly like peeking does.&lt;/p&gt;

&lt;h3&gt;
  
  
  A stats reset means the tool is broken
&lt;/h3&gt;

&lt;p&gt;The opposite. A reset means the engine detected that the environment changed and is protecting you from standing behind a conclusion the new data no longer supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Guidance for Trusting Your Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rank your metrics deliberately.&lt;/strong&gt; Put the metric that defines success as the primary metric, ideally measured close to the change in the funnel. It gets independent, fastest-to-significance treatment; everything else is secondary or monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the test run to its planned duration even though you can peek.&lt;/strong&gt; Sequential validity means peeking will not break your stats, but a result that has barely cleared the threshold on thin data is fragile. Treat experimentation as a standardized process, not a dashboard you babysit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the confidence interval, not just the significance number.&lt;/strong&gt; Width tells you precision; position relative to zero tells you direction; the tick mark's position warns you whether a call is at risk of reversing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use segments to explore, never to decide.&lt;/strong&gt; If a segment looks interesting, treat it as a hypothesis for a new, properly powered experiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the method to the intent.&lt;/strong&gt; Use a standard A/B test (sequential Stats Engine) when you need a trustworthy decision; add Stats Accelerator to reach that decision faster; use a multi-armed bandit only for temporary value-maximization where you do not need a statistically defensible winner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For how the three inference families differ in practice, see &lt;a href="https://optipilot.com/data/bayesian-vs-frequentist-vs-sequential-optimizely" rel="noopener noreferrer"&gt;Bayesian vs frequentist vs sequential testing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related guides
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;How to Read the Optimizely Results Page Correctly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/event-properties-vs-user-attributes" rel="noopener noreferrer"&gt;Event Properties vs User Attributes in Optimizely&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://optipilot.com/data/resolve-mau-overages-optimizely" rel="noopener noreferrer"&gt;Optimizely MAUs: What Counts as a Monthly Active User &amp;amp; How to Reduce Overages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>statistics</category>
      <category>abtesting</category>
      <category>optimizely</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Feature Flag Best Practices for Production Systems</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:07:11 +0000</pubDate>
      <link>https://dev.to/david_sert/feature-flag-best-practices-for-production-systems-3238</link>
      <guid>https://dev.to/david_sert/feature-flag-best-practices-for-production-systems-3238</guid>
      <description>&lt;p&gt;Your flag count has quietly crossed a threshold. What started as a clean way to ship a risky feature behind a toggle is now a few hundred conditionals scattered across services, half of them permanently on, nobody quite sure which are safe to delete. Someone flips the wrong one during an incident. A test passes locally and fails in CI because a flag defaulted differently. This is the predictable midlife of feature flagging: the mechanism is trivial, but the discipline around it is what separates teams who ship faster from teams who have simply moved their risk somewhere less visible.&lt;/p&gt;

&lt;p&gt;The good news is that the disciplines are well understood and largely independent of any one vendor. This article lays them out, then shows how they map onto Optimizely Feature Experimentation specifically, so that the practices have a concrete home in tooling rather than living only in a wiki page nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify every flag by type and lifespan
&lt;/h2&gt;

&lt;p&gt;The single most useful idea in feature flagging is that not all flags are the same thing. Martin Fowler's taxonomy splits them into four categories, and the reason it matters is that each has a different owner, a different lifespan, and a different failure mode when you ignore it (&lt;a href="https://martinfowler.com/articles/feature-toggles.html" rel="noopener noreferrer"&gt;Fowler, Feature Toggles&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Release toggles&lt;/strong&gt; hide in-progress work so it can merge to trunk and ship dormant. They are the shortest-lived flags you own, meant to survive days or weeks, and they should be deleted the moment the feature is fully rolled out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment toggles&lt;/strong&gt; split traffic between variations to measure an outcome. They live exactly as long as the test needs to reach a decision, then collapse to the winning variation. This is the layer beneath A/B testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ops toggles&lt;/strong&gt; give operators control over system behavior: degrade an expensive feature under load, disable a flaky dependency, or trip a kill switch during an incident. Some are short-lived, but a genuine kill switch may live indefinitely as a safety control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissioning toggles&lt;/strong&gt; decide who gets access to what, such as gating a feature to premium accounts. These can legitimately live for years and are effectively part of your product, not temporary scaffolding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fowler frames these along two axes: &lt;strong&gt;longevity&lt;/strong&gt; (how long the flag should exist) and &lt;strong&gt;dynamism&lt;/strong&gt; (whether the decision is fixed per deployment or must be re-evaluated per request). The categories fall at different points, and conflating them is the root of most flag debt. A release toggle that gets treated like a permanent ops switch never gets cleaned up, because nobody remembers it was supposed to be temporary.&lt;/p&gt;

&lt;p&gt;The practical takeaway is to make the type explicit at creation. Decide, before you write the flag, whether it is a release, experiment, ops, or permissioning flag, and record that alongside an owner and an expected removal or review date. In Optimizely, flag rules make the distinction concrete: an A/B test rule is an experiment flag, while a targeted delivery is a release or ops flag. If you are weighing a fixed split against an adaptive one for an experiment flag, the trade-offs are covered in &lt;a href="https://optipilot.com/data/stats-accelerator-vs-mab-vs-contextual-bandit" rel="noopener noreferrer"&gt;Stats Accelerator vs multi-armed bandit vs contextual bandit&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[New flag] --&amp;gt; B{What is its job?}
    B --&amp;gt;|Hide in-progress work| C[Release toggle&amp;lt;br/&amp;gt;lifespan: days-weeks&amp;lt;br/&amp;gt;delete after rollout]
    B --&amp;gt;|Split traffic to measure| D[Experiment toggle&amp;lt;br/&amp;gt;lifespan: the test&amp;lt;br/&amp;gt;collapse to winner]
    B --&amp;gt;|Operator control / kill switch| E[Ops toggle&amp;lt;br/&amp;gt;lifespan: short, or&amp;lt;br/&amp;gt;indefinite for safety]
    B --&amp;gt;|Gate access by plan/role| F[Permissioning toggle&amp;lt;br/&amp;gt;lifespan: years&amp;lt;br/&amp;gt;part of the product]
    C --&amp;gt; G[Book its removal now]
    D --&amp;gt; G
    E --&amp;gt; H[Owner + review date]
    F --&amp;gt; H
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Name flags so they stay searchable at scale
&lt;/h2&gt;

&lt;p&gt;At ten flags, naming is a matter of taste. At three hundred, it is the difference between a flag you can find and a flag you are afraid to touch. A good flag key is greppable across your codebase, self-describing, and encodes enough context that someone who did not create it can guess its purpose and owner.&lt;/p&gt;

&lt;p&gt;A convention worth adopting encodes, in a consistent order, the owning team or domain, the flag's intent, and where it applies. Something like &lt;code&gt;checkout_new-address-form_release&lt;/code&gt; or &lt;code&gt;search_semantic-ranking_experiment&lt;/code&gt; reads cleanly, sorts sensibly, and tells a reader the type without opening a dashboard. Keep to one delimiter, avoid spaces and punctuation, and never reuse a retired key for a new purpose. OptiPilot's free &lt;a href="https://optipilot.com/tools/flag-naming-generator" rel="noopener noreferrer"&gt;feature flag naming convention generator&lt;/a&gt; turns a flag's type, team, and feature into a consistent key across dot, kebab, and snake styles, so a whole team names flags the same way.&lt;/p&gt;

&lt;p&gt;Naming discipline pays off precisely because flag keys are the join between your code and your configuration. Optimizely makes this contract literal: a rule's key cannot be changed after creation, and if the key referenced in your &lt;code&gt;decide&lt;/code&gt; call does not match a key in the platform, no traffic is served to that rule (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38908008297741-Manage-rules" rel="noopener noreferrer"&gt;Manage rules&lt;/a&gt;). A typo is not a loud error; it is a silent no-op that quietly serves the fallback. Consistent, reviewed names make that class of mistake far easier to catch in code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roll out gradually and keep a kill switch
&lt;/h2&gt;

&lt;p&gt;The reason flags earn their keep in production is that they decouple deploy from release. You ship code dark, then turn it on for a slice of traffic, watch, and widen the slice, or reverse instantly if something breaks. Two properties make this safe: a gradual ramp and a fast reversal.&lt;/p&gt;

&lt;p&gt;A gradual ramp means starting small. Optimizely's own guidance for feature rollouts recommends 10% as a starting allocation before widening (&lt;a href="https://support.optimizely.com/hc/en-us/articles/45552846481037-Run-Feature-Rollouts-in-Feature-Experimentation" rel="noopener noreferrer"&gt;Run Feature Rollouts&lt;/a&gt;). You can also target the ramp: deliver to internal users first, then beta customers, then the general population, using audience conditions rather than a blunt percentage. Optimizely's targeted delivery supports exactly this, letting you roll out to a percentage of a specific audience and increase it over time (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38932063198989-Flag-delivery-targeted-delivery-overview" rel="noopener noreferrer"&gt;Flag delivery overview&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Fast reversal is the kill switch. Because flag changes take effect through configuration rather than a code deploy, you can roll a feature back in seconds. In Optimizely, deliveries take effect without a code deployment, and you can toggle a flag on and off without re-bucketing the users already assigned, as long as you do not change the traffic allocation at the same moment (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38938396924941-Run-flag-deliveries-targeted-delivery" rel="noopener noreferrer"&gt;Run flag deliveries&lt;/a&gt;). That last caveat matters operationally: an incident kill switch should flip the flag off, not simultaneously re-slice traffic, or you will disturb bucketing for everyone else.&lt;/p&gt;

&lt;p&gt;There is a subtle trap in ramping that shows up most in experiment flags. Bucketing is deterministic: Optimizely hashes the user ID to assign a variation, so the same user gets the same experience as long as the distribution has not changed (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38931713970189-Core-concepts-of-Feature-Experimentation" rel="noopener noreferrer"&gt;Core concepts&lt;/a&gt;). Change the traffic allocation on a running experiment and users can be re-bucketed, which both disturbs their experience and can skew results. If you need to change allocation mid-flight, use a user profile service to keep bucketing sticky, and watch for &lt;a href="https://optipilot.com/data/sample-ratio-mismatch" rel="noopener noreferrer"&gt;sample ratio mismatch&lt;/a&gt;, the canary that tells you your split is not landing the way you configured it.&lt;/p&gt;

&lt;p&gt;One decision to make deliberately: whether a given flag needs measurement. Optimizely separates a plain &lt;strong&gt;targeted delivery&lt;/strong&gt; (free, no analytics, dispatches no decision events) from a &lt;strong&gt;feature rollout&lt;/strong&gt; (full analytics, consumes monthly active users and impressions). Use a targeted delivery for a simple release or kill switch where you only need on/off control, and a rollout or A/B test when you actually want to measure impact. Reaching for the analytics-bearing rule on a flag you will never analyze is a common and needless cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environments and the datafile: where flags actually live
&lt;/h2&gt;

&lt;p&gt;Flags are configuration, and configuration needs somewhere to live per stage of your pipeline. In Optimizely, a project contains one or more &lt;strong&gt;environments&lt;/strong&gt;, typically staging and production, and each environment holds its own ruleset for a flag. You can run a flag at 100% in development and 10% in production, or configure entirely different rules per environment, with separate permissions for who can change each (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38931713970189-Core-concepts-of-Feature-Experimentation" rel="noopener noreferrer"&gt;Core concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;At runtime the SDK does not call back to Optimizely on every decision. It downloads a &lt;strong&gt;datafile&lt;/strong&gt;, a JSON snapshot of your flags and rules for one environment, and evaluates decisions locally against it. When you edit a rule, the datafile updates within a few seconds, and how quickly that reaches production depends on how often your app fetches it; webhooks let you push updates in near real time (&lt;a href="https://support.optimizely.com/hc/en-us/articles/38908008297741-Manage-rules" rel="noopener noreferrer"&gt;Manage rules&lt;/a&gt;). This architecture is why flag evaluation is fast and why it keeps working during a brief network blip: decisions are computed from an in-memory config, not a synchronous network round trip.&lt;/p&gt;

&lt;p&gt;It also defines your failure mode, which is the next thing to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test safely with flags and sensible fallbacks
&lt;/h2&gt;

&lt;p&gt;Flags multiply the states your system can be in, and untended they make tests flaky and behavior unpredictable. Three habits keep that under control.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;always have a safe default&lt;/strong&gt;. Design so that the flag being off, or the configuration being unavailable, yields a working, conservative experience. Optimizely's &lt;code&gt;decide&lt;/code&gt; method returns an &lt;code&gt;OptimizelyDecision&lt;/code&gt; object whose &lt;code&gt;enabled&lt;/code&gt; field is false until a rule turns the flag on, and on a critical error, such as the SDK not being ready or an unknown flag key, it returns a &lt;code&gt;null&lt;/code&gt;&lt;code&gt;variationKey&lt;/code&gt; and populates &lt;code&gt;reasons&lt;/code&gt; (&lt;a href="https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-for-the-javascript-sdk" rel="noopener noreferrer"&gt;Decide methods&lt;/a&gt;). Your code should treat that state as "off" and fall back to a value you control, rather than assuming a variable will be present. For resilience against the SDK not having a datafile yet, you can also bundle a datafile with the application so the client can initialize synchronously from a known-good snapshot.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;decouple the decision from the decision point&lt;/strong&gt;. Fowler's advice is to avoid scattering raw flag checks throughout the codebase, because changing the logic then means hunting down every call site. Instead, funnel decisions through a single abstraction, a &lt;code&gt;FeatureDecisions&lt;/code&gt; object or equivalent, so the toggle point (where behavior branches) is separated from the toggle router (how the decision is made). This keeps business logic testable without the flagging system present, and confines flag removal to one place.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;test the states you actually ship&lt;/strong&gt;. At minimum, exercise the flag-on and flag-off paths, since both are real production states. Wrap decisions so tests can inject a decision directly rather than standing up the SDK, which removes network dependence and the flakiness that comes with it. For the human side of pre-launch verification, a structured pass like the &lt;a href="https://optipilot.com/tools/experiment-qa-checklist" rel="noopener noreferrer"&gt;experiment QA checklist&lt;/a&gt; catches the targeting, bucketing, and event-tracking mistakes that unit tests miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pay down flag debt before it compounds
&lt;/h2&gt;

&lt;p&gt;Every flag is inventory, and inventory has a carrying cost. Fowler's framing is blunt: "savvy teams view their feature toggles in their codebase as inventory which comes with a carrying cost and seek to keep that inventory as low as possible." Each live flag is an extra branch to reason about, an extra combination to test, and an extra thing that can be misconfigured. Stale flags are the single most common feature-flag problem, and they accumulate because deleting a flag is nobody's urgent job.&lt;/p&gt;

&lt;p&gt;The fix is to make cleanup a scheduled, owned activity rather than a good intention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Book the removal when you create the flag.&lt;/strong&gt; For every release toggle, add a removal task to the backlog at the moment it is introduced. The work of retiring it is then already tracked, not rediscovered months later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give short-lived flags an expiry.&lt;/strong&gt; Record an expected removal date on release and experiment flags. Some teams go further and build "time bombs" that fail a test, or even refuse to start the application, once a flag outlives its expiration date, converting silent debt into a loud, actionable failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the inventory.&lt;/strong&gt; A Lean-style limit on the total number of live flags forces a retirement before a new flag can be added, keeping the pile bounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the whole set on a cadence.&lt;/strong&gt; Optimizely's flags dashboard gives you the raw material: every flag with its status, creator, and last-modified date, filterable by environment and audience (&lt;a href="https://support.optimizely.com/hc/en-us/articles/30715996139789-Custom-Flags-Dashboard" rel="noopener noreferrer"&gt;Custom Flags Dashboard&lt;/a&gt;). A monthly sweep for flags that are fully rolled out, concluded, or untouched for a quarter turns "someone should clean these up" into a concrete list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a flag is genuinely done, retire it in both places. Remove the code branch and the flag definition together, so you never leave a dangling key or an orphaned conditional. The code side of that sweep can be automated: &lt;a href="https://optipilot.com/feature-experimentation/implementation/find-unused-feature-flags" rel="noopener noreferrer"&gt;find unused feature flags&lt;/a&gt; walks through locating stale flag references and dangling keys in your codebase before they calcify. In Optimizely, archiving a flag removes it from the datafile and requires that it not be active in your highest-priority environment, usually production, which is a useful guardrail against archiving something still serving traffic; the flag's data is retained so you can unarchive later if needed (&lt;a href="https://support.optimizely.com/hc/en-us/articles/30715996139789-Custom-Flags-Dashboard" rel="noopener noreferrer"&gt;Custom Flags Dashboard&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  How Optimizely maps flags to your code
&lt;/h2&gt;

&lt;p&gt;Pulling the pieces together, here is the shape of the integration. A flag is, in Optimizely's own words, "a place in code where a decision is made." You create a user context from a user ID and any targeting attributes, then call &lt;code&gt;decide&lt;/code&gt; with the flag key. The returned decision tells you whether the flag is enabled and carries any configuration variables and the variation key (&lt;a href="https://docs.developers.optimizely.com/feature-experimentation/docs/key-concepts-and-differences-in-full-stack-and-feature-experimentation" rel="noopener noreferrer"&gt;Key concepts&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The following is illustrative JavaScript, showing the decision routed through a single wrapper with an explicit fallback. Method names are verified against current SDK docs, but exact syntax varies by language and SDK major version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Illustrative: one place that owns the decision, with a safe default.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alphabetical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// conservative, always-works behavior&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProductSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Wait until the SDK has a datafile; on failure, fall back.&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sortMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimizely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUserContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;product_sort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Critical error: variationKey is null and reasons are populated.&lt;/span&gt;
  &lt;span class="c1"&gt;// Treat as "off" and use our own fallback rather than trusting variables.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variationKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flag decision failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sortMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sortMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sort_method&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FALLBACK_SORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two SDK details are worth knowing for production hygiene. First, &lt;code&gt;decide&lt;/code&gt; dispatches a decision (impression) event by default so the exposure shows up in results; when you only want to read a flag's state without recording an exposure, the &lt;code&gt;OptimizelyDecideOption.DISABLE_DECISION_EVENT&lt;/code&gt; option suppresses it (&lt;a href="https://docs.developers.optimizely.com/feature-experimentation/docs/decide-methods-for-the-javascript-sdk" rel="noopener noreferrer"&gt;Decide methods&lt;/a&gt;). This is the right tool when you evaluate a flag early to pre-render content and will record the real exposure later at the point of interaction. Second, remember the silent-failure rule from earlier: a flag key in your code with no matching, running rule serves no traffic, so keep code and configuration in lockstep.&lt;/p&gt;

&lt;p&gt;The through-line across all of this is modest but powerful. Treat each flag as a small, owned, dated commitment rather than a permanent fixture. Name it so it can be found, ramp it so a mistake is cheap, keep a switch that reverses it fast, and schedule its removal the day you create it. Do that consistently and feature flags go back to being what they were supposed to be: a mechanism for shipping with confidence, not a second codebase you are quietly maintaining by accident.&lt;/p&gt;

&lt;p&gt;To see a flag decision run end to end, open the &lt;a href="https://optipilot.com/demo" rel="noopener noreferrer"&gt;demo lab&lt;/a&gt;. The &lt;a href="https://optipilot.com/demo/javascript" rel="noopener noreferrer"&gt;JavaScript v6+&lt;/a&gt;, &lt;a href="https://optipilot.com/demo/react" rel="noopener noreferrer"&gt;React&lt;/a&gt; and &lt;a href="https://optipilot.com/demo/nextjs" rel="noopener noreferrer"&gt;Next.js SSR&lt;/a&gt; harnesses each make a live decision and show its events. Teams still on v5 can use the &lt;a href="https://optipilot.com/demo/javascript-browser-v5" rel="noopener noreferrer"&gt;JavaScript Browser v5&lt;/a&gt; harness.&lt;/p&gt;

</description>
      <category>featureflags</category>
      <category>devops</category>
      <category>webdev</category>
      <category>abtesting</category>
    </item>
    <item>
      <title>False Discovery Rate Control in A/B Testing</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:06:35 +0000</pubDate>
      <link>https://dev.to/david_sert/false-discovery-rate-control-in-ab-testing-35fg</link>
      <guid>https://dev.to/david_sert/false-discovery-rate-control-in-ab-testing-35fg</guid>
      <description>&lt;p&gt;Track one metric on an A/B test and a false positive is a 1-in-20 event at the usual 95% confidence threshold. Track twenty metrics and, under the null hypothesis, you should &lt;em&gt;expect&lt;/em&gt; one of them to look like a winner purely by chance. Add a few variations and a segment breakdown, and the number of simultaneous comparisons climbs into the dozens. This is the multiple comparisons problem, and left uncontrolled it turns a rich results page into a machine for manufacturing phantom wins. The false discovery rate is the modern statistical answer to it — a way to keep testing many things at once without letting your rate of false claims run away from you.&lt;/p&gt;

&lt;p&gt;This guide explains what the false discovery rate is, how it differs from older corrections like family-wise error control and Bonferroni, how the Benjamini-Hochberg procedure works with a fully worked example, and how Optimizely's Stats Engine applies false discovery rate control automatically across every metric and variation on the results page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the False Discovery Rate?
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;false discovery rate (FDR)&lt;/strong&gt; is the expected proportion of your "discoveries" — the results you flag as statistically significant — that are actually false positives. If you declare 10 winning metrics and your FDR is controlled at 5%, then on average you expect about 0.5 of those 10 to be a mistake. FDR is a property of the &lt;em&gt;set&lt;/em&gt; of significant results, not of any single test.&lt;/p&gt;

&lt;p&gt;That framing is the whole point. A traditional p-value controls the error rate of one hypothesis in isolation: a 5% significance level means that &lt;em&gt;if this one variation has no real effect&lt;/em&gt;, there is a 5% chance you wrongly call it significant. But an experiment rarely tests one hypothesis. It tests a primary metric, several secondaries, a stack of guardrails, maybe two or three variations, and often a few segments. Every one of those cells is a hypothesis test. The false discovery rate asks a more useful question: across everything I flagged as a discovery, what fraction should I expect to be wrong?&lt;/p&gt;

&lt;p&gt;Controlling the FDR lets you keep a wide, informative results page — which you want, because catching harm requires watching many metrics — while bounding how often you chase something that was never really there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multiple Comparisons Problem
&lt;/h2&gt;

&lt;p&gt;The mechanism is pure arithmetic. Suppose every metric on your experiment is truly null (no real difference between variation and control), and you test each at a 5% significance level. The probability that a single test does &lt;em&gt;not&lt;/em&gt; produce a false positive is 0.95. The probability that &lt;em&gt;none&lt;/em&gt; of &lt;code&gt;m&lt;/code&gt; independent tests produces one is &lt;code&gt;0.95^m&lt;/code&gt;, so the probability of at least one false positive is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(at least one false positive) = 1 - 0.95^m

m = 1   metric   --&amp;gt;  5.0%
m = 5   metrics  --&amp;gt;  22.6%
m = 10  metrics  --&amp;gt;  40.1%
m = 20  metrics  --&amp;gt;  64.2%
m = 50  metrics  --&amp;gt;  92.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At twenty metrics you are more likely than not to see at least one "significant" result even when nothing is happening. This is why a naive results page that runs every metric at a flat 95% threshold is untrustworthy the moment it carries more than a handful of metrics: the headline significance numbers no longer mean what they appear to mean. The problem compounds with variations and segments, because each comparison is another draw from the same lottery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Ffdr-multiple-metrics.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Ffdr-multiple-metrics.svg" alt="Line chart of the probability that at least one of m independent null metrics reads significant at alpha equals 5%. Without correction the chance climbs from 5% at one metric to about 64% at twenty; with false discovery rate control it stays flat near 5%." width="720" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The instinct to fix this by simply "watching fewer metrics" is the wrong trade. You need guardrail metrics to catch changes that win on conversion but quietly raise refunds or slow the page — that is a deliberately wide net. The right fix is not to look at less, but to adjust the significance bar to account for how much you are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Family-Wise Error Rate vs False Discovery Rate
&lt;/h2&gt;

&lt;p&gt;There are two different error rates you might try to control, and the choice shapes everything downstream.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;family-wise error rate (FWER)&lt;/strong&gt; is the probability of making &lt;em&gt;even one&lt;/em&gt; false positive across the entire family of tests. Controlling FWER at 5% means there is at most a 5% chance that &lt;em&gt;any&lt;/em&gt; of your significant results is wrong. This is a strict, conservative standard — appropriate when a single false claim is catastrophic, such as a drug-approval trial.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;false discovery rate (FDR)&lt;/strong&gt; is the &lt;em&gt;expected proportion&lt;/em&gt; of false positives among the results you called significant. Controlling FDR at 5% means you accept that some individual discoveries may be wrong, as long as, on average, no more than 5% of them are.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Family-wise error rate (FWER)&lt;/th&gt;
&lt;th&gt;False discovery rate (FDR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Controls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Probability of &lt;em&gt;any&lt;/em&gt; false positive&lt;/td&gt;
&lt;td&gt;Expected &lt;em&gt;fraction&lt;/em&gt; of false positives among discoveries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One mistake is unacceptable&lt;/td&gt;
&lt;td&gt;A bounded share of mistakes is tolerable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low — misses many real effects&lt;/td&gt;
&lt;td&gt;High — detects far more real effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typical procedure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bonferroni, Holm&lt;/td&gt;
&lt;td&gt;Benjamini-Hochberg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small number of critical tests&lt;/td&gt;
&lt;td&gt;Many exploratory tests, as in A/B testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For online experimentation, FDR is almost always the better target. You run many tests, most effects are small, and the cost of missing a real winner (low power) is a genuine business cost. FDR control gives you dramatically more statistical power than FWER control while still keeping false claims on a leash. That trade — accept a known, bounded fraction of errors in exchange for detecting many more true effects — is why FDR has become the default for large-scale testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Benjamini-Hochberg Procedure
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Benjamini-Hochberg (BH) procedure&lt;/strong&gt;, introduced by Yoav Benjamini and Yosef Hochberg in 1995, is the standard method for controlling the false discovery rate. It is refreshingly mechanical.&lt;/p&gt;

&lt;p&gt;Given &lt;code&gt;m&lt;/code&gt; p-values and a target FDR level &lt;code&gt;q&lt;/code&gt; (say 0.10):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sort the p-values in ascending order, so &lt;code&gt;p(1) &amp;lt;= p(2) &amp;lt;= ... &amp;lt;= p(m)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For each rank &lt;code&gt;k&lt;/code&gt;, compute the BH critical value &lt;code&gt;(k / m) * q&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;largest&lt;/strong&gt; rank &lt;code&gt;k&lt;/code&gt; for which &lt;code&gt;p(k) &amp;lt;= (k / m) * q&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Declare significant every hypothesis from rank 1 up to and including that &lt;code&gt;k&lt;/code&gt; — even any whose raw p-value sits above the naive threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key idea is the sloped threshold. Instead of comparing every p-value to one fixed cutoff, BH compares each ranked p-value to a line that rises with its rank. A p-value only needs to beat &lt;code&gt;(k / m) * q&lt;/code&gt;, which is more forgiving for higher ranks when there are many small p-values supporting them. The procedure adapts: when the data contain many strong effects, the effective cutoff loosens; when almost nothing is real, it tightens toward the strict Bonferroni bound.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Collect m p-values from every metric and variation] --&amp;gt; B[Sort ascending: p1 to pm]
    B --&amp;gt; C[For each rank k compute BH critical value k over m times q]
    C --&amp;gt; D{Find largest k where p_k is at or below its BH critical value}
    D --&amp;gt; E[Reject ranks 1 through k: declare significant]
    D --&amp;gt; F[Ranks above k: not significant]
    E --&amp;gt; G[Expected false positives among rejections stays at or below q]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A Worked Example
&lt;/h3&gt;

&lt;p&gt;Take 10 metrics on an experiment and a target FDR of &lt;code&gt;q = 0.10&lt;/code&gt;. Sort the p-values ascending, compute each BH critical value &lt;code&gt;(k / 10) * 0.10 = k * 0.01&lt;/code&gt;, and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rank k   p-value    BH critical (k/m)*q    p &amp;lt;= critical?
  1      0.001         0.010                yes
  2      0.008         0.020                yes
  3      0.012         0.030                yes
  4      0.021         0.040                yes
  5      0.030         0.050                yes
  6      0.041         0.060                yes
  7      0.060         0.070                yes   &amp;lt;-- largest passing rank
  8      0.200         0.080                no
  9      0.350         0.090                no
 10      0.600         0.100                no

Largest k with p(k) &amp;lt;= (k/m)*q  is  k = 7.
Benjamini-Hochberg rejects ranks 1 through 7  -&amp;gt;  7 discoveries.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice rank 7: its p-value is 0.060, which is &lt;em&gt;above&lt;/em&gt; the naive 0.05 threshold, yet BH still declares it significant. That is the procedure working as designed — the weight of six even-smaller p-values below it earns rank 7 a place. Now compare the same data under a strict Bonferroni correction, which would test every metric against &lt;code&gt;0.10 / 10 = 0.01&lt;/code&gt;: only ranks 1 (0.001) and 2 (0.008) clear that bar, for &lt;strong&gt;2 discoveries&lt;/strong&gt; instead of 7. Same data, same 10% error budget — BH finds more than three times as many real effects because it spends that budget across the whole set instead of on each test in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  FDR Control vs Bonferroni Correction
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Bonferroni correction&lt;/strong&gt; is the best-known multiple-comparisons fix: divide your significance level by the number of tests, so with 20 metrics at 5% each metric must clear 0.05 / 20 = 0.0025. It controls the family-wise error rate, and it is trivially simple. It is also badly suited to A/B testing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Bonferroni&lt;/th&gt;
&lt;th&gt;Benjamini-Hochberg (FDR)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Controls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Family-wise error rate&lt;/td&gt;
&lt;td&gt;False discovery rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed: &lt;code&gt;alpha / m&lt;/code&gt; for every test&lt;/td&gt;
&lt;td&gt;Sloped: &lt;code&gt;(k / m) * q&lt;/code&gt;, rank-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;As metrics grow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cutoff shrinks fast; power collapses&lt;/td&gt;
&lt;td&gt;Cutoff adapts; power preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;False negatives&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many — misses real winners&lt;/td&gt;
&lt;td&gt;Far fewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Right context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A few make-or-break tests&lt;/td&gt;
&lt;td&gt;Many metrics, exploratory testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Bonferroni's flaw in this setting is that it treats every additional metric as equally threatening and pays for it with a cutoff that shrinks toward zero. With 50 metrics, each must beat 0.001 — so real, moderate effects get thrown out alongside the noise, and your false-negative rate soars. FDR control accepts a small, known fraction of false positives in exchange for keeping the power to detect the effects that matter. For an experimentation program that lives or dies by finding true wins across a broad metric set, that is the correct trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Optimizely's Stats Engine Controls the False Discovery Rate
&lt;/h2&gt;

&lt;p&gt;Optimizely's Stats Engine applies false discovery rate control automatically. Per Optimizely's documentation, the Stats Engine "provides a data-rich view of visitor interactions, includes confidence intervals, and applies &lt;a href="https://support.optimizely.com/hc/en-us/articles/39028383356813-Optimizely-Experiment-Results-page" rel="noopener noreferrer"&gt;false discovery rate control&lt;/a&gt;" across the metrics on the Experiment Results page. You do not configure it, sort p-values, or apply a correction by hand — it is built into how significance is reported. For the broader mechanics of how significance and always-valid inference are computed, see &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;How the Optimizely Stats Engine Works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two documented properties are worth internalizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing more does not raise your false-positive risk.&lt;/strong&gt; Optimizely states that the "Stats Engine adjusts statistical significance for every metric and variation, so testing more does not raise the risk of false positives." That is the FDR guarantee in plain language — the correction scales with how many comparisons the results page carries, so adding a guardrail does not silently inflate your chance of a phantom winner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The primary metric is protected.&lt;/strong&gt; On the results page, "the primary metric's statistical significance is calculated independently from all other metrics and monitoring goals." The tiering matters: your headline result reaches significance as fast as possible, while secondary and monitoring metrics absorb the multiple-comparisons adjustment. Adding many low-improvement secondary metrics slows &lt;em&gt;their&lt;/em&gt; time to significance, but never the primary's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why the &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289762189-Change-the-statistical-significance-setting-in-Optimizely-Experimentation" rel="noopener noreferrer"&gt;Change the statistical significance setting&lt;/a&gt; documentation can promise that carrying a wide metric set — including a full complement of &lt;a href="https://optipilot.com/data/guardrail-metrics" rel="noopener noreferrer"&gt;guardrail metrics&lt;/a&gt; — does not cost you in false alarms. FDR control is what makes a broad, honest results page statistically safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do Not Change Metrics Mid-Experiment
&lt;/h3&gt;

&lt;p&gt;There is one operational rule that follows directly from how FDR control is computed. Optimizely controls the false discovery rate over "metrics currently appearing on the Experiment Results page." Its &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410283825421-Edit-a-metric" rel="noopener noreferrer"&gt;Edit a metric&lt;/a&gt; documentation warns that changing the metrics during a running experiment "alters the number and nature of the hypotheses being tested, which can affect the false discovery rate control calculations," and that "excessive metric changes after an experiment or campaign has started may invalidate Optimizely's guarantees regarding false detection."&lt;/p&gt;

&lt;p&gt;The reason is exactly the BH mechanism above: the critical values depend on &lt;code&gt;m&lt;/code&gt;, the number of hypotheses. Add or swap metrics after data has started arriving and you retroactively change &lt;code&gt;m&lt;/code&gt;, which shifts every threshold and reopens the door to the cherry-picking FDR control was meant to close. Decide your metric set before launch and freeze it — the same discipline that protects you from the &lt;a href="https://optipilot.com/data/winners-curse-regression-to-the-mean" rel="noopener noreferrer"&gt;winner's curse&lt;/a&gt; and from &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing&lt;/a&gt; peeking traps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Implications for Your Experiments
&lt;/h2&gt;

&lt;p&gt;Understanding FDR control changes a few concrete habits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the metrics you need, not fewer.&lt;/strong&gt; Because the Stats Engine controls FDR across the results page, you can carry a healthy set of guardrails and secondaries without inflating false positives. Trim metrics for clarity, not out of statistical fear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock the metric set at launch.&lt;/strong&gt; Write your primary, secondary, and guardrail metrics into the experiment brief before traffic starts, and do not edit them mid-flight. Changing &lt;code&gt;m&lt;/code&gt; mid-experiment undermines the FDR guarantee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the primary metric first.&lt;/strong&gt; It is computed independently and reaches significance fastest. Treat secondary and monitoring metrics as supporting evidence whose significance the FDR adjustment deliberately makes more conservative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Power the experiment for the metrics that matter.&lt;/strong&gt; FDR control makes false positives rare, but it cannot rescue an underpowered test from false negatives. Size the experiment properly — see &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;A/B test sample size and statistical power&lt;/a&gt; — so you can actually detect the effects you care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate the traffic split.&lt;/strong&gt; No error-rate control matters if the experiment is broken at the instrumentation level. Check for &lt;a href="https://optipilot.com/data/sample-ratio-mismatch" rel="noopener noreferrer"&gt;sample ratio mismatch&lt;/a&gt; before you trust any significance number on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a good false discovery rate to target?
&lt;/h3&gt;

&lt;p&gt;For most online experimentation, controlling the FDR at 5% to 10% is standard — the same range as conventional significance levels. A 10% FDR means you accept that, on average, up to one in ten of your flagged discoveries may be a false positive, in exchange for substantially more power to detect real effects. Optimizely ties this to your project-wide statistical significance setting rather than asking you to specify an FDR directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the false discovery rate the same as the false positive rate?
&lt;/h3&gt;

&lt;p&gt;No, and the distinction is important. The false positive rate (or Type I error rate) is the probability of a false positive among all &lt;em&gt;truly null&lt;/em&gt; tests — it looks backward from the ground truth. The false discovery rate is the expected fraction of false positives among the results you &lt;em&gt;declared significant&lt;/em&gt; — it looks forward from your discoveries. FDR is the more actionable quantity because it describes the reliability of the conclusions you actually act on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use Bonferroni correction?
&lt;/h3&gt;

&lt;p&gt;Bonferroni controls the family-wise error rate by dividing your significance level across all tests, which makes the threshold shrink rapidly as you add metrics. With many metrics it becomes so conservative that it discards real effects along with the noise, driving up false negatives. Benjamini-Hochberg controls the false discovery rate instead, adapting the threshold to the data and preserving far more power — a better fit for the many-metric reality of A/B testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does tracking more metrics in Optimizely increase my false positive risk?
&lt;/h3&gt;

&lt;p&gt;No. Optimizely's documentation states that the Stats Engine "adjusts statistical significance for every metric and variation, so testing more does not raise the risk of false positives." The false discovery rate control scales with the number of comparisons on the results page. Adding metrics can lengthen the time for secondary metrics to reach significance, but it does not inflate your overall false-positive risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does changing metrics mid-experiment break FDR control?
&lt;/h3&gt;

&lt;p&gt;Benjamini-Hochberg critical values depend on the number of hypotheses being tested. When you add, remove, or edit metrics on a running experiment, you change that number retroactively, which shifts the thresholds the Stats Engine already applied and can invalidate Optimizely's guarantees against false detection. Decide your metric set before launch and keep it fixed for the life of the experiment.&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>abtesting</category>
      <category>datascience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Experiment Design for A/B Testing: A Practical Framework</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:05:59 +0000</pubDate>
      <link>https://dev.to/david_sert/experiment-design-for-ab-testing-a-practical-framework-1b33</link>
      <guid>https://dev.to/david_sert/experiment-design-for-ab-testing-a-practical-framework-1b33</guid>
      <description>&lt;p&gt;An A/B test can be implemented perfectly and still answer the wrong question. Experiment design is the written contract that connects a business decision to a causal comparison: who is eligible, what is randomized, what changes, how outcomes are measured, and what evidence will trigger action.&lt;/p&gt;

&lt;p&gt;This guide turns those choices into a one-page specification, then maps the same design to Optimizely Web Experimentation and Feature Experimentation. It complements the deeper guides to an &lt;a href="https://optipilot.com/strategy/ab-testing-hypothesis-template-optimizely" rel="noopener noreferrer"&gt;A/B testing hypothesis&lt;/a&gt;, &lt;a href="https://optipilot.com/data/ab-testing-metrics-framework-oec-optimizely" rel="noopener noreferrer"&gt;experiment metrics&lt;/a&gt;, &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;sample size and statistical power&lt;/a&gt;, &lt;a href="https://optipilot.com/data/how-long-to-run-ab-test" rel="noopener noreferrer"&gt;test duration&lt;/a&gt;, and &lt;a href="https://optipilot.com/strategy/optimizely-experiment-qa-checklist" rel="noopener noreferrer"&gt;pre-launch QA&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment design is the blueprint for a causal answer
&lt;/h2&gt;

&lt;p&gt;The output of experiment design is not a list of variation ideas. It is a precise description of the comparison that will support a decision. Optimizely's own &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410282997005-Create-a-basic-experiment-plan" rel="noopener noreferrer"&gt;basic experiment plan&lt;/a&gt; asks why the experiment runs, where and when variations run, who sees them, what changes, and how success is measured. A rigorous design adds the unit of randomization, estimand, power assumptions, and decision rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Business decision] --&amp;gt; B[Causal question]
  B --&amp;gt; C[Unit and eligibility]
  C --&amp;gt; D[Control and treatment]
  D --&amp;gt; E[Metrics and estimand]
  E --&amp;gt; F[MDE, sample, and duration]
  F --&amp;gt; G[Optimizely configuration]
  G --&amp;gt; H[QA and launch]
  H --&amp;gt; I[Ship, iterate, stop, or conclude]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start with the decision the result must support
&lt;/h3&gt;

&lt;p&gt;Write the action before the hypothesis. A useful decision is specific enough that a result can change it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ship the checkout recommendation module to all eligible customers if it produces a practically valuable increase in completed orders without materially increasing refund rate; otherwise iterate or stop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Turn that decision into one falsifiable causal question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Among eligible signed-in shoppers, what is the effect of showing the recommendation module during checkout, compared with the current checkout, on completed orders within seven days of assignment?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This order prevents a common failure: designing an attractive variation, choosing an available metric, and only later deciding what the test was meant to prove.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the estimand before the dashboard metric
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;estimand&lt;/strong&gt; is the effect the team wants to estimate. Define five elements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Checkout recommendation example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Population&lt;/td&gt;
&lt;td&gt;Signed-in shoppers who begin checkout in supported markets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treatment&lt;/td&gt;
&lt;td&gt;Checkout with the recommendation module enabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comparator&lt;/td&gt;
&lt;td&gt;Current production checkout with the module absent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome&lt;/td&gt;
&lt;td&gt;Orders completed within seven days of assignment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time window&lt;/td&gt;
&lt;td&gt;Seven days from the first eligible checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use an intention-to-treat estimand by default: analyze people according to the variation to which they were assigned, even if rendering fails or they do not engage with the module. This preserves the benefit of random assignment and estimates the effect of the policy as deployed.&lt;/p&gt;

&lt;p&gt;An exposure-only analysis answers a different question because it conditions on behavior or technical events that may happen after assignment. It can be useful as a diagnostic, but it should not silently replace the primary analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Know when an experiment is the wrong tool
&lt;/h3&gt;

&lt;p&gt;Do not randomize merely because the platform makes it possible. An experiment is a poor choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No plausible outcome would change the decision.&lt;/li&gt;
&lt;li&gt;Available traffic cannot detect an effect worth acting on within a useful timeframe.&lt;/li&gt;
&lt;li&gt;The treatment cannot be assigned independently enough to create a credible control.&lt;/li&gt;
&lt;li&gt;Random assignment would be unethical, unlawful, or operationally unacceptable.&lt;/li&gt;
&lt;li&gt;Instrumentation cannot distinguish assignment, exposure, and outcomes reliably.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When assignment cannot be randomized but a decision still needs evidence, use a quasi-experimental design and state its assumptions explicitly. That design should not be presented as equivalent to a randomized test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the experimental unit and assignment mechanism
&lt;/h2&gt;

&lt;p&gt;The experimental unit is the entity independently assigned to control or treatment. It determines what must remain stable, where interference can occur, and which observations are independent in the analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  User, session, account, device, or cluster
&lt;/h3&gt;

&lt;p&gt;Match the unit to how the treatment is delivered and how people can influence one another:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Unit&lt;/th&gt;
&lt;th&gt;Appropriate when&lt;/th&gt;
&lt;th&gt;Main risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;A durable identity receives an individual experience&lt;/td&gt;
&lt;td&gt;The same person appears under multiple IDs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session&lt;/td&gt;
&lt;td&gt;The treatment is intentionally temporary and session-contained&lt;/td&gt;
&lt;td&gt;Repeat sessions receive inconsistent experiences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Account&lt;/td&gt;
&lt;td&gt;Members share settings, workflows, or outcomes&lt;/td&gt;
&lt;td&gt;Too few accounts for adequate power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;The experience is device-specific and identity is unavailable&lt;/td&gt;
&lt;td&gt;Cross-device contamination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster&lt;/td&gt;
&lt;td&gt;Locations, teams, or networks can affect one another&lt;/td&gt;
&lt;td&gt;Correlated outcomes reduce effective &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a B2B account-level recommendation feature, randomizing individual members can contaminate the comparison: one member may configure the experience for colleagues, or team members may discuss it. Assign the account, then analyze with methods that respect account-level assignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep assignment stable
&lt;/h3&gt;

&lt;p&gt;Use a durable identifier for the unit of randomization. If the treatment must persist across browser sessions and services, a transient session ID is not sufficient. Document how anonymous identities merge with authenticated identities and what happens when a user changes devices.&lt;/p&gt;

&lt;p&gt;Optimizely Web Experimentation uses deterministic bucketing: it hashes a user ID or cookie with MurmurHash, so the same identifier maps to the same variation while the relevant configuration remains stable. Optimizely also warns that some traffic changes can rebucket visitors; the official &lt;a href="https://docs.developers.optimizely.com/web-experimentation/docs/how-bucketing-works" rel="noopener noreferrer"&gt;bucketing guide&lt;/a&gt; recommends avoiding allocation changes after results accumulate.&lt;/p&gt;

&lt;p&gt;The randomization unit is not necessarily the metric denominator. A user-randomized experiment may measure revenue per eligible user, orders per user, or average order value among orders. Each denominator answers a different question and must be fixed in the measurement plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detect spillovers and network effects
&lt;/h3&gt;

&lt;p&gt;Random assignment does not prevent treated units from changing control units' outcomes. Before launch, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can users communicate about or share the treatment?&lt;/li&gt;
&lt;li&gt;Does treatment change inventory, marketplace liquidity, queue time, or prices for everyone?&lt;/li&gt;
&lt;li&gt;Can one account member alter settings seen by other members?&lt;/li&gt;
&lt;li&gt;Does a recommender learn from behavior pooled across variations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If interference is plausible, assign clusters that contain the spillover, use a switchback or marketplace-specific design, or redefine the estimand. Record the remaining interference risk rather than assuming it away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define control, treatment, and eligibility precisely
&lt;/h2&gt;

&lt;p&gt;A reproducible comparison needs versioned experiences and frozen entry rules. “Business as usual” and “all users” are not adequate specifications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the control a real production baseline
&lt;/h3&gt;

&lt;p&gt;Describe the control as thoroughly as the treatment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User interface and copy.&lt;/li&gt;
&lt;li&gt;Code path and service dependencies.&lt;/li&gt;
&lt;li&gt;Default feature-flag values.&lt;/li&gt;
&lt;li&gt;Existing personalization or recommendation logic.&lt;/li&gt;
&lt;li&gt;Analytics events expected from the baseline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the checkout example, the control is “the current checkout with no recommendation component and no recommendation API call,” not simply “original.” This matters if production changes while the experiment is being prepared.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change one causal idea at a time
&lt;/h3&gt;

&lt;p&gt;One hypothesis can require several implementation edits. Adding a recommendation module may change layout, make an API request, add loading behavior, and introduce new tracking. Those edits express one causal idea if the intended decision is whether to deploy the module as a package.&lt;/p&gt;

&lt;p&gt;If the decision is which component causes the effect—placement, copy, image, or algorithm—use separate experiments or a &lt;a href="https://optipilot.com/data/multivariate-testing-vs-ab-testing" rel="noopener noreferrer"&gt;multivariate test&lt;/a&gt;. A bundled treatment cannot identify the effect of each ingredient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Freeze inclusion and exclusion rules
&lt;/h3&gt;

&lt;p&gt;Define eligibility before anyone inspects results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target pages, routes, or flag call sites.&lt;/li&gt;
&lt;li&gt;Geography, language, device, browser, and app version.&lt;/li&gt;
&lt;li&gt;Account tier, authentication state, and product entitlement.&lt;/li&gt;
&lt;li&gt;Prior exposure and concurrent-experiment exclusions.&lt;/li&gt;
&lt;li&gt;Employee, bot, fraud, and QA-traffic handling.&lt;/li&gt;
&lt;li&gt;The exact moment a unit enters the experiment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changing eligibility after viewing outcomes creates a new analysis population. If a safety issue forces a change, record the time and configuration, then decide whether to restart rather than blending incompatible cohorts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the measurement plan
&lt;/h2&gt;

&lt;p&gt;Metrics operationalize the estimand. They should be specified as formulas and event rules, not only selected by name in a dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose one primary metric
&lt;/h3&gt;

&lt;p&gt;The primary metric must be affected by the treatment, interpretable by stakeholders, and tied to the ship decision. Define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Numerator and denominator.&lt;/li&gt;
&lt;li&gt;Aggregation unit.&lt;/li&gt;
&lt;li&gt;Attribution or conversion window.&lt;/li&gt;
&lt;li&gt;Event source and deduplication key.&lt;/li&gt;
&lt;li&gt;Direction of improvement.&lt;/li&gt;
&lt;li&gt;Practical effect threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the checkout example, “conversion” is ambiguous. A better definition is “distinct eligible users with at least one completed order within seven days divided by distinct assigned eligible users.”&lt;/p&gt;

&lt;p&gt;Optimizely Web Experimentation treats the first metric added as the primary metric; its &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289104013-Steps-to-create-an-experiment" rel="noopener noreferrer"&gt;experiment setup guide&lt;/a&gt; recommends choosing an event directly affected by the change. Put the decision metric first deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add restrained secondary and guardrail metrics
&lt;/h3&gt;

&lt;p&gt;Secondary metrics explain mechanism or downstream impact. For the recommendation module, useful secondary measures might include recommendation clicks, units per order, and revenue per eligible user.&lt;/p&gt;

&lt;p&gt;Guardrails describe harms that can veto a nominal win: refund rate, checkout latency, payment errors, support contacts, or gross margin. State each veto rule in advance. A dashboard containing dozens of metrics invites selective interpretation; include only metrics with a defined role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instrument assignment and outcomes separately
&lt;/h3&gt;

&lt;p&gt;An outcome event is not proof that assignment or exposure worked. Validate three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assignment:&lt;/strong&gt; the unit was bucketed into a variation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposure:&lt;/strong&gt; the intended experience was actually delivered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome:&lt;/strong&gt; the conversion or guardrail event occurred and was attributed correctly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Specify how the pipeline handles duplicate events, bots, missing identifiers, delayed events, identity merges, and events arriving after the experiment stops. Run an A/A or staged validation when the cost of a broken decision pipeline is high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for enough information, not merely significance
&lt;/h2&gt;

&lt;p&gt;A design should collect enough information to make the business decision. Statistical significance alone does not establish that an effect is large enough, safe enough, or measured without bias.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick an MDE worth acting on
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://optipilot.com/data/minimum-detectable-effect" rel="noopener noreferrer"&gt;minimum detectable effect&lt;/a&gt; (MDE) should reflect the smallest effect that would justify implementation and opportunity cost. Derive it from baseline performance, expected value, engineering cost, risk, and competing roadmap items.&lt;/p&gt;

&lt;p&gt;Do not inflate the MDE simply to make the required sample fit available traffic. That creates a faster test by declaring smaller—but potentially valuable—effects invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculate sample size and calendar duration
&lt;/h3&gt;

&lt;p&gt;Use the baseline rate, MDE, significance threshold, desired power, and number of variations to estimate the required sample. Then convert eligible sample into calendar time using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The share of traffic satisfying eligibility.&lt;/li&gt;
&lt;li&gt;Total experiment allocation.&lt;/li&gt;
&lt;li&gt;Variation traffic distribution.&lt;/li&gt;
&lt;li&gt;Weekly and seasonal cycles.&lt;/li&gt;
&lt;li&gt;Ramp time and planned outages.&lt;/li&gt;
&lt;li&gt;Conversion delay and data-processing latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;sample-size guide&lt;/a&gt; covers the calculation; the design document should retain the inputs and result. The &lt;a href="https://optipilot.com/data/how-long-to-run-ab-test" rel="noopener noreferrer"&gt;duration guide&lt;/a&gt; explains why reaching a nominal sample early does not erase calendar-cycle or delayed-conversion requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Predefine the stopping and decision rules
&lt;/h3&gt;

&lt;p&gt;Write the complete rule before launch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Statistical method and confidence policy.&lt;/li&gt;
&lt;li&gt;Minimum runtime and information requirement.&lt;/li&gt;
&lt;li&gt;Practical-effect threshold or acceptable interval.&lt;/li&gt;
&lt;li&gt;Guardrail vetoes.&lt;/li&gt;
&lt;li&gt;Safety-stop conditions.&lt;/li&gt;
&lt;li&gt;Treatment of inconclusive results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not stop because a preferred variation briefly looks significant, change the primary metric after seeing results, or segment repeatedly until a favorable result appears. Optimizely Stats Engine supports ongoing results monitoring, but operational visibility is not permission to rewrite the design after outcomes are known.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map the experiment design into Optimizely
&lt;/h2&gt;

&lt;p&gt;The same causal design can be implemented in either Web Experimentation or Feature Experimentation. The platform configuration enforces parts of the design; it does not choose the question, unit, estimand, MDE, or decision threshold for the team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start from one design card
&lt;/h3&gt;

&lt;p&gt;Use a one-page design card as the source of truth:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Checkout recommendation design&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decision&lt;/td&gt;
&lt;td&gt;Ship, iterate, or stop the module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Causal question&lt;/td&gt;
&lt;td&gt;Effect of offering recommendations on seven-day order completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Population&lt;/td&gt;
&lt;td&gt;Signed-in checkout starters in supported markets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomization unit&lt;/td&gt;
&lt;td&gt;Durable user ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control&lt;/td&gt;
&lt;td&gt;Current checkout without recommendation call or component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treatment&lt;/td&gt;
&lt;td&gt;Checkout with recommendation module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary metric&lt;/td&gt;
&lt;td&gt;Seven-day completed-order users per assigned user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secondary metrics&lt;/td&gt;
&lt;td&gt;Recommendation clicks, units per order, revenue per user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guardrails&lt;/td&gt;
&lt;td&gt;Refund rate, checkout errors, p95 latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allocation&lt;/td&gt;
&lt;td&gt;100% of eligible traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution&lt;/td&gt;
&lt;td&gt;50% control, 50% treatment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Precomputed minimum sample plus complete weekly cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision rule&lt;/td&gt;
&lt;td&gt;Practical lift and statistical evidence with no guardrail veto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  D[Shared experiment design card]
  D --&amp;gt; W1[Web: URL or saved page]
  D --&amp;gt; W2[Web: audience]
  D --&amp;gt; W3[Web: original and variation]
  D --&amp;gt; W4[Web: allocation, distribution, metrics]
  D --&amp;gt; F1[Feature: flag and variables]
  D --&amp;gt; F2[Feature: attributes and audience]
  D --&amp;gt; F3[Feature: A/B test rule]
  D --&amp;gt; F4[Feature: decide, events, metrics]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web Experimentation mapping
&lt;/h3&gt;

&lt;p&gt;The current official &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289104013-Steps-to-create-an-experiment" rel="noopener noreferrer"&gt;Web Experimentation workflow&lt;/a&gt; maps the design as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design field&lt;/th&gt;
&lt;th&gt;Web Experimentation object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where treatment can occur&lt;/td&gt;
&lt;td&gt;Target By URL or Target By Saved Pages; page triggers and conditions where needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eligible population&lt;/td&gt;
&lt;td&gt;Audience conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control and treatment&lt;/td&gt;
&lt;td&gt;Original and variation under Design &amp;gt; Variations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random assignment&lt;/td&gt;
&lt;td&gt;Design &amp;gt; Traffic Allocation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary and supporting outcomes&lt;/td&gt;
&lt;td&gt;Track &amp;gt; Metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-launch evidence&lt;/td&gt;
&lt;td&gt;Preview, testing, Summary, and pre-launch review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set total traffic allocation to 100% when every eligible visitor should enter the experiment, then distribute that traffic 50/50 between original and treatment. Optimizely distinguishes the eligible share entering the test (&lt;strong&gt;allocation&lt;/strong&gt;) from the share assigned to each variation (&lt;strong&gt;distribution&lt;/strong&gt;) in its &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289350029-Update-traffic-allocation-and-distribution" rel="noopener noreferrer"&gt;traffic documentation&lt;/a&gt;. Freeze both before launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature Experimentation mapping
&lt;/h3&gt;

&lt;p&gt;Feature Experimentation implements the same design in application code and a flag ruleset:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design field&lt;/th&gt;
&lt;th&gt;Feature Experimentation object&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where treatment can occur&lt;/td&gt;
&lt;td&gt;Flag evaluation call site&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control and treatment&lt;/td&gt;
&lt;td&gt;Flag variations and variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eligible population&lt;/td&gt;
&lt;td&gt;User-context attributes and audience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Random assignment&lt;/td&gt;
&lt;td&gt;A/B Test flag rule, allocation, and distribution mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exposure&lt;/td&gt;
&lt;td&gt;SDK &lt;code&gt;decide&lt;/code&gt; call and its decision event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcomes&lt;/td&gt;
&lt;td&gt;Tracked events configured as metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Optimizely's official &lt;a href="https://support.optimizely.com/hc/en-us/articles/4410289860493-Get-started-with-Optimizely-Feature-Experimentation" rel="noopener noreferrer"&gt;Feature Experimentation getting-started guide&lt;/a&gt; describes the sequence: create a flag, implement it with &lt;code&gt;decide&lt;/code&gt;, define an audience and flag rule, then analyze the Experiment Results page. The newer &lt;a href="https://support.optimizely.com/hc/en-us/articles/38937573368589-Run-A-B-tests-in-Feature-Experimentation" rel="noopener noreferrer"&gt;A/B test configuration guide&lt;/a&gt; adds the hypothesis, test plan, traffic allocation, metrics, distribution mode, and flag variations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;optimizelyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUserContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;durableUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;account_tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountTier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout_recommendations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variationKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;treatment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderCheckout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The durable user ID implements user-level assignment. Attributes implement declared eligibility. The flag key defines the decision point, while the returned variation selects the control or treatment code path. Track the completed-order event with the same user context so the result can be attributed to the decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve the analysis contract in Collaboration
&lt;/h3&gt;

&lt;p&gt;If the organization uses Optimizely Collaboration, store the structured design alongside the work. The official &lt;a href="https://support.optimizely.com/hc/en-us/articles/16504341933069-Manage-hypotheses" rel="noopener noreferrer"&gt;hypothesis documentation&lt;/a&gt; provides a Brief tab for a structured plan, a Variations tab for images or links, an Experiment tab for linking the implementation, and History for an activity trail.&lt;/p&gt;

&lt;p&gt;Attach the design card, metric definitions, MDE inputs, QA evidence, risks, and assumptions. Collaboration can preserve the contract and make changes visible; it does not repair a design that was never specified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a pre-launch design review
&lt;/h2&gt;

&lt;p&gt;Review the design before launch, when weaknesses can still be fixed without contaminating results. Separate validity from feasibility so “we can build it” is not mistaken for “it will answer the question.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Validity review
&lt;/h3&gt;

&lt;p&gt;Challenge the causal comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selection:&lt;/strong&gt; Are eligibility rules evaluated before treatment, and are exclusions symmetrical?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spillover:&lt;/strong&gt; Can treated units alter control outcomes?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attrition:&lt;/strong&gt; Can missing outcomes differ by variation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Novelty:&lt;/strong&gt; Could a temporary reaction dominate the intended long-run effect?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrumentation:&lt;/strong&gt; Can assignment, exposure, and outcomes be reconciled?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent tests:&lt;/strong&gt; Can another experiment interact with this treatment or metric?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; Can one randomization unit receive multiple IDs or variations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Record mitigations and residual risks. Randomization balances expected pre-treatment differences; it does not fix differential loss, broken event collection, or post-assignment filtering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feasibility review
&lt;/h3&gt;

&lt;p&gt;Confirm the operating plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Required sample and realistic calendar duration.&lt;/li&gt;
&lt;li&gt;Engineering, design, analytics, and QA ownership.&lt;/li&gt;
&lt;li&gt;Event latency and conversion window.&lt;/li&gt;
&lt;li&gt;Rollout and rollback procedure.&lt;/li&gt;
&lt;li&gt;Safety monitoring and incident owner.&lt;/li&gt;
&lt;li&gt;Browser, device, and environment coverage.&lt;/li&gt;
&lt;li&gt;Production baseline freeze and change control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the design cannot be executed as written, revise it before exposure rather than improvising after launch.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-page experiment-design checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Decision, causal question, and hypothesis are explicit.&lt;/li&gt;
&lt;li&gt;Population, randomization unit, and analysis unit are distinct.&lt;/li&gt;
&lt;li&gt;Control and treatment are versioned production specifications.&lt;/li&gt;
&lt;li&gt;Eligibility, exclusions, and prior-exposure rules are frozen.&lt;/li&gt;
&lt;li&gt;One primary metric has a complete formula and conversion window.&lt;/li&gt;
&lt;li&gt;Secondary metrics explain mechanism; guardrails have veto rules.&lt;/li&gt;
&lt;li&gt;Assignment, exposure, and outcome events can be reconciled.&lt;/li&gt;
&lt;li&gt;MDE, sample, duration, and stopping rule are recorded.&lt;/li&gt;
&lt;li&gt;Optimizely pages or flags, audiences, traffic, variations, and metrics match the design.&lt;/li&gt;
&lt;li&gt;QA evidence, rollback owner, and launch approval are documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the &lt;a href="https://optipilot.com/strategy/optimizely-experiment-qa-checklist" rel="noopener noreferrer"&gt;Optimizely experiment QA checklist&lt;/a&gt; to turn the approved design into a production launch review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is experiment design in A/B testing?
&lt;/h3&gt;

&lt;p&gt;Experiment design is the specification that connects a decision to a causal comparison. It defines the population, randomization unit, eligibility, control, treatment, metrics, power assumptions, duration, and decision rule before traffic is exposed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the randomization unit the same as the analysis unit?
&lt;/h3&gt;

&lt;p&gt;Not always. A test can randomize accounts and observe users or transactions, but the analysis must account for dependence within each randomized account. Treating correlated observations as independently randomized overstates the amount of information in the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should an A/B test change only one page element?
&lt;/h3&gt;

&lt;p&gt;It should test one causal idea, which may require several coordinated implementation changes. If the decision requires isolating the contribution of individual elements or their interactions, use separate experiments or a multivariate design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can traffic allocation change after an experiment starts?
&lt;/h3&gt;

&lt;p&gt;Operationally, Optimizely permits traffic changes, but they can affect bucketing and interpretation. Predefine allocation and distribution, avoid changing them after results accumulate, and document any safety-driven intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Optimizely replace an experiment design document?
&lt;/h3&gt;

&lt;p&gt;No. Optimizely implements pages or flags, audiences, variations, traffic rules, and metrics. The team must still define the causal question, unit, estimand, MDE, duration, risks, and action threshold. The configuration should be a faithful implementation of that prior design.&lt;/p&gt;

&lt;p&gt;Use a &lt;a href="https://optipilot.com/data/multivariate-testing-vs-ab-testing" rel="noopener noreferrer"&gt;multivariate test&lt;/a&gt; when one decision depends on the separate effect of multiple components.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>productmanagement</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Running Multiple A/B Tests at Once: When Overlap Is Safe</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Thu, 20 Aug 2026 05:04:11 +0000</pubDate>
      <link>https://dev.to/david_sert/running-multiple-ab-tests-at-once-when-overlap-is-safe-1dbf</link>
      <guid>https://dev.to/david_sert/running-multiple-ab-tests-at-once-when-overlap-is-safe-1dbf</guid>
      <description>&lt;p&gt;Every experimentation program hits this question within its first few months: test A is running on the homepage, the checkout team wants to launch test B — do we wait? Run them together? Will one contaminate the other? Teams that answer "always isolate" grind their velocity to a halt. Teams that answer "never worry" eventually ship a result that was quietly distorted by a colliding experiment.&lt;/p&gt;

&lt;p&gt;The correct answer is more useful than either extreme: overlap is usually fine, the risky cases are identifiable in advance, and when you do need isolation, Optimizely gives you exclusion groups to enforce it deterministically. This guide covers when overlapping A/B tests are safe, when they are not, and exactly how to configure mutually exclusive experiments in both Optimizely Web Experimentation and Feature Experimentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Short Answer: Overlap Is Usually Safe
&lt;/h2&gt;

&lt;p&gt;By default, Optimizely lets experiments overlap — a single visitor can be in test A and test B at the same time. Bucketing uses an independent hash per experiment, so being in A's variation says nothing about which variation of B you will get.&lt;/p&gt;

&lt;p&gt;That independence is exactly what makes overlap statistically tolerable. Because visitors from A's control and A's variation flow into B's control and B's variation &lt;em&gt;proportionally&lt;/em&gt;, any influence test A has on behavior spreads evenly across test B's arms. It adds a little noise, but it does not systematically favor one of B's variations. For most pairs of experiments — different pages, different flows, different metrics — the interaction risk is negligible and forcing isolation would only slow both tests down.&lt;/p&gt;

&lt;p&gt;This is also Optimizely's own guidance: make experiments mutually exclusive only when required, because every exclusion group splits your traffic and &lt;a href="https://optipilot.com/data/how-long-to-run-ab-test" rel="noopener noreferrer"&gt;extends how long every test inside it needs to run&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is an Interaction Effect?
&lt;/h2&gt;

&lt;p&gt;An interaction effect occurs when a user's exposure to one experiment changes how they respond to another, in a way that does not average out. The classic failure mode is two treatments that are individually fine but incoherent together.&lt;/p&gt;

&lt;p&gt;A concrete example: test A changes your homepage hero to emphasize a discount. Test B redesigns the checkout to remove the promo-code field. A visitor who saw A's discount messaging and then hits B's variation checkout — where the promised promo field is gone — abandons at an elevated rate. That abandonment shows up in B's results as "the redesign loses," when in reality the redesign only loses &lt;em&gt;in combination with&lt;/em&gt; A's variation. Neither team can see this from their own results page.&lt;/p&gt;

&lt;p&gt;Interactions distort in proportion to how strongly the two experiences collide. Two tests on unrelated pages measured on unrelated metrics essentially cannot produce this pattern; two tests mutating the same funnel step almost cannot avoid it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Overlap Is Risky: Three Red Flags
&lt;/h2&gt;

&lt;p&gt;Optimizely's guidance identifies the situations where interaction effects become likely. Treat these as your pre-launch review checklist for any pair of concurrent tests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Red flag&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Same page or application area&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Both treatments compose visually and functionally — variations can conflict outright&lt;/td&gt;
&lt;td&gt;Two tests both modifying the pricing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Same user flow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An upstream change alters who reaches the downstream test and in what state&lt;/td&gt;
&lt;td&gt;A signup test and an onboarding test in one funnel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shared conversion metric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Both tests claim credit for movement in the same number&lt;/td&gt;
&lt;td&gt;Two tests both optimizing checkout completion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If none of the three apply, run the tests concurrently and spend your energy elsewhere. If one or more apply, you have a decision to make — and it has more than two options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Four Options for Colliding Tests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A{Same page, funnel,&amp;lt;br/&amp;gt;or primary metric?} -- No --&amp;gt; B[Run concurrently&amp;lt;br/&amp;gt;no isolation needed]
    A -- Yes --&amp;gt; C{Same element,&amp;lt;br/&amp;gt;same hypothesis area?}
    C -- Yes --&amp;gt; D[Combine into one test&amp;lt;br/&amp;gt;with more variations]
    C -- No --&amp;gt; E{Can one wait?}
    E -- Yes --&amp;gt; F[Run sequentially]
    E -- No --&amp;gt; G[Exclusion group&amp;lt;br/&amp;gt;mutual exclusion]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run concurrently.&lt;/strong&gt; The default. Full traffic to both tests, fastest combined learning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine into one experiment.&lt;/strong&gt; If two ideas target the same element toward the same goal, they are not two tests — they are one test with more variations. This also measures the combination explicitly instead of leaving it to chance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run sequentially.&lt;/strong&gt; Zero interaction risk and zero configuration, at the cost of calendar time. Sensible when one test is short or clearly higher &lt;a href="https://optipilot.com/strategy/how-to-prioritize-ab-tests" rel="noopener noreferrer"&gt;priority&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutual exclusion.&lt;/strong&gt; Both tests run simultaneously, but no visitor sees both. The traffic cost is real: two tests in a 50/50 exclusion group each get half your traffic, so each takes roughly twice as long to reach significance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mutual Exclusion in Optimizely Web: Exclusion Groups
&lt;/h2&gt;

&lt;p&gt;In Web Experimentation, mutual exclusion is implemented through &lt;strong&gt;exclusion groups&lt;/strong&gt;. The mechanics are worth understanding precisely, because they explain the operating rules that follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groups are evaluated before page activation and audience targeting.&lt;/strong&gt; The moment a visitor touches the snippet, Optimizely deterministically assigns them to one experiment's slice of the group — before checking whether they qualify for that experiment's URL or audience conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocation is by weight.&lt;/strong&gt; If Experiment A has 70% of the group and Experiment B has 30%, every visitor lands in exactly one slice. A visitor in A's slice can never enter B, and vice versa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assignment is sticky and deterministic.&lt;/strong&gt; Returning visitors re-enter the same slice; each group, experiment, and campaign uses its own bucketing ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group membership does not guarantee participation.&lt;/strong&gt; A visitor allocated to Experiment A's slice must still pass A's page and audience conditions to actually enter the experiment. If they fail, they see nothing — they do not fall through to Experiment B.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the subtle one: exclusion groups spend traffic on visitors who never enter any experiment. Combined with the slice split itself, this is why Optimizely recommends using groups only when genuinely required.&lt;/p&gt;

&lt;p&gt;One shortcut worth knowing: if two experiments already target mutually exclusive audiences — "Android users" and "iPhone users" — they can never overlap, and no exclusion group is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mutual Exclusion in Feature Experimentation
&lt;/h2&gt;

&lt;p&gt;Feature Experimentation implements the same concept with a cleaner surface. Exclusion groups live in their own tab on the Flags dashboard, and &lt;strong&gt;no SDK changes are required&lt;/strong&gt; — the &lt;code&gt;Decide&lt;/code&gt; method automatically respects group membership when assigning users.&lt;/p&gt;

&lt;p&gt;To set it up: create the group under &lt;strong&gt;Flags → Exclusion Groups → Create New Exclusion Group&lt;/strong&gt; (name it, pick the environment), then attach each experiment from its flag rule: &lt;strong&gt;Configure Rule → "Add this experiment to the following exclusion group"&lt;/strong&gt;, and set the percentage of the group's traffic that experiment receives.&lt;/p&gt;

&lt;p&gt;One detail matters for debugging surprises: in FX's bucketing order, exclusion groups are evaluated &lt;em&gt;after&lt;/em&gt; forced variations, user allowlists, the user profile service, and audience targeting — but &lt;em&gt;before&lt;/em&gt; traffic allocation. A forced variation or allowlist entry will happily put a user into an experiment their exclusion-group slice says they should never see. If a "mutually exclusive" user somehow appears in two experiments, check for forced bucketing first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rules That Keep an Exclusion Group Valid
&lt;/h2&gt;

&lt;p&gt;Exclusion groups only deliver their guarantee if the group's composition stays fixed for its entire lifetime. Optimizely is explicit about the failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start every experiment in the group together, and never add one mid-flight.&lt;/strong&gt; Adding an experiment to a group that is already running shifts the traffic allocation ranges. A visitor who was bucketed into Experiment A can be silently reassigned to Experiment B's slice — they have now been exposed to both, which is precisely what the group existed to prevent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never remove a running experiment either.&lt;/strong&gt; The remaining experiments' bucketing ranges shift to fill the gap, reassigning visitors between experiments and contaminating both result sets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicating an experiment drops its group membership.&lt;/strong&gt; A copied experiment starts with no exclusion group attached — easy to miss if you build tests by duplication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiments in the group do not need to **&lt;/strong&gt;&lt;em&gt;end&lt;/em&gt;**** together** — but the group must contain all of them from the first launch until the last one finishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat an exclusion group like a &lt;a href="https://optipilot.com/data/guardrail-metrics" rel="noopener noreferrer"&gt;pre-registered decision&lt;/a&gt;: configured before launch, frozen during flight.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Detect an Interaction Effect After the Fact
&lt;/h2&gt;

&lt;p&gt;Suppose two tests ran concurrently without isolation and you now suspect a collision. Detection is possible but harder than prevention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Segment one experiment's results by the other's variations.&lt;/strong&gt; If Experiment B's lift differs materially between visitors in A's control and A's variation, you have evidence of an interaction. In Optimizely Web you can debug live assignment with &lt;code&gt;?optimizely_log=debug&lt;/code&gt; (look for &lt;code&gt;Group&lt;/code&gt; entries) or inspect &lt;code&gt;window.optimizely.get('data').groups&lt;/code&gt;, whose &lt;code&gt;weightDistributions&lt;/code&gt; property shows the group's allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect to be underpowered.&lt;/strong&gt; Splitting an experiment's sample four ways (A-control/B-control, A-control/B-variant, ...) slashes your sensitivity — a real interaction can easily hide below the noise floor. An inconclusive segmentation is not proof of safety. This is the same power arithmetic covered in &lt;a href="https://optipilot.com/data/minimum-detectable-effect" rel="noopener noreferrer"&gt;choosing a minimum detectable effect&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check validity first.&lt;/strong&gt; Before attributing a weird result to an interaction, rule out the boring explanations: a &lt;a href="https://optipilot.com/data/sample-ratio-mismatch" rel="noopener noreferrer"&gt;sample ratio mismatch&lt;/a&gt; or a &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;peeking-driven false positive&lt;/a&gt; is far more common than a true interaction effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest conclusion from the detection math: if a collision would genuinely matter, design it away up front — with a group, a sequence, or a combined test — rather than hoping to catch it in analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolating everything.&lt;/strong&gt; The most expensive failure mode. A program that puts every test in one giant exclusion group divides its traffic across all of them and multiplies every test's duration. Reserve isolation for the three red flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the traffic bill.&lt;/strong&gt; A 50/50 exclusion group doubles both tests' runtimes. Before creating one, check whether your &lt;a href="https://optipilot.com/tools/velocity-calculator" rel="noopener noreferrer"&gt;testing velocity&lt;/a&gt; can afford it — sequential execution is sometimes faster in wall-clock terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editing a live group.&lt;/strong&gt; Adding or removing experiments mid-flight reassigns visitors and quietly breaks both experiments. If a test must join, wait for the group's current experiments to finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming group membership means participation.&lt;/strong&gt; Visitors allocated to an experiment's slice who fail its audience conditions see nothing. Budget for that loss when sizing the group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting exclusion against forced bucketing (FX).&lt;/strong&gt; Forced variations, allowlists, and the user profile service override exclusion groups in the bucketing order. Audit these before concluding your isolation is broken.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Decision in One Paragraph
&lt;/h2&gt;

&lt;p&gt;Let concurrent tests overlap by default — independent bucketing spreads any influence proportionally, and the cost of isolation is real traffic and real calendar time. Escalate to mutual exclusion only when two tests share a page, a funnel, or a primary metric, and consider combining or sequencing them before reaching for a group. When you do create an exclusion group — in Web via exclusion groups, in Feature Experimentation via the Flags dashboard — freeze its membership from first launch to last finish. Isolation is a scalpel, not a default.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>experimentation</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Sequential Testing in A/B Tests: When Peeking Is Safe</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:11:19 +0000</pubDate>
      <link>https://dev.to/david_sert/sequential-testing-in-ab-tests-when-peeking-is-safe-25l8</link>
      <guid>https://dev.to/david_sert/sequential-testing-in-ab-tests-when-peeking-is-safe-25l8</guid>
      <description>&lt;p&gt;You launch a test, and by mid-afternoon the variation is up 8% with a green "&lt;a href="https://optipilot.com/tools/statistical-significance-calculator" rel="noopener noreferrer"&gt;statistically significant&lt;/a&gt;" badge. The obvious question is also the dangerous one: can you call it now, or do you have to wait? On most classical A/B testing setups the honest answer is that you were never supposed to be looking yet, and acting on that early peek quietly wrecks your error rate. Optimizely's Stats Engine is built specifically so that the answer flips to "yes, you can trust this look" — and the reason why is worth understanding before you ship a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why peeking breaks a fixed-horizon test
&lt;/h2&gt;

&lt;p&gt;A classical, fixed-horizon A/B test (the kind powered by a t-test or z-test) makes one promise: if you fix the sample size in advance, collect exactly that many visitors, and test &lt;strong&gt;once&lt;/strong&gt; at the end, then your false-positive rate is capped at your significance level — typically 5%. Every part of that promise depends on testing a single time at a pre-committed sample size.&lt;/p&gt;

&lt;p&gt;Peeking violates the promise. Each time you evaluate significance on accumulating data, you give random noise another independent chance to cross the threshold. The p-value of an A/A test (two identical variations) wanders up and down as visitors trickle in; over a long enough experiment it will dip below 0.05 purely by chance, even though there is nothing to find. Stop the moment it does, and you have "found" a winner that does not exist. This is the &lt;strong&gt;optional stopping&lt;/strong&gt; problem, and it is a specific instance of the multiple-comparisons problem: more looks means more opportunities to be fooled.&lt;/p&gt;

&lt;p&gt;The inflation is not subtle. Optimizely ran the experiment on itself — simulating A/A tests (no real difference) at 5,000 visitors and letting an observer peek. More than &lt;strong&gt;57% of those A/A tests falsely declared a winner or loser at least once&lt;/strong&gt;. Checking every 500 visitors produced a false-declaration rate around &lt;strong&gt;26%&lt;/strong&gt;; checking every 1,000 visitors, around &lt;strong&gt;20%&lt;/strong&gt;. Even applying a naive post-hoc correction still left error rates hovering near &lt;strong&gt;25%&lt;/strong&gt;. A test you believed was operating at 5% risk was actually running at four to eleven times that, entirely because someone looked early and acted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Fpeeking-false-positives.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Fpeeking-false-positives.svg" alt="Line chart showing the actual false-positive rate of a fixed-horizon A/B test rising from 5% at a single look to roughly 25% after 20 looks, while a sequential always-valid test stays flat at the nominal 5% regardless of how many times results are checked." width="720" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is why a fixed-horizon test needs a pre-computed sample size, and why our &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size calculator&lt;/a&gt; exists: under fixed-horizon rules, the sample size is a contract you sign before the experiment starts and are not allowed to renegotiate by watching the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the peeking problem in a short simulation
&lt;/h2&gt;

&lt;p&gt;If the effect feels abstract, simulate it. The script below runs many A/A experiments — control and treatment drawn from the &lt;em&gt;same&lt;/em&gt; 10% conversion rate — and checks a standard two-proportion z-test at 20 interim looks, declaring a "winner" the first time p &amp;lt; 0.05.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;peeked_aa_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;looks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One A/A experiment. Returns True if it EVER crosses significance.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;  &lt;span class="c1"&gt;# identical conversion rate for both arms — there is no real effect
&lt;/span&gt;    &lt;span class="n"&gt;control&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_max&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;looks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;looks&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;c_sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;treatment&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;pooled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c_sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t_sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;se&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pooled&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pooled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;se&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t_sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c_sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;se&lt;/span&gt;
        &lt;span class="n"&gt;p_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p_value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# stop the first time it "looks significant"
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;trials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;
&lt;span class="n"&gt;false_positives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;peeked_aa_test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trials&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20 peeks: false-positive rate = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;false_positives&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;trials&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# A single look at the final sample would sit near 5%.
# Peeking 20 times typically pushes this into the ~20–30% range.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single-look version of this test behaves: evaluate only at &lt;code&gt;n_max&lt;/code&gt; and the false-positive rate sits near 5%, exactly as advertised. Add the interim looks and it climbs into the 20–30% range — the same phenomenon Optimizely measured on real traffic. Nothing about the data changed; only the number of looks did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed-horizon, Bayesian, and sequential testing
&lt;/h2&gt;

&lt;p&gt;There are three broad ways to analyze an experiment, and Optimizely supports all three. A baking analogy (Optimizely's own) makes the trade-off concrete before the mechanics do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frequentist (fixed-horizon)&lt;/strong&gt; — You must decide the bake time before the cake goes in. You wait for the timer; opening the oven early and pulling the cake out ruins it, even if it looks done, because it may be raw inside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bayesian&lt;/strong&gt; — You can open the oven whenever you like. Each look updates your belief about whether the cake is done, and you act on the probability accumulated so far. You are never told the cake is definitively "finished" — only how likely it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential&lt;/strong&gt; — You can open the oven whenever you like &lt;em&gt;and&lt;/em&gt; get a trustworthy verdict each time. If it looks done, it is done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction that matters for peeking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Frequentist (fixed-horizon)&lt;/th&gt;
&lt;th&gt;Bayesian&lt;/th&gt;
&lt;th&gt;Sequential&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sample size calculation&lt;/td&gt;
&lt;td&gt;Required before starting&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peeking mid-test&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Early stopping&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result style&lt;/td&gt;
&lt;td&gt;Significance / confidence intervals&lt;/td&gt;
&lt;td&gt;Probability statements&lt;/td&gt;
&lt;td&gt;Frequentist-style significance and confidence intervals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Optimizely's Stats Engine is the &lt;strong&gt;sequential&lt;/strong&gt; option. It gives you the continuous-monitoring freedom of a Bayesian approach while still reporting the frequentist-style significance and confidence intervals most experimentation teams already reason about. For a fuller treatment of the engine itself, see &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;How the Optimizely Stats Engine works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are weighing fixed-horizon, Bayesian, and sequential analysis for a specific test, the &lt;a href="https://optipilot.com/tools/stats-method-picker" rel="noopener noreferrer"&gt;stats method picker&lt;/a&gt; walks you through the trade-offs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[Starting an experiment] --&amp;gt; B{Can you fix a sample size in advance and not look until it is reached?}
  B --&amp;gt;|Yes, and you have strong stats expertise| C[Frequentist fixed-horizon]
  B --&amp;gt;|No, you want to monitor continuously| D{Which result style do you prefer?}
  D --&amp;gt;|Direct probability statements| E[Bayesian]
  D --&amp;gt;|Significance and confidence intervals| F[Sequential - Optimizely Stats Engine]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What always-valid inference actually means
&lt;/h2&gt;

&lt;p&gt;The property that makes peeking safe has a precise name: &lt;strong&gt;always-valid inference&lt;/strong&gt;. A p-value (or confidence interval) is "always valid" if it holds its guarantee no matter &lt;em&gt;when&lt;/em&gt; you choose to look and stop. With an always-valid p-value, you can check the test after every visitor, stop the first time it drops below your threshold, and your Type I (false-positive) error is still bounded by that threshold. The decision rule is safe under optional stopping by construction.&lt;/p&gt;

&lt;p&gt;The mechanism, drawn from the academic work behind Stats Engine (Johari, Pekelis, and Walsh's &lt;em&gt;Always Valid Inference&lt;/em&gt;, and the KDD paper &lt;em&gt;Peeking at A/B Tests&lt;/em&gt;), is a &lt;strong&gt;mixture Sequential Probability Ratio Test (mSPRT)&lt;/strong&gt;, an idea tracing back to Robbins in 1970. Rather than computing a fresh single-shot p-value at each look, the engine maintains a running likelihood ratio that accumulates evidence across the whole stream of visitors. The always-valid p-value is essentially the reciprocal of that accumulated evidence, and the test is what Optimizely calls a &lt;strong&gt;"test of power one"&lt;/strong&gt;: the p-value now represents the chance the experiment will &lt;em&gt;ever&lt;/em&gt; cross the significance threshold under the null. Because that quantity already accounts for every possible future look, looking early costs you nothing.&lt;/p&gt;

&lt;p&gt;Note the attribution: mSPRT is the term used in the underlying literature. Optimizely's product documentation describes the same mechanism in plainer language ("an average likelihood ratio calculated every time a new visitor triggers an event") without naming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How sequential testing preserves the error rate
&lt;/h2&gt;

&lt;p&gt;The fixed-horizon test fails under peeking because its 5% guarantee was only ever a promise about a single look. The sequential test makes a stronger promise from the start: the error bound holds across the &lt;em&gt;entire sequence&lt;/em&gt; of looks, treated as one continuous procedure. Instead of spending your whole 5% error budget at one pre-chosen moment, the engine spreads and controls it across all looks simultaneously.&lt;/p&gt;

&lt;p&gt;The visible consequence, documented by Optimizely, is that statistical significance in a stable experiment should generally &lt;strong&gt;increase over time&lt;/strong&gt; as evidence accumulates, rather than fluctuate. Early on — when the sample is small — large observed gaps are treated conservatively, because a big early swing is exactly what noise produces. As the same-direction difference persists over more visitors, the engine becomes willing to call it. Two forms of evidence drive significance up: larger conversion-rate differences, and differences that &lt;em&gt;persist&lt;/em&gt; across more visitors.&lt;/p&gt;

&lt;p&gt;Real traffic is not a clean simulation, so significance can occasionally fall. Optimizely reports this happens in roughly 4% of experiments, from two causes: an early run of data that later looks like noise, or a genuine mid-experiment change in conditions. For the latter, Stats Engine has a protective &lt;strong&gt;stats reset&lt;/strong&gt; that can drop significance sharply (potentially to 0%) when it detects that the underlying data-generating process has shifted. That is the safety mechanism doing its job, not a bug. Reducing metric variance up front — for example with &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;CUPED variance reduction&lt;/a&gt; — is the cleaner way to reach a confident verdict sooner, rather than hoping an early peek holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  False discovery rate control across metrics and variations
&lt;/h2&gt;

&lt;p&gt;Peeking over time is one multiplicity problem. Testing many metrics and variations at once is another, and Optimizely handles it with a second layer: &lt;a href="https://optipilot.com/data/false-discovery-rate-control" rel="noopener noreferrer"&gt;&lt;strong&gt;false discovery rate&lt;/strong&gt;&lt;/a&gt;** (FDR) control**.&lt;/p&gt;

&lt;p&gt;Classical significance controls the false-positive rate across &lt;em&gt;all&lt;/em&gt; goals and variations. But you do not implement all of them — you implement the ones that won. Optimizely's documentation gives the sharp example: nine inconclusive results and one false winner is a 10% false-positive rate across everything, but if you only ship winners and one of two winners is false, your rate of shipping a false result is 50%. That "share of your declared winners that are actually null" is the &lt;strong&gt;false discovery rate&lt;/strong&gt;, and it is the number that maps to a real business mistake.&lt;/p&gt;

&lt;p&gt;Stats Engine controls FDR using a &lt;strong&gt;tiered Benjamini-Hochberg procedure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your &lt;strong&gt;primary metric&lt;/strong&gt; is evaluated independently of the others, so it retains full &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;statistical power&lt;/a&gt; and reaches significance as fast as possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary metrics&lt;/strong&gt; (ranked 2–5) have their thresholds adjusted for the number of metrics and variations; adding more of them slows each one down but never slows the primary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring metrics&lt;/strong&gt; (ranked beyond 5) each get a fractional weight of 1/n, so they contribute diagnostic signal without dragging on the metrics you actually decide on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical takeaway: &lt;strong&gt;rank your metrics deliberately.&lt;/strong&gt; The metric your decision hinges on belongs in the primary slot. One important caveat from the docs — FDR control is &lt;em&gt;not&lt;/em&gt; maintained when you segment results. The deeper you slice, the more the false-positive risk climbs, so treat segments as exploration, not as a basis for decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[New visitor event] --&amp;gt; B[Update running likelihood ratio]
  B --&amp;gt; C[Recompute always-valid p-value and FDR-adjusted significance]
  C --&amp;gt; D{Threshold crossed?}
  D --&amp;gt;|No| E[Keep running - the current read is still valid]
  E --&amp;gt; A
  D --&amp;gt;|Yes| F[Declare winner or loser - safe to stop and act]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When you can stop the test
&lt;/h2&gt;

&lt;p&gt;Here is the direct answer to "am I allowed to stop when it looks significant?" With Optimizely's Stats Engine, &lt;strong&gt;yes&lt;/strong&gt; — a variation that has crossed your significance threshold on the primary metric is a valid result you can act on, with a few practitioner caveats:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do a launch sanity check first.&lt;/strong&gt; Statistical validity does not catch a broken implementation. Early in the test, confirm the variation renders correctly, events fire, and traffic split roughly matches your allocation (a sample ratio mismatch usually signals a setup bug, and no statistical engine repairs biased data).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide on the metric you ranked primary.&lt;/strong&gt; Secondary and monitoring metrics are context, not the verdict. See &lt;a href="https://optipilot.com/data/optimizely-results-page" rel="noopener noreferrer"&gt;reading the Optimizely results page&lt;/a&gt; for how each is displayed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weigh practical significance, not just statistical significance.&lt;/strong&gt; A result can be real and still too small to justify the engineering cost of shipping. Check that the confidence (improvement) interval clears the effect size you actually care about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let it run a full business cycle when you can.&lt;/strong&gt; Statistical validity at any look is not the same as representativeness. If your traffic behaves differently on weekends or across a purchase cycle, a Tuesday-only sample may be valid but unrepresentative. Covering at least one full weekly cycle guards against novelty and day-of-week effects. (This is practitioner best practice, not a statistical requirement of the engine.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Within those guardrails, you do not owe the test a pre-computed sample size, and you are not penalized for looking. That is the entire point of sequential inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common misconceptions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Peeking is always cheating."&lt;/strong&gt; Peeking breaks &lt;em&gt;fixed-horizon&lt;/em&gt; tests. Under an always-valid method it is a designed-in feature. The sin is peeking with the wrong statistical engine, not peeking itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Sequential testing is just Bayesian testing."&lt;/strong&gt; Both allow continuous monitoring, but they answer different questions. Bayesian testing reports the probability a variation is better given a prior; sequential testing reports frequentist significance and confidence intervals that stay valid under optional stopping. Optimizely's Stats Engine is sequential, not Bayesian.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Significance dropping means the tool is broken."&lt;/strong&gt; In a stable experiment, significance generally climbs. A drop usually reflects a real change in conditions or an early false signal correcting itself — and a stats reset is the safeguard, not a defect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Always-valid means I can ignore sample size entirely."&lt;/strong&gt; You can skip the &lt;em&gt;pre-commitment&lt;/em&gt; to a sample size, but low-traffic tests still take real time to accumulate evidence, and tiny true effects still need many visitors. Always-valid inference removes the peeking penalty; it does not remove the need for data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I can slice the winning segment and trust it the same way."&lt;/strong&gt; FDR control is not maintained across segments. A segment that looks like a standout is a hypothesis for a future test, not a result to ship.&lt;/p&gt;

&lt;p&gt;The short version: with a fixed-horizon test you must set the timer before you bake and never open the oven. With Optimizely's sequential Stats Engine you can open the oven whenever you like — and when the result looks done, it genuinely is.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>datascience</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Multi-Armed Bandit Testing: How It Works and When to Use</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:10:43 +0000</pubDate>
      <link>https://dev.to/david_sert/multi-armed-bandit-testing-how-it-works-and-when-to-use-1ldd</link>
      <guid>https://dev.to/david_sert/multi-armed-bandit-testing-how-it-works-and-when-to-use-1ldd</guid>
      <description>&lt;p&gt;A multi-armed bandit is an algorithm that decides how to split traffic across several variations while a test is still running, shifting more visitors toward whichever option is performing best. Instead of holding an even split until a fixed &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size&lt;/a&gt; is reached, a bandit continuously reallocates traffic to earn conversions during the test itself. This guide explains how multi-armed bandits work, the core algorithms behind them (epsilon-greedy, Thompson sampling, and Upper Confidence Bound), the explore-exploit tradeoff that governs every bandit, when a bandit beats a fixed A/B test, and how Optimizely implements bandit optimization.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Multi-Armed Bandit?
&lt;/h2&gt;

&lt;p&gt;The name comes from slot machines. A single slot machine is nicknamed a "one-armed bandit" because it has one lever and it tends to take your money. Now imagine a row of slot machines, each with a different unknown payout rate. You have a fixed number of pulls and one goal: win as much as possible. Every pull forces a dilemma. Do you keep pulling the machine that has paid out best so far, or do you try the others in case one of them is secretly better? That is the multi-armed bandit problem, and it maps directly onto experimentation. Each "arm" is a variation, each "pull" is a visitor, and the "payout" is a conversion.&lt;/p&gt;

&lt;p&gt;A fixed A/B test answers this dilemma by refusing to answer it during the test. It sends an equal share of traffic to every variation until enough data accumulates to declare a winner with statistical confidence, then you deploy the winner afterward. A multi-armed bandit answers it continuously. As evidence builds that one variation converts better, the bandit routes more traffic to it in real time, so fewer visitors are sent to the underperforming options while the test runs.&lt;/p&gt;

&lt;p&gt;This changes the objective. A fixed A/B test optimizes for &lt;strong&gt;learning&lt;/strong&gt;: it wants a precise, unbiased estimate of how each variation performs so you can make a confident decision. A bandit optimizes for &lt;strong&gt;earning&lt;/strong&gt;: it wants to maximize total conversions across the whole population of visitors, even at the cost of a cleaner statistical readout. Understanding that tradeoff is the key to knowing when to reach for a bandit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Explore-Exploit Tradeoff
&lt;/h2&gt;

&lt;p&gt;Every bandit balances two competing impulses on every decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exploit&lt;/strong&gt; – Serve the variation that currently looks best, to capture conversions now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore&lt;/strong&gt; – Serve other variations, to gather more data in case the current leader is not actually the best.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pure exploitation is dangerous. If an inferior variation happens to get lucky in its first few hundred visitors, a greedy algorithm that only exploits will lock onto it and never discover its mistake. Pure exploration is wasteful: it is just a fixed A/B test that keeps sending traffic to known losers. A good bandit algorithm blends the two, exploring enough to stay confident about which arm is best while exploiting enough to bank conversions along the way.&lt;/p&gt;

&lt;p&gt;The metric that formalizes this is &lt;strong&gt;regret&lt;/strong&gt;: the cumulative difference between the conversions you would have earned if you had always served the truly best variation and the conversions you actually earned. A bandit's job is to minimize regret. Early on, regret accrues quickly because the algorithm does not yet know which arm is best. As evidence accumulates and the bandit concentrates traffic on the leader, the rate of regret slows. The whole design of a bandit algorithm is a strategy for driving regret as low as possible, as fast as possible.&lt;/p&gt;

&lt;p&gt;The allocation loop below runs continuously for the life of a bandit experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Split traffic evenly] --&amp;gt; B[Serve a variation]
  B --&amp;gt; C[Observe conversion outcome]
  C --&amp;gt; D[Update value estimate per arm]
  D --&amp;gt; E{Explore or exploit?}
  E --&amp;gt;|Explore| F[Sample under-tested arms]
  E --&amp;gt;|Exploit| G[Send more traffic to the leader]
  F --&amp;gt; B
  G --&amp;gt; B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the loop never stops updating, a bandit adapts if conditions change mid-test. If a variation that was losing starts winning (for example, because a new audience arrives), the value estimates shift and traffic follows. A fixed A/B test cannot do this: its allocation is frozen for the duration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bandit Algorithms: Epsilon-Greedy, Thompson Sampling, and UCB
&lt;/h2&gt;

&lt;p&gt;"Multi-armed bandit" names the problem, not a single algorithm. Several algorithms solve it, and they differ mainly in &lt;em&gt;how&lt;/em&gt; they decide to explore versus exploit. Three are worth knowing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Epsilon-Greedy
&lt;/h3&gt;

&lt;p&gt;Epsilon-greedy is the simplest bandit. You pick a small exploration rate, epsilon (for example, 0.1). On each visitor, the algorithm flips a biased coin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With probability epsilon, it &lt;strong&gt;explores&lt;/strong&gt; by serving a random variation.&lt;/li&gt;
&lt;li&gt;With probability 1 minus epsilon, it &lt;strong&gt;exploits&lt;/strong&gt; by serving the variation with the highest observed conversion rate so far.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With epsilon set to 0.1, roughly one visitor in ten sees a random variation and the other nine see the current leader. Epsilon-greedy is easy to reason about and easy to implement, but it has two weaknesses. It explores blindly, wasting some of its exploration budget on variations that are clearly bad, and a fixed epsilon keeps exploring at the same rate forever even after the best arm is obvious. A common fix is &lt;strong&gt;epsilon-decreasing&lt;/strong&gt;, where epsilon shrinks over time so the algorithm explores heavily at first and exploits more as confidence grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Thompson Sampling
&lt;/h3&gt;

&lt;p&gt;Thompson sampling is a Bayesian approach and the one most modern platforms favor. Instead of tracking a single conversion-rate estimate per variation, it maintains a full probability distribution over each variation's true conversion rate. For a binary conversion metric, that distribution is a Beta distribution updated from the counts of conversions and non-conversions each arm has seen.&lt;/p&gt;

&lt;p&gt;On each decision, Thompson sampling draws one random sample from every arm's distribution and serves whichever arm produced the highest sample. The elegance is that exploration falls out automatically. An arm with little data has a wide distribution, so it sometimes produces a high sample and gets explored; an arm with lots of data has a narrow distribution tightly centered on its true rate. Over many decisions, each arm receives traffic roughly in proportion to the probability that it is the best arm. No epsilon to tune, and exploration self-adjusts as certainty grows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Illustrative only - demonstrates the Thompson Sampling concept,
# not any specific platform's production implementation.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="c1"&gt;# (conversions, non_conversions) observed per variation
&lt;/span&gt;&lt;span class="n"&gt;arms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;388&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# 12 conversions in 400 visitors
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;370&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# 30 conversions in 400 visitors
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;382&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;choose_variation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;best_arm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;non_conv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Sample the arm's plausible conversion rate from its Beta posterior
&lt;/span&gt;        &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;betavariate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;non_conv&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;best_arm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best_arm&lt;/span&gt;

&lt;span class="c1"&gt;# Arm B wins most draws, but A and C are still explored
# whenever their sampled rate happens to come out on top.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Upper Confidence Bound (UCB)
&lt;/h3&gt;

&lt;p&gt;UCB follows the principle of "optimism in the face of uncertainty." For each arm it computes the observed conversion rate plus a bonus that grows with how uncertain that estimate is. An arm that has been shown to few visitors carries a large uncertainty bonus, so UCB is optimistic about it and explores it; an arm shown to many visitors carries a small bonus, so it is judged mostly on its actual performance. On each decision, UCB simply serves the arm with the highest upper confidence bound. As every arm accumulates data, the bonuses shrink and the algorithm converges on exploiting the genuinely best arm. Unlike Thompson sampling, UCB is deterministic given the same data, which makes its behavior easy to audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bandit vs Fixed A/B Test
&lt;/h2&gt;

&lt;p&gt;A bandit and a fixed-horizon A/B test are built for different jobs. The table below summarizes the practical differences.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Fixed A/B test&lt;/th&gt;
&lt;th&gt;Multi-armed bandit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary objective&lt;/td&gt;
&lt;td&gt;Learn which variation is best (unbiased estimate)&lt;/td&gt;
&lt;td&gt;Maximize conversions during the test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic allocation&lt;/td&gt;
&lt;td&gt;Fixed and even until conclusion&lt;/td&gt;
&lt;td&gt;Dynamic; shifts toward the leader in real time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statistical significance&lt;/td&gt;
&lt;td&gt;Reaches significance and quantifies the effect&lt;/td&gt;
&lt;td&gt;Does not target or report significance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect-size estimate&lt;/td&gt;
&lt;td&gt;Precise, with confidence intervals&lt;/td&gt;
&lt;td&gt;Biased toward the winner; weaker per-arm reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Durable decisions and roadmap learnings&lt;/td&gt;
&lt;td&gt;Short-lived opportunities; many low-stakes options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Losing variations&lt;/td&gt;
&lt;td&gt;Keep receiving traffic until the test ends&lt;/td&gt;
&lt;td&gt;Starved of traffic as evidence accumulates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The core trade is precision for opportunity cost. A fixed A/B test pays a known price - it deliberately sends half its traffic to a variation that may be worse - in exchange for a clean, trustworthy measurement of exactly how much better the winner is. A bandit reduces that opportunity cost by steering traffic to the leader early, but it sacrifices the clean measurement: because allocation is entangled with performance, you cannot read an unbiased effect size off a bandit the way you can off a controlled A/B test. If you plan test duration with a &lt;a href="https://optipilot.com/data/ab-test-sample-size-statistical-power" rel="noopener noreferrer"&gt;sample-size and statistical power&lt;/a&gt; calculation and care about the &lt;a href="https://optipilot.com/data/minimum-detectable-effect" rel="noopener noreferrer"&gt;minimum detectable effect&lt;/a&gt;, you are thinking in fixed-A/B-test terms, and that is usually the right default.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Multi-Armed Bandit Beats a Fixed A/B Test
&lt;/h2&gt;

&lt;p&gt;A bandit is the better tool when maximizing conversions &lt;em&gt;during&lt;/em&gt; the test matters more than measuring the exact difference between variations. Reach for a bandit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The opportunity is short-lived.&lt;/strong&gt; For a weekend sale, a holiday banner, or a news-driven promotion, there is no "afterward" in which to deploy the winner. Whatever conversions you earn, you earn during the campaign. A bandit captures them; a fixed A/B test spends the whole window collecting evidence you will never act on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are choosing among many low-stakes options.&lt;/strong&gt; Testing ten subject lines or fifteen hero images as a full A/B test needs enormous traffic to power every comparison. A bandit concentrates traffic on the front-runners and quietly starves the losers, earning more conversions without waiting for every arm to reach significance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You do not need a defensible effect size.&lt;/strong&gt; If the decision is "serve the best headline," not "prove headline B lifts conversions by 4.2% for the quarterly readout," the bandit's biased estimates are an acceptable price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stick with a fixed A/B test when the opposite is true: when you need a trustworthy, quantified result for a durable decision, when the change is high-stakes or hard to reverse, when you want a clean learning to generalize to future work, or when peeking and early stopping are the real concern - in which case &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing&lt;/a&gt; is the tool designed for valid early looks, not a bandit. The decision comes down to a single question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A[New test] --&amp;gt; B{Need a precise, defensible&amp;lt;br/&amp;gt;effect size?}
  B --&amp;gt;|Yes| C[Fixed A/B test]
  B --&amp;gt;|No| D{Is the window short&amp;lt;br/&amp;gt;or are there many options?}
  D --&amp;gt;|Yes| E[Multi-armed bandit]
  D --&amp;gt;|No| C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Optimizely Implements Multi-Armed Bandits
&lt;/h2&gt;

&lt;p&gt;Optimizely groups its experiment types by objective. A/B tests, multivariate tests, and Stats Accelerator are built for &lt;em&gt;learning&lt;/em&gt; and incorporate statistical significance, while multi-armed bandit and contextual bandit optimizations are built for &lt;em&gt;immediate impact&lt;/em&gt; and optimize traffic allocation without performing significance analysis. Optimizely states plainly that a multi-armed bandit "does not rely on a fixed sample size or equal traffic allocation," and that bandits "do not achieve statistical significance because their goal is to optimize traffic allocation dynamically to maximize conversions." (&lt;a href="https://support.optimizely.com/hc/en-us/articles/45515379407757-Contextual-bandits-FAQ" rel="noopener noreferrer"&gt;Optimizely: Contextual bandits FAQ&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-armed bandit optimization
&lt;/h3&gt;

&lt;p&gt;In a standard Optimizely multi-armed bandit, you choose a primary metric and the platform displays variations to visitors, then routes more traffic toward whichever variation has the greatest impact on that metric. Optimizely's own example is capitalizing on conversions "during a short cycle like a weekend sale" - exactly the short-window case where a bandit shines. (&lt;a href="https://support.optimizely.com/hc/en-us/articles/45515322959117-Contextual-bandits" rel="noopener noreferrer"&gt;Optimizely: Contextual bandits&lt;/a&gt;) Because a MAB optimizes for impact rather than significance, the Results page shows an &lt;strong&gt;Improvement&lt;/strong&gt; figure for the variation over the baseline, but it does not declare a statistically significant winner the way a manual A/B test does. (&lt;a href="https://support.optimizely.com/hc/en-us/articles/39028383356813-Optimizely-Experiment-Results-page" rel="noopener noreferrer"&gt;Optimizely: Experiment Results page&lt;/a&gt;) Bandits also do not use the sticky bucketing that keeps an A/B-test visitor in the same variation across sessions, since a bandit is continuously reallocating traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exploration and exploitation rates
&lt;/h3&gt;

&lt;p&gt;Optimizely exposes the explore-exploit tradeoff directly as two rates. The &lt;strong&gt;exploration rate&lt;/strong&gt; is the probability the system serves a random variation (Optimizely calls this the "learning rate"), and the &lt;strong&gt;exploitation rate&lt;/strong&gt; is the probability it serves the best-performing variation. When set to automatic, the exploration rate starts high and decreases as the model gathers more events - the same epsilon-decreasing idea described above, managed for you. (&lt;a href="https://support.optimizely.com/hc/en-us/articles/45515379407757-Contextual-bandits-FAQ" rel="noopener noreferrer"&gt;Optimizely: Contextual bandits FAQ&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  Stats Accelerator and contextual bandits
&lt;/h3&gt;

&lt;p&gt;Optimizely offers two neighboring tools that are easy to confuse with a plain bandit. &lt;strong&gt;Stats Accelerator&lt;/strong&gt; reallocates traffic to help an experiment reach a statistically significant result &lt;em&gt;faster&lt;/em&gt; - it optimizes for learning speed, not pure earning, and it keeps significance in play. A &lt;strong&gt;contextual bandit&lt;/strong&gt; (CMAB) goes the other direction: rather than finding one best variation for everyone, it personalizes the variation per visitor using user attributes, running server-side behind a prediction API. Like a plain MAB, a contextual bandit does not report statistical significance. (&lt;a href="https://support.optimizely.com/hc/en-us/articles/45515322959117-Contextual-bandits" rel="noopener noreferrer"&gt;Optimizely: Contextual bandits&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing an Allocation Method
&lt;/h2&gt;

&lt;p&gt;The right choice depends on whether you are optimizing for learning, earning, or personalization. Because Optimizely gives you a manual A/B test, Stats Accelerator, a multi-armed bandit, and a contextual bandit, picking among them is its own decision. For a full side-by-side of those four allocation methods and a decision framework for choosing between them, see the companion guide: &lt;a href="https://optipilot.com/data/stats-accelerator-vs-mab-vs-contextual-bandit" rel="noopener noreferrer"&gt;Stats Accelerator vs MAB vs contextual bandit&lt;/a&gt;. If your reason for reaching past a fixed A/B test is speed to a trustworthy result rather than raw conversions, two complementary techniques are worth combining with your A/B tests instead of a bandit: &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;CUPED&lt;/a&gt; reduces variance so tests reach significance faster, and Optimizely's &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;Stats Engine&lt;/a&gt; provides always-valid significance so you can act on results sooner without inflating false positives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does a multi-armed bandit reach statistical significance?
&lt;/h3&gt;

&lt;p&gt;No. A multi-armed bandit is designed to maximize conversions by dynamically reallocating traffic, not to test a hypothesis, so it does not rely on a fixed sample size or equal allocation and does not report statistical significance. If you need a defensible, quantified effect size, run a fixed A/B test instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a multi-armed bandit the same as an A/B test?
&lt;/h3&gt;

&lt;p&gt;No. Both compare variations, but an A/B test holds an even split to measure each variation precisely, while a bandit continuously shifts traffic toward the leader to earn more conversions during the test. An A/B test optimizes for learning; a bandit optimizes for earning.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between epsilon-greedy and Thompson sampling?
&lt;/h3&gt;

&lt;p&gt;Epsilon-greedy explores at a fixed rate by serving a random variation a set fraction of the time, exploiting the current best otherwise. Thompson sampling explores adaptively by sampling from each variation's probability distribution, so uncertain variations get explored more and exploration shrinks automatically as data accumulates. Thompson sampling generally produces lower regret without a rate to tune.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I not use a multi-armed bandit?
&lt;/h3&gt;

&lt;p&gt;Avoid a bandit when you need a precise effect size for a durable or high-stakes decision, when you want a clean learning to generalize to future tests, or when your primary concern is valid early stopping - sequential testing is the correct tool for peeking, not a bandit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Optimizely support multi-armed bandits?
&lt;/h3&gt;

&lt;p&gt;Yes. Optimizely offers multi-armed bandit optimization that reallocates traffic to the highest-impact variation for a chosen primary metric, plus contextual bandits that personalize the variation per visitor using user attributes. Both optimize for impact and do not report statistical significance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;A multi-armed bandit reframes experimentation from "which variation is best?" to "how do I earn the most conversions while I find out?" It answers the explore-exploit tradeoff continuously, shifting traffic toward the leader to minimize regret rather than holding an even split to measure each option cleanly. Epsilon-greedy, Thompson sampling, and UCB are three strategies for making that explore-exploit decision, with Thompson sampling the modern default because its exploration self-adjusts as certainty grows. Reach for a bandit when the opportunity is short-lived or you are screening many low-stakes options and do not need a defensible effect size; stick with a fixed A/B test when you need a trustworthy, quantified result. Optimizely implements bandit optimization directly, exposing exploration and exploitation rates and offering both standard and contextual bandits, neither of which reports statistical significance - because for a bandit, conversions earned, not significance reached, is the goal.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>machinelearning</category>
      <category>statistics</category>
      <category>datascience</category>
    </item>
    <item>
      <title>How to Choose a Minimum Detectable Effect (MDE)</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:08:33 +0000</pubDate>
      <link>https://dev.to/david_sert/how-to-choose-a-minimum-detectable-effect-mde-2k31</link>
      <guid>https://dev.to/david_sert/how-to-choose-a-minimum-detectable-effect-mde-2k31</guid>
      <description>&lt;p&gt;You have opened your sample-size calculator, entered your baseline conversion rate, kept the defaults for significance and power, and now the calculator is asking for one more number: the minimum detectable effect. It is the input that decides everything downstream — how many visitors you need, how many weeks the test runs, and whether the experiment is even worth starting. Yet it is the one field with no obvious "correct" value, so it tends to get a number typed into it that is really a wish rather than a decision.&lt;/p&gt;

&lt;p&gt;This guide is about choosing that number deliberately. Not "what is MDE" in the abstract, but "what should I actually put in the box," why the answer is a business judgement as much as a statistical one, and how to avoid the two mistakes that quietly ruin experiment programs: an MDE so small the test never finishes, and one so large the test is underpowered to see the wins you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a minimum detectable effect actually is
&lt;/h2&gt;

&lt;p&gt;The minimum detectable effect is the smallest true improvement you want your experiment to be able to reliably catch. Optimizely defines it as "the smallest improvement you are willing to detect" and notes that it "determines how 'sensitive' an experiment is." If you set a 5% MDE, you are telling the calculator: size this test so that a genuine 5% lift would show up as statistically significant. A real effect exactly at your MDE has a good chance of being detected; a real effect smaller than your MDE probably will not, because you did not buy enough traffic to see it.&lt;/p&gt;

&lt;p&gt;The critical thing to internalize is that MDE is a &lt;em&gt;planning input&lt;/em&gt;, not a &lt;em&gt;result&lt;/em&gt;. It is not a prediction of how much your variation will lift conversions, and it is not the effect you will report at the end. It is a design lever you set beforehand to determine sample size. The test itself might measure a 3% lift or a 9% lift or nothing at all — the MDE only governed how sensitive the instrument was.&lt;/p&gt;

&lt;p&gt;Three inputs work together to fix your sample size: the baseline conversion rate, the statistical significance and power you require, and the MDE. Hold the first two constant and MDE becomes the dial that trades detectable precision against how much traffic — and therefore time — the test consumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relative vs. absolute MDE
&lt;/h2&gt;

&lt;p&gt;Almost every calculator, Optimizely's included, expresses MDE as a &lt;em&gt;relative&lt;/em&gt; lift, and this trips people up constantly. Optimizely states that MDE "represents the relative minimum improvement over the baseline." A 10% MDE does not mean "10 percentage points." It means a 10% change &lt;em&gt;relative to your baseline&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Work through Optimizely's own example. If your baseline conversion rate is 20% and you set a 10% MDE, the test is sized to detect a move outside the absolute band of 18% to 22% — because 10% of 20% is 2 percentage points. So a "10% relative MDE" is a "2 percentage-point absolute MDE" at that baseline.&lt;/p&gt;

&lt;p&gt;This distinction matters because absolute and relative framings diverge wildly at different baselines. A 10% relative MDE on a 2% baseline checkout rate is a 0.2 percentage-point move — invisible without enormous traffic. The same 10% relative MDE on a 40% baseline is a 4 percentage-point move, far easier to detect. Always confirm which convention your calculator uses before you type a number, and think in the units your business actually cares about. If stakeholders reason in percentage points ("we need to add two points of conversion"), convert to relative before entering it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sample size scales with the inverse square of MDE
&lt;/h2&gt;

&lt;p&gt;Here is the single most important fact for setting a realistic MDE: required sample size grows with roughly &lt;em&gt;one over the square of the MDE&lt;/em&gt;. Optimizely puts the direction plainly — "the smaller your MDE is, the larger the sample size required to reach statistical significance" — and the standard fixed-horizon formula makes the rate precise.&lt;/p&gt;

&lt;p&gt;Sample size per variation is approximately proportional to 1/MDE². The consequences are steep and non-linear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Halving your MDE (say 10% down to 5%) roughly &lt;strong&gt;quadruples&lt;/strong&gt; the sample size, because (1/0.5)² = 4.&lt;/li&gt;
&lt;li&gt;Cutting MDE to a third (10% to 3.3%) needs roughly &lt;strong&gt;nine times&lt;/strong&gt; the traffic.&lt;/li&gt;
&lt;li&gt;Going the other way, doubling the sample size only shrinks your detectable effect by about 29%, not half, because 1 − 1/√2 ≈ 0.293.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one that surprises people. Traffic buys detectability at a punishing exchange rate. You cannot casually "just run it a bit longer" to chase a smaller effect — the marginal week of traffic detects a smaller and smaller increment of lift. This asymmetry is exactly why MDE has to be chosen against what your traffic can afford, not against what you would like to prove.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The 1/MDE^2 penalty (relative MDE, baseline and settings held constant)

Anchor point (from Optimizely's own worked example):
  baseline = 15%,  MDE = 10% relative  -&amp;gt;  ~8,000 visitors PER VARIATION

Apply the sample-size ∝ 1 / MDE^2 scaling law to that anchor:

  MDE = 10%   -&amp;gt;  N          ~   8,000 / variation   (baseline)
  MDE =  5%   -&amp;gt;  N x (10/5)^2  = 4N  ~  32,000 / variation
  MDE =  2.5% -&amp;gt;  N x (10/2.5)^2 = 16N ~ 128,000 / variation

Reading it the other way:
  To HALVE the effect you can detect, you need ~4x the visitors.
  Doubling visitors only shrinks detectable MDE by ~29% (1 - 1/sqrt(2)).

Note: 8,000 is Optimizely's figure; the 32,000 and 128,000 are that
figure scaled by 1/MDE^2 — illustrative, not calculator-exact. Real
Stats Engine (sequential) durations differ, but the scaling holds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At two visitors per test, the difference between a 10% and a 2.5% MDE is the difference between a test that concludes in a couple of weeks and one that needs several months of the same traffic. Same hypothesis, same page — a 4x to 16x difference in cost, driven entirely by the number you chose for one field.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose an MDE that isn't wishful thinking
&lt;/h2&gt;

&lt;p&gt;A defensible MDE sits at the intersection of two independent constraints. Set it too optimistically on either and the experiment fails you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint one — the smallest lift worth shipping.&lt;/strong&gt; Ask what improvement would actually change a decision. If shipping the variation carries engineering cost, maintenance, or risk, a 0.5% lift might not be worth it even if it is real. The floor of your MDE should be the smallest effect that would still make you press "launch." Optimizely frames this as using "potential business impact to decide on the sensitivity of your experiment" — and notes that when conversions tie directly to revenue, a lower MDE can be justified precisely because each fraction of a percent is worth real money. A checkout step feeding millions in revenue earns a smaller MDE than a blog newsletter signup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint two — what your traffic can power in a reasonable window.&lt;/strong&gt; Take your weekly traffic to the surface under test, decide the longest you are willing to run (commonly two to four weeks so you capture full business cycles without letting the test rot), and work out the largest sample you can realistically collect. Then find the smallest MDE that sample can power. Optimizely's prioritization guidance is explicit: divide total required sample by the traffic you can allocate to see how long a test will take, and judge the tradeoff — "a 4% lift in 5 weeks may be a reasonable tradeoff of impact for effort, but a 2% lift measured over several months may not be."&lt;/p&gt;

&lt;p&gt;Your MDE is the &lt;em&gt;larger&lt;/em&gt; of these two floors. If the smallest lift worth shipping is 3% but your traffic can only power an 8% MDE in a month, you have a mismatch to resolve — you cannot honestly detect the 3% you care about. Do not pretend otherwise by typing 3% into a test that cannot see it.&lt;/p&gt;

&lt;p&gt;As rough orientation — not a rule — high-traffic pages often support a 2–5% relative MDE, while lower-traffic flows frequently bottom out around 10–20%. Treat these as sanity checks on your own calculation, not targets. And follow Optimizely's advice to work in "limits and ranges rather than looking for exact numbers": run the calculator at a few candidate MDEs and pick the point where detectable precision and runtime both look acceptable. The &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size calculator&lt;/a&gt; and &lt;a href="https://optipilot.com/tools/velocity-calculator" rel="noopener noreferrer"&gt;velocity calculator&lt;/a&gt; turn each candidate MDE into a concrete sample size and a testing-cadence estimate, which is the fastest way to see the tradeoff in weeks rather than abstractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision framework for picking your MDE
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Start: which MDE do I enter?] --&amp;gt; B[What is the smallest lift&amp;lt;br/&amp;gt;worth shipping? = business floor]
    A --&amp;gt; C[How much traffic can I collect&amp;lt;br/&amp;gt;in an acceptable window?]
    C --&amp;gt; D[Smallest MDE that traffic&amp;lt;br/&amp;gt;can power = traffic floor]
    B --&amp;gt; E{Is business floor&amp;lt;br/&amp;gt;&amp;gt;= traffic floor?}
    D --&amp;gt; E
    E --&amp;gt;|Yes| F[Set MDE = business floor.&amp;lt;br/&amp;gt;Test is worth running and powerable]
    E --&amp;gt;|No: want to detect smaller&amp;lt;br/&amp;gt;than traffic allows| G[Mismatch — pick one]
    G --&amp;gt; H[Reduce variance:&amp;lt;br/&amp;gt;CUPED, better metric,&amp;lt;br/&amp;gt;higher-traffic surface]
    G --&amp;gt; I[Extend runtime or&amp;lt;br/&amp;gt;concentrate traffic allocation]
    G --&amp;gt; J[De-prioritize: effect you care&amp;lt;br/&amp;gt;about is too small to power]
    H --&amp;gt; E
    I --&amp;gt; E
    F --&amp;gt; K[Enter MDE in calculator,&amp;lt;br/&amp;gt;commit sample size, launch]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you hit the mismatch branch, you have real options before you give up. Variance reduction is the highest-leverage one: techniques like &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;CUPED&lt;/a&gt; lower the noise in your metric, which effectively lets you detect a smaller effect from the same traffic — the equivalent of shrinking your MDE for free. You can also choose a higher-traffic surface, pick a metric closer to the change, or concentrate allocation. Only when none of those close the gap should you de-prioritize, which is itself a legitimate and common outcome — Optimizely explicitly advises that "if the traffic cost is too high, consider de-prioritizing the hypothesis… or seek to measure lift with less granularity."&lt;/p&gt;

&lt;p&gt;To see how much a given pre-period correlation shrinks the sample you need, try the &lt;a href="https://optipilot.com/tools/cuped-estimator" rel="noopener noreferrer"&gt;CUPED savings estimator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes: too low and too high
&lt;/h2&gt;

&lt;p&gt;Two symmetric errors account for most botched MDE decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MDE set too low (the test that never ends).&lt;/strong&gt; Chasing a tiny effect because "even 1% matters" without checking whether your traffic can power it. The 1/MDE² penalty means a 1% MDE can demand a hundred times the sample of a 10% MDE. The test drags on for months, business context shifts underneath it, and pressure builds to peek and stop early — which inflates false positives if you are not on a method built for it. If you genuinely need to detect small effects, plan for it with variance reduction or a sequential-testing approach, don't just lower the number and hope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MDE set too high (the underpowered test).&lt;/strong&gt; Entering a large MDE to make the sample size look affordable, then treating a non-significant result as proof the variation "didn't work." It proves no such thing. A test sized for a 15% MDE is simply blind to a real 6% lift — a genuinely valuable win — because it was never powered to see it. You will ship losers and kill winners on the strength of a test that was underpowered by construction. High MDEs are only honest when the smallest lift worth shipping really is that large.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peeking as an MDE problem in disguise.&lt;/strong&gt; When a test sized for a modest MDE runs longer than hoped, the temptation is to check daily and stop the moment significance flickers. On a fixed-horizon test this quietly wrecks your error rate. The disciplined fixes are to size honestly up front and, if you need the flexibility to stop early, adopt a method designed for it — see &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing and the peeking problem&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  MDE is a planning input, not a result
&lt;/h2&gt;

&lt;p&gt;The recurring theme is worth stating once more, cleanly: the MDE you enter shapes the experiment; it is not something the experiment gives back. Optimizely's own guidance is to "use MDE as a guide rather than an exact prediction." It sets the sensitivity of your instrument before you collect a single visitor. Confuse it with the expected or measured lift and you will either over-promise ("we'll get a 10% lift" — no, you sized to &lt;em&gt;detect&lt;/em&gt; one) or misread a null result.&lt;/p&gt;

&lt;p&gt;So treat the MDE field as a design decision with two owners: the business, who names the smallest lift worth shipping, and the traffic, which caps how small an effect you can afford to detect in a sensible window. Set your MDE at the larger of those two floors, verify the resulting sample size and runtime in a calculator, and only then launch. Done that way, the number in the box stops being a wish and becomes the honest specification of what your experiment can — and cannot — tell you.&lt;/p&gt;

&lt;p&gt;Once your MDE is set, size the test with the &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;sample size calculator&lt;/a&gt;, pace your roadmap with the &lt;a href="https://optipilot.com/tools/velocity-calculator" rel="noopener noreferrer"&gt;velocity calculator&lt;/a&gt;, and if the effect you care about is too small to power, reach for &lt;a href="https://optipilot.com/data/cuped-variance-reduction" rel="noopener noreferrer"&gt;CUPED variance reduction&lt;/a&gt; or &lt;a href="https://optipilot.com/data/sequential-testing-peeking" rel="noopener noreferrer"&gt;sequential testing&lt;/a&gt; before you compromise on the MDE itself.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>analytics</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Reach Significance Faster: CUPED Variance Reduction</title>
      <dc:creator>David Sert</dc:creator>
      <pubDate>Tue, 18 Aug 2026 05:08:24 +0000</pubDate>
      <link>https://dev.to/david_sert/reach-significance-faster-cuped-variance-reduction-eb4</link>
      <guid>https://dev.to/david_sert/reach-significance-faster-cuped-variance-reduction-eb4</guid>
      <description>&lt;p&gt;Experiments stall for one boring reason more than any other: not enough signal relative to noise. When a metric like revenue-per-visitor swings wildly from user to user, the treatment effect you care about is buried under variance that has nothing to do with your change. CUPED (Controlled-experiment Using Pre-Experiment Data) attacks that variance directly — it uses each user's own pre-experiment behavior to subtract out the predictable part of the noise, so the same experiment reaches significance with less traffic and less time. This article explains the intuition, the math, how Optimizely's regression-based implementation differs from the textbook version, and — just as important — the cases where CUPED does nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Variance, Not Effect Size, Is Usually the Bottleneck
&lt;/h2&gt;

&lt;p&gt;The number of visitors an experiment needs to detect an effect scales with the &lt;em&gt;variance&lt;/em&gt; of the metric divided by the square of the effect you want to detect. You cannot manufacture a bigger effect — that is set by the quality of your idea. But you can often shrink the variance, and every reduction in variance translates directly into a smaller required sample size.&lt;/p&gt;

&lt;p&gt;This matters most for exactly the teams that struggle: low-traffic sites, high-value pages that convert rarely, and revenue or engagement metrics with long, heavy tails where a handful of whales dominate the average. For those metrics, the classic advice — "run it longer" — is expensive and sometimes impossible. Variance reduction is the alternative: change the &lt;em&gt;statistics&lt;/em&gt;, not the traffic.&lt;/p&gt;

&lt;p&gt;CUPED is the most widely adopted variance-reduction technique in the industry, introduced by Deng, Xu, Kohavi, and Walker at Microsoft in 2013 and now standard at companies including Netflix, Booking.com, and Optimizely. If you want the broader context on how experiments reach significance in the first place, see OptiPilot's explainer on &lt;a href="https://optipilot.com/data/optimizely-stats-engine" rel="noopener noreferrer"&gt;how the Optimizely Stats Engine works&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Intuition: Noise You Can Predict, You Can Remove
&lt;/h2&gt;

&lt;p&gt;Here is the core idea in one sentence: &lt;strong&gt;if you can predict part of a user's post-experiment metric before the experiment even starts, that predictable part is not evidence about your treatment — so remove it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider revenue-per-user on an e-commerce site. A customer who spent 500 USD in the two weeks before your test is likely to spend more than average during the test, regardless of which variation they land in. That tendency is a property of the &lt;em&gt;user&lt;/em&gt;, not your &lt;em&gt;change&lt;/em&gt;. When a big spender happens to land in the treatment group, they inflate the treatment average — pure luck of the draw, and exactly the kind of noise that makes results wobble.&lt;/p&gt;

&lt;p&gt;Because assignment to control or treatment is random, a user's pre-experiment behavior is (in expectation) &lt;em&gt;balanced&lt;/em&gt; across variations and &lt;em&gt;uncorrelated with the treatment assignment&lt;/em&gt;. That is what makes it a safe covariate: it is strongly correlated with the outcome, but independent of the thing you are trying to measure. CUPED subtracts the predictable, user-level component of the metric and analyzes what remains. The treatment effect survives untouched; the random noise shrinks.&lt;/p&gt;

&lt;p&gt;The strength of the effect hinges entirely on one quantity: &lt;strong&gt;the correlation between a user's pre-experiment metric and their in-experiment metric.&lt;/strong&gt; High correlation (a user's past revenue strongly predicts their future revenue) means large variance reduction. Zero correlation means CUPED does nothing — it cannot hurt you, but it cannot help either.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    A[Pre-experiment window&amp;lt;br/&amp;gt;e.g. 2 weeks of history] --&amp;gt; B[Per-user covariate X&amp;lt;br/&amp;gt;past value of the metric]
    C[In-experiment metric Y] --&amp;gt; D[Regress Y on X&amp;lt;br/&amp;gt;remove predictable part]
    B --&amp;gt; D
    D --&amp;gt; E[Residuals:&amp;lt;br/&amp;gt;variance reduced]
    E --&amp;gt; F[Stats Engine]
    F --&amp;gt; G[Significance reached&amp;lt;br/&amp;gt;with less traffic / time]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Math
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The classic CUPED estimator (2013)
&lt;/h3&gt;

&lt;p&gt;The original method adjusts the metric mean using a single covariate &lt;code&gt;X&lt;/code&gt; (typically the same metric measured in the pre-period). The CUPED-adjusted estimate of the mean is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ŷ_cuped = Ȳ − θ (X̄ − E[X])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;θ&lt;/code&gt; is chosen to minimize the variance of the adjusted estimator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;θ = Cov(Y, X) / Var(X)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;E[X]&lt;/code&gt; is the expected value of the covariate across the whole population (the same for both groups, since assignment is random). Subtracting &lt;code&gt;θ(X̄ − E[X])&lt;/code&gt; removes the component of each group's mean that is explained by the covariate. The variance of the adjusted metric is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Var(Ŷ_cuped) = Var(Ȳ) · (1 − ρ²)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;ρ&lt;/code&gt; is the correlation between &lt;code&gt;Y&lt;/code&gt; and &lt;code&gt;X&lt;/code&gt;. This single equation is the whole story: a covariate correlated 0.7 with the outcome removes &lt;code&gt;0.7² ≈ 49%&lt;/code&gt; of the variance; a covariate correlated 0.3 removes only about 9%. &lt;strong&gt;Your variance reduction is the square of the correlation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Fcuped-variance-reduction.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdb.devchild.me%2Fstorage%2Fv1%2Fobject%2Fpublic%2Foptipilot-docs-images%2Fstat-charts%2Fcuped-variance-reduction.svg" alt="Curve of the reduction in required sample size against the correlation between the pre-experiment covariate and the in-experiment metric, following variance removed = rho squared. The saving is negligible below a correlation of 0.3, reaches about 49% at 0.7, and about 81% at 0.9." width="720" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Illustrative Python: computing the classic CUPED adjustment
&lt;/h3&gt;

&lt;p&gt;The snippet below computes the classic mean-adjustment CUPED estimate from pre-period and in-experiment arrays. It is intended to build intuition — it is &lt;em&gt;not&lt;/em&gt; how Optimizely computes CUPED internally (see the next section), and it uses the pooled covariate to estimate &lt;code&gt;θ&lt;/code&gt;, which is the standard practice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cuped_adjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Classic CUPED (Deng et al. 2013) mean adjustment.

    pre  : per-user metric value in the pre-experiment window (covariate X)
    post : per-user metric value during the experiment (outcome Y)
    Returns the CUPED-adjusted post values, aligned per user.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# theta = Cov(Y, X) / Var(X), estimated on the pooled sample
&lt;/span&gt;    &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cov&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Center the covariate on its population mean, then subtract its
&lt;/span&gt;    &lt;span class="c1"&gt;# predictable contribution from each user's outcome.
&lt;/span&gt;    &lt;span class="n"&gt;adjusted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pre&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;adjusted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt;

&lt;span class="c1"&gt;# --- demo: correlated pre/post, random assignment ---
&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20_000&lt;/span&gt;
&lt;span class="n"&gt;user_baseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gamma&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# heavy-tailed "spend"
&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_baseline&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# in-experiment metric
&lt;/span&gt;&lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;                              &lt;span class="c1"&gt;# true +1.0 effect
&lt;/span&gt;
&lt;span class="n"&gt;adjusted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cuped_adjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_baseline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;raw_effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;adj_effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adjusted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;adjusted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;treatment&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;theta = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw   variance of post:     &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuped variance of adjusted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;adjusted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw effect:  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw_effect&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuped effect:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;adj_effect&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# unbiased: still ~+1.0, lower variance
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adjusted metric has substantially lower variance while the estimated treatment effect stays centered on the truth. That is the entire value proposition: same answer, tighter confidence interval, fewer users required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizely's Regression-Based CUPED
&lt;/h2&gt;

&lt;p&gt;Optimizely does not use the 2013 mean-adjustment formula directly. Its implementation is a &lt;strong&gt;regression-based covariance adjustment&lt;/strong&gt;, which the documentation describes as "more advanced than the standard CUPED method." Instead of adjusting the mean with a single &lt;code&gt;θ&lt;/code&gt;, Optimizely fits a linear regression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Yᵢ = α + β·Tᵢ + γᵀ·X̃ᵢ + eᵢ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Yᵢ&lt;/code&gt; — the numeric target metric for user &lt;em&gt;i&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Tᵢ&lt;/code&gt; — treatment indicator (0 = control, 1 = treatment)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;X̃ᵢ&lt;/code&gt; — centered covariates derived from that user's pre-experiment data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;α&lt;/code&gt;, &lt;code&gt;β&lt;/code&gt;, &lt;code&gt;γ&lt;/code&gt; — regression coefficients&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eᵢ&lt;/code&gt; — error term&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The coefficient &lt;code&gt;β&lt;/code&gt; is the treatment effect, and its Ordinary Least Squares estimate &lt;code&gt;β̂&lt;/code&gt; is an &lt;strong&gt;unbiased&lt;/strong&gt; estimator of the true effect. Including the pre-experiment covariates &lt;code&gt;X̃ᵢ&lt;/code&gt; reduces the variance of &lt;code&gt;β̂&lt;/code&gt; — that is the variance reduction, expressed in regression form.&lt;/p&gt;

&lt;p&gt;In practice Optimizely runs this in two steps, following the &lt;strong&gt;Frisch–Waugh–Lovell theorem&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Regress &lt;code&gt;Yᵢ&lt;/code&gt; on the covariates &lt;code&gt;X̃ᵢ&lt;/code&gt; and compute each user's predicted value.&lt;/li&gt;
&lt;li&gt;Compute the residuals &lt;code&gt;Yᵢ − Ŷᵢ&lt;/code&gt; and feed those residuals into the Stats Engine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Stats Engine then evaluates significance on the residualized, lower-variance metric.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why regression rather than the classic formula?
&lt;/h3&gt;

&lt;p&gt;The regression framing is strictly more general. As Optimizely's own documentation notes, if the covariate vector &lt;code&gt;X̃ᵢ&lt;/code&gt; contained nothing but the single historical value of &lt;code&gt;Yᵢ&lt;/code&gt;, the regression method would collapse exactly to the classic CUPED estimator. Because it can incorporate multiple covariates and handles them in a unified least-squares framework, it dominates the standard method — hence Optimizely's choice. In the current release, the covariates are limited to the pre-experiment calculations of your primary and secondary target metrics; user-defined covariates are not yet supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  When CUPED Applies — and When It Does Not
&lt;/h2&gt;

&lt;p&gt;CUPED is not a universal switch. Verified against Optimizely's documentation, here is where it helps and where it is silent or unavailable.&lt;/p&gt;

&lt;h3&gt;
  
  
  CUPED helps when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The metric is &lt;strong&gt;numeric&lt;/strong&gt; (revenue per visitor, pages per session, time on site, order value).&lt;/li&gt;
&lt;li&gt;Users have &lt;strong&gt;pre-experiment history&lt;/strong&gt; for that metric.&lt;/li&gt;
&lt;li&gt;That history is &lt;strong&gt;correlated&lt;/strong&gt; with in-experiment behavior — the stronger the correlation, the larger the reduction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CUPED does nothing (or is unavailable) when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The metric is a conversion / binary metric.&lt;/strong&gt; Optimizely restricts CUPED to numeric metrics; it is most effective there and is not offered for conversion-rate metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users have no prior data.&lt;/strong&gt; The pre-experiment window runs from (by default) two weeks before the experiment start date up to each user's first decision event. A user with no activity in that window — a brand-new user, or a newly created metric with no history — contributes no covariate, so CUPED has no effect for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pre-period metric is uncorrelated with the outcome.&lt;/strong&gt; Zero correlation means zero variance reduction. It will not bias your result, but it will not speed it up.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A[Want faster significance?] --&amp;gt; B{Is the metric numeric?}
    B -- No, it is a conversion metric --&amp;gt; X[CUPED not available&amp;lt;br/&amp;gt;consider outlier management instead]
    B -- Yes --&amp;gt; C{Do users have&amp;lt;br/&amp;gt;pre-experiment history?}
    C -- No, new users / new metric --&amp;gt; Y[CUPED has no effect]
    C -- Yes --&amp;gt; D{Is history correlated&amp;lt;br/&amp;gt;with in-experiment metric?}
    D -- Weakly / not at all --&amp;gt; Z[Little to no benefit&amp;lt;br/&amp;gt;but safe to leave on]
    D -- Strongly --&amp;gt; W[Enable CUPED&amp;lt;br/&amp;gt;meaningful variance reduction]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful mental model: CUPED rewards metrics where the past predicts the present. Returning-user revenue and engagement metrics are ideal. First-touch acquisition metrics, or anything measured on users with no history, are exactly the cases where it quietly does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up CUPED in Optimizely
&lt;/h2&gt;

&lt;p&gt;CUPED lives on Optimizely's &lt;strong&gt;new A/B Results page&lt;/strong&gt; and its warehouse-native &lt;strong&gt;Analytics Scorecards&lt;/strong&gt;. A few prerequisites and steps, verified against the docs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access via &lt;strong&gt;Opti ID&lt;/strong&gt;, on the &lt;strong&gt;new A/B Results page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual A/B experiments only.&lt;/strong&gt; Stats Accelerator experiments and multivariate tests are not supported.&lt;/li&gt;
&lt;li&gt;A supported data warehouse: &lt;strong&gt;Snowflake, BigQuery, or Databricks&lt;/strong&gt; (CUPED is part of Warehouse-Native Experimentation Analytics). Contact Optimizely for other warehouses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Numeric&lt;/strong&gt; metrics — CUPED options appear only for numeric metrics, alongside outlier management.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an Experiment Scorecard in Analytics (or open the new A/B Results page for your experiment).&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Advanced options&lt;/strong&gt;, enable the &lt;strong&gt;CUPED&lt;/strong&gt; toggle.&lt;/li&gt;
&lt;li&gt;Optionally set the &lt;strong&gt;CUPED duration&lt;/strong&gt; — the length of the pre-experiment window. The default is two weeks of history; adjust it if a different window better reflects your users' behavior.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Run&lt;/strong&gt; (or Ctrl/Cmd+Enter) to recalculate results with CUPED applied. You can toggle it on and off to compare scorecards side by side.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because toggling CUPED re-runs the analysis over your warehouse data, treat it as a lens on the &lt;em&gt;same&lt;/em&gt; experiment rather than a change to the experiment itself — the underlying decision and conversion events are untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Turning it on and expecting magic on conversion metrics.&lt;/strong&gt; CUPED is numeric-only. If your primary metric is a conversion rate, CUPED will not appear or apply. For heavy-tailed numeric metrics, pair CUPED with Optimizely's outlier management (Winsorization or capping) — they address different problems (systematic vs. extreme-value variance) and compose well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expecting help where there is no history.&lt;/strong&gt; For a metric that only starts collecting data at experiment launch, or an audience of brand-new users, the covariate is empty and CUPED has no effect. This is not a bug; it is the method's precondition. Plan a longer pre-period, or accept that variance reduction is not available for that metric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peeking at "with vs. without CUPED" and cherry-picking.&lt;/strong&gt; CUPED is a legitimate variance-reduction method configured &lt;em&gt;before&lt;/em&gt; you look at results, not a knob to flip after the fact until significance appears. Decide up front that you will report the CUPED-adjusted scorecard, and stick with it. Toggling repeatedly and keeping whichever view crosses the threshold reintroduces exactly the false-positive risk that a disciplined stats process exists to prevent. Continuous monitoring is valid under Optimizely's Sequential Stats Engine in a way it is not under a fixed-horizon test — but that is a property of the statistical method, not a license to cherry-pick which CUPED view you report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assuming a covariate-imbalance safety net exists.&lt;/strong&gt; Optimizely notes there are no automated health checks yet for covariate imbalance, the way there are for Sample Ratio Mismatch. With sparse prior data, an unbalanced covariate can in principle &lt;em&gt;increase&lt;/em&gt; variance. In practice this is uncommon, but do not assume the platform will flag it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much Time or Traffic Does It Actually Save?
&lt;/h2&gt;

&lt;p&gt;The honest answer: &lt;strong&gt;it depends entirely on the correlation between your pre-period and in-experiment metrics, and Optimizely does not publish a guaranteed number.&lt;/strong&gt; The mechanism, however, is precise. Because variance reduction equals the square of that correlation, and required sample size scales linearly with variance, the &lt;em&gt;fractional reduction in required traffic is approximately the fractional reduction in variance.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The original 2013 CUPED research and subsequent industry practice commonly report variance reductions in the &lt;strong&gt;30–50% range on well-correlated engagement and revenue metrics&lt;/strong&gt; — which, taken at face value, corresponds to needing roughly 30–50% fewer users (or days) to reach the same significance. Treat that as an &lt;em&gt;illustrative&lt;/em&gt; band from the literature, not an Optimizely commitment: on a metric with weak pre-period correlation you might see a few percent, and on a conversion metric you will see nothing because CUPED does not apply.&lt;/p&gt;

&lt;p&gt;The practical takeaway for planning: if your metric is numeric and your users have predictive history, CUPED can meaningfully shorten test duration for free — no extra traffic, no change to your experiment design. To translate an expected variance reduction into concrete sample-size and runtime numbers for your own baseline and &lt;a href="https://optipilot.com/data/minimum-detectable-effect" rel="noopener noreferrer"&gt;MDE&lt;/a&gt;, use &lt;a href="https://optipilot.com/tools/sample-size-calculator" rel="noopener noreferrer"&gt;OptiPilot's traffic and sample-size estimator&lt;/a&gt; and compare the required duration with and without the reduction.&lt;/p&gt;

&lt;p&gt;To translate a specific pre-period correlation into an estimated sample-size and duration saving, use the &lt;a href="https://optipilot.com/tools/cuped-estimator" rel="noopener noreferrer"&gt;CUPED savings estimator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;CUPED reduces the variance of numeric experiment metrics by using each user's pre-experiment behavior as a covariate, stripping out predictable noise so tests reach significance faster without more traffic. Optimizely implements it as a regression-based covariance adjustment — a generalization of the classic 2013 estimator — that residualizes the metric and feeds the result to its Stats Engine. It shines on numeric, high-history, well-correlated metrics; it is unavailable for conversion metrics and inert for users with no prior data. Turn it on for the right metrics, decide to use it before you look at results, and it is one of the cheapest ways to make an under-powered experiment program faster.&lt;/p&gt;

</description>
      <category>abtesting</category>
      <category>statistics</category>
      <category>datascience</category>
      <category>analytics</category>
    </item>
  </channel>
</rss>
