The Control-Limit Mistake Almost Everyone Makes with EWMA/CUSUM

In short: Drift detection is weakest right after a restart, a lot change or a setpoint change, which is often exactly when the process is doing something worth catching. Fixed control limits make that blind spot worse.

EWMA and CUSUM are two of the most widely used tools for catching slow drift and small sustained shifts that a standard Shewhart control chart will miss. They’re also two of the most commonly mis-implemented tools, because most reference material shows you the easy version of the control limits: the version that’s only correct after the process has been running for a while.

A quick primer

  • EWMA (Exponentially Weighted Moving Average) smooths a signal by weighting recent observations more heavily than older ones, controlled by a smoothing parameter λ. It’s good at catching gradual drift that individual points wouldn’t flag.
  • CUSUM (Cumulative Sum) accumulates deviations from a target value over time, and flags when that cumulative sum crosses a threshold. It’s especially sensitive to small, sustained shifts.

Both need control limits: a band outside of which a value is considered a signal, not noise.

The mistake: treating the limits as static

Most tutorials give you a fixed control limit for EWMA, something like:

UCL = μ + L·σ·sqrt(λ / (2 − λ))
LCL = μ − L·σ·sqrt(λ / (2 − λ))

This formula is correct, but only as t → ∞. It describes the steady-state variance of the EWMA statistic. Early in a run, right after initialization, the actual variance of the EWMA statistic is smaller than this steady-state value, because there hasn’t been enough time for the exponential weighting to “spread out.”

The full, time-dependent formula is:

Var(Z_t) = σ² · (λ / (2 − λ)) · (1 − (1 − λ)^(2t))

At t = 1, this is much smaller than the steady-state value. As t grows, the (1 − (1−λ)^(2t)) term approaches 1, and you recover the familiar steady-state formula.

Why this matters in practice

If you use the steady-state (fixed) control limits from the very first sample:

  • Early in a run, your limits are too wide. You’re using a limit sized for the steady-state variance when the actual variance is smaller, so real early shifts can slip through undetected.
  • After a recalculation event, if EWMA/CUSUM state isn’t properly reset, you get a blend of old and new process behavior contaminating the statistic, on top of the same static-limit problem.

In a live monitoring system, recalculation events happen constantly: a new lot starts, a setpoint changes, an operator manually resets targets. Every one of those is effectively a new t = 0. If the system doesn’t reset EWMA/CUSUM state and recompute limits with the time-dependent formula, the fresh data gets judged against a fixed strip of steady-state math no matter how much or how little data actually supports it.

The fix

Two changes, both required:

  1. Implement time-varying control limits using the full Var(Z_t) formula, recomputing the limit at each time step until it converges to steady state.
  2. Explicitly reset EWMA/CUSUM accumulator state on every recalculation trigger, so post-reset data doesn’t get smoothed together with pre-reset data.
def ewma_variance(sigma, lam, t):
    return (sigma ** 2) * (lam / (2 - lam)) * (1 - (1 - lam) ** (2 * t))

Every control limit calculation should call this instead of assuming steady state, at least for the first several dozen samples after any reset. After that, it converges close enough to the fixed limit that it stops mattering.

Making it visible, not just correct

A time-series chart with the EWMA (or CUSUM) statistic plotted against its control limit bands, not a single static line but the actual time-varying boundary drawn as it widens and narrows, is what makes this legible to anyone reading the chart. Seeing the band itself change over time is what makes the “why did that early point not alarm, but this one did” question answerable at a glance, instead of requiring someone to go check the math.

The broader lesson

Statistical formulas often have a steady-state version that’s easier to remember and a transient version that’s actually correct near a boundary condition: initialization, reset, a regime change. Anomaly detection systems live and die on exactly those transient windows, since that’s often when the process itself is doing something worth watching. Defaulting to the steady-state formula because it’s what’s in the textbook is a quiet way to blind the system during the moments it matters most.

This is Part 2 of a 7-part series on building a real-time process monitoring system. Part 3 covers the architecture that let this engine, SPC, and a later influence-scoring engine share one codebase.