← Applied algorithms
Chance and live data From a stream of readings to its spread

Welford’s online variance

Three numbers. No readings kept.

Five barometer readings of 101,325.01 pascals have no spread at all. Keep a running total and a running total of squares, use the textbook formula at the end, and the variance comes out as −0.0000019 Pa²: negative, which no set of readings can be. John D. Cook, writing about exactly this: “The loss of precision can be so bad that the expression above evaluates to a negative number even though variance is always positive.”

B. P. Welford’s 1962 update never subtracts one big total from another. River, a Python library for data that arrives as a stream, describes its stats.Var as “Running variance using Welford's algorithm.” We’ll use it on a rooftop weather station: a pressure reading every second, a mean and spread for every hour, and nothing kept but three numbers.

TypeScriptGoOne barometer in each language

01 / The idea

Keep three numbers, not an hour of readings.

The station reads the air pressure once a second and reports, every hour, the mean and how much the readings spread around it. An hour is 3,600 readings, and the board on the roof doesn’t need any of them once they have been counted in.

Welford’s method keeps a count, a mean, and the squares: the sum of every reading’s squared distance from the mean. Each reading moves the mean by its distance over the count, and adds its old distance times its new distance to the squares. The variance is the squares over the count minus one, and the standard deviation is its square root, back in pascals.

Watch five readings go in. The mean moves, and the squares grow by one product each time.

Welford

Three numbers. No readings kept.

ROOFTOP BAROMETER · PASCALS 1 reading of 5
101,316101,320101,324101,328101,332mean 101,322

count 1 · mean 101,322 Pa · squares 0

Reading 1, 101,322 Pa, becomes the mean. It is 0 from the new mean, so the squares stay 0.

distance from the old mean distance from the new mean

01/ 03
Move the mean

Each reading moves the mean.

The first reading, 101,322 Pa, is the mean. The second, 101,326 Pa, is 4 above it, so the mean moves 4 ÷ 2 to 101,324 Pa, and the squares gain 4 × 2 = 8.

Reduced motion: choose a scene to see its completed state.

Read this scene

The first reading, 101,322 Pa, is the mean. The second, 101,326 Pa, is 4 above it, so the mean moves 4 ÷ 2 to 101,324 Pa, and the squares gain 4 × 2 = 8.

Readings so far: 101,322 Pa. Count 1, mean 101,322 Pa, squares 0.

Watch and Step through replay the updates over five readings. Try it runs the same TypeScript on the examples and on readings you add, one reading at a time, beside the textbook formula, and starts fresh each time you open it.

The fifth reading, 101,318 Pa, is 7.5 below the old mean and 6 below the new one, so it adds 45. Neither 7.5² nor 6² would be right: one reading moved the mean, and with it every other reading’s distance.

02 / Name the rule

Old distance times new distance.

A summary starts at count 0, mean 0, squares 0. Each reading does three things:

Count

One more

count = count + 1

Mean

Move by distance over count

delta = reading − mean, then mean = mean + delta / count

Squares

Add old distance × new distance

squares = squares + delta × (reading − mean)

Two ways to divide. The squares over count − 1 is the sample variance: your best estimate of how much the air varies, from readings that are a sample of it. The squares over count is the population variance: the spread of exactly these readings. With one reading, the population variance is 0 and the sample variance is undefined, so the code returns nothing rather than a number. The lesson’s code uses the sample variance unless you ask for the other.

Why not keep totals? The textbook formula is the total of squares minus the total squared over the count. For pressure in pascals both are about ten billion times the count, and the variance is a few ten-thousandths. Doubles hold about 16 significant digits, so the subtraction throws most of them away. In the still room that gives −0.0000019 Pa². For an hour of readings that differ only in the hundredths, the textbook variance comes out 49% higher than the exact one, while Welford’s is within a billionth of it. The tests compute the exact value from whole hundredths and check both languages.

Why does old distance times new distance work?Three lines of algebra

Moving the mean by delta / count changes every earlier reading’s squared distance. Their distances from the old mean add up to zero, so the cross terms cancel and together they gain (count − 1) × (delta / count)².

The new reading is delta × (count − 1) / count from the new mean, so it adds that squared. Add the two and factor: delta² × (count − 1) / count, which is delta times (reading − new mean). No term needs a total, so no large numbers meet.

Welford’s note, “Note on a Method for Calculating Corrected Sums of Squares and Products,” appeared in Technometrics in 1962. Cook adds that the method “is presented in Donald Knuth's Art of Computer Programming, Vol 2, page 232, 3rd edition.”

03 / Read the shape

A summary, an update, and two ways to read it.

Basic form is the whole method: add returns a new summary, and variance and standardDeviation read one. In the wild checks summaries that arrive from stations and merges them. At the call site runs a minute, the still room, and an hour, and merges the hour from 60 minute summaries. Both languages print the same four lines.

The whole method: a summary is a count, a mean, and the sum of squared distances from the mean. add moves the mean by the distance over the count and adds the old distance times the new one; variance and standardDeviation read it.

TypeScriptReading
summary.ts
// Welford's method: the mean and spread of a stream of readings, updated one reading at a time.
// The readings are not kept. Three numbers are the whole state.
export const MAX_ABS_READING = 1e12;
export const MAX_COUNT = 1e15;

export type Summary = {
	count: number; // readings so far
	mean: number;
	squares: number; // the sum of every reading's squared distance from the mean
};

export type SummaryErrorCode = 'bad-reading' | 'too-many' | 'bad-summary';

export class SummaryError extends Error {
	readonly code: SummaryErrorCode;
	constructor(code: SummaryErrorCode, message: string) {
		super(message);
		this.name = 'SummaryError';
		this.code = code;
	}
}

export const empty: Summary = Object.freeze({ count: 0, mean: 0, squares: 0 });

export function add(summary: Summary, reading: number): Summary {
	if (!Number.isFinite(reading) || Math.abs(reading) > MAX_ABS_READING)
		throw new SummaryError('bad-reading', `readings are finite numbers within ±${MAX_ABS_READING}`);
	if (summary.count >= MAX_COUNT)
		throw new SummaryError('too-many', `a summary holds up to ${MAX_COUNT} readings`);
	const count = summary.count + 1;
	const delta = reading - summary.mean; // distance from the old mean
	const mean = summary.mean + delta / count;
	// The old distance times the new one. Nothing here subtracts one big total from another.
	return { count, mean, squares: summary.squares + delta * (reading - mean) };
}

// Sample variance divides by count − 1 and needs two readings. Population variance divides by
// count: the spread of exactly these readings. Null when there aren't enough readings.
export function variance(
	summary: Summary,
	kind: 'sample' | 'population' = 'sample'
): number | null {
	const divisor = kind === 'sample' ? summary.count - 1 : summary.count;
	return divisor > 0 ? summary.squares / divisor : null;
}

// The sample standard deviation, in the readings' own unit.
export function standardDeviation(summary: Summary): number | null {
	const sample = variance(summary);
	return sample === null ? null : Math.sqrt(sample);
}
GoAlongside
summary.go
// Welford's method: the mean and spread of a stream of readings, updated one reading at a time.
// The readings are not kept. Three numbers are the whole state.
const (
	MaxAbsReading = 1e12
	MaxCount      = 1_000_000_000_000_000
)

type Summary struct {
	Count   int // readings so far
	Mean    float64
	Squares float64 // the sum of every reading's squared distance from the mean
}

type SummaryError struct{ Code, Message string }

func (e *SummaryError) Error() string { return e.Message }

// Add returns the summary with one more reading.
func (s Summary) Add(reading float64) (Summary, error) {
	if math.IsNaN(reading) || math.Abs(reading) > MaxAbsReading {
		return s, &SummaryError{"bad-reading", fmt.Sprintf("readings are finite numbers within ±%g", MaxAbsReading)}
	}
	if s.Count >= MaxCount {
		return s, &SummaryError{"too-many", fmt.Sprintf("a summary holds up to %d readings", MaxCount)}
	}
	count := s.Count + 1
	delta := reading - s.Mean // distance from the old mean
	mean := s.Mean + delta/float64(count)
	// The old distance times the new one. float64(...) stops Go fusing the multiply into the add,
	// so every result matches the TypeScript bit for bit.
	return Summary{count, mean, s.Squares + float64(delta*(reading-mean))}, nil
}

// Variance is the sample variance: squares over count − 1. It needs two readings.
func (s Summary) Variance() (float64, bool) {
	if s.Count < 2 {
		return 0, false
	}
	return s.Squares / float64(s.Count-1), true
}

// PopulationVariance divides by count: the spread of exactly these readings.
func (s Summary) PopulationVariance() (float64, bool) {
	if s.Count < 1 {
		return 0, false
	}
	return s.Squares / float64(s.Count), true
}

// StandardDeviation is the sample standard deviation, in the readings' own unit.
func (s Summary) StandardDeviation() (float64, bool) {
	v, ok := s.Variance()
	return math.Sqrt(v), ok
}
Reading the TypeScriptA frozen empty summary, null for undefined

add returns a new object instead of changing the one it was given, so a caller can keep the summary in a variable, a store, or a message. empty is frozen so nobody changes the starting point by accident.

variance returns null when there aren’t enough readings rather than NaN, so a readout can say so. Errors are a SummaryError whose code matches the Go version.

Reading the Go(float64, bool), and float64(...) around products

Add is a method on a Summary value and returns a new one. Variance returns a value and an ok flag instead of null.

The products are wrapped in float64(...), which looks like it does nothing. The Go specification allows a compiler to fuse a multiply and an add into one instruction: “An implementation may combine multiple floating-point operations into a single fused operation, possibly across statements, and produce a result that differs from the value obtained by executing and rounding the instructions individually. An explicit floating-point type conversion rounds to the precision of the target type, preventing fusion that would discard that rounding.” With the conversions, every shared case matches the TypeScript bit for bit.

What is refusedReadings, counts, and summaries

A reading that isn’t a finite number, or is beyond ±10¹² in either direction, is bad-reading. A summary holds up to 10¹⁵ readings, which keeps the count an exact whole number in TypeScript; one more is too-many.

A summary from outside is bad-summary unless its count is a whole number in range, its mean is finite and in range, its squares are finite and not negative, an empty summary has mean 0, and a single reading has squares 0. Both languages run the same cases, apart from two that Go’s decoder refuses before they reach the check.

04 / Try a decision

Predict the squares.

One more reading arrives. Work out what it adds before the feedback tells you.

After four readings the summary is count 4, mean 101,325.5 Pa, squares 35. The fifth reading is 101,318 Pa. What are the squares after it?

05 / Follow the cost

Constant work, constant memory.

Welford: time and extra space per operation, with n readings
OperationTimeExtra spaceWhat it assumes
Add a readingO(1)O(1)Two subtractions, a division, a multiplication, and three additions. The reading is not kept.
Read the variance or deviationO(1)O(1)Squares over count − 1, and a square root for the deviation.
Merge two summariesO(1)O(1)Three numbers from each part, three numbers out.
Running totals, textbook formulaO(1)O(1)The same cost as Welford. The difference is what rounding does to the answer.
Keep every reading, recomputeO(n) per readoutO(n)What a running summary avoids. An hour at one reading a second is 3,600 numbers.
A median or 95th percentileO(n)O(n)Three numbers can’t give it. Keep the readings, or an approximate histogram.

The summary is the same size after one reading or a year of them: a count and two doubles. That is why it fits on a small board, in a metrics message, or in a long-lived page.

Running totals cost the same order of work: Welford adds a division and two subtractions per reading, which is all it pays for staying accurate. What you give up with either is everything that needs the readings themselves: the median, a percentile, or the spread of only the last ten minutes.

06 / Give it a real job

Send summaries, not readings.

A network of stations reports to one server. Each sends a summary every minute, 60 readings folded into three numbers, and the server builds the hour. merge adds the counts, moves the mean by the gap between the two means times the second part’s share, and adds to the squares one extra term: the gap squared times the two counts multiplied together, over the combined count.

Merging isn’t bit for bit the same as adding every reading in order; the rounding happens in different places. In both languages the tests merge 60 minute summaries into the hour, and the squares agree with adding every reading at once to within a billionth. Split generated readings at random and they agree to within a hundred-millionth. The call site prints the same hourly variance both ways.

checkSummary runs first. A summary with a negative count, negative squares, or a spread from a single reading couldn’t have come from real readings, and one bad message would spoil the hour.

What a summary can’t do is forget. To report the last ten minutes instead of the day so far, start a fresh summary each ten minutes, or keep the readings from the window. Taking a reading back out needs the reading itself, which is the thing the summary didn’t keep.

Build UIs?Your browser already keeps running totals for video calls. A latency readout is where you own it.

Where it already is in your components

In a WebRTC video call, every incoming video stream’s statistics include totalInterFrameDelay and totalSquaredInterFrameDelay. The WebRTC statistics specification defines the second as the “Sum of the squared interframe delays in seconds between consecutively rendered frames,” and gives the variance as (totalSquaredInterFrameDelay - totalInterFrameDelay^2/ framesRendered)/framesRendered. MDN’s compatibility data lists both in Chrome from version 79 and Firefox from version 106, and not in Safari.

So getStats() gives you a spread without the browser keeping every frame. It is the running-totals form, not Welford’s, and it is fine there: a frame delay’s spread is not tiny next to the delay itself, so the subtraction keeps most of its digits. The TypeScript tests run 100,000 delays around a thirtieth of a second through both, and they agree to within a billionth.

When you have to own it

An operations dashboard stays open all day and calls its API thousands of times, and the footer shows how long requests take. Keeping every duration grows without end; createLatencyMeter keeps one summary. time wraps any async work, records the duration whether it succeeds or throws, and read returns the count, mean, and deviation in milliseconds.

latency-meter.ts
import { add, empty, standardDeviation, type Summary } from '../summary';

// A latency readout for a page that stays open all day. Every request's duration goes into three
// numbers, so the readout costs the same after ten requests or ten million. It gives the mean and
// the spread, not a 95th percentile: that needs a histogram.
export type LatencyReadout = {
	requests: number;
	meanMs: number | null;
	deviationMs: number | null; // needs two requests
};

export function createLatencyMeter(now: () => number = () => performance.now()) {
	let summary: Summary = empty;
	return {
		// Time one piece of async work, whether it resolves or throws.
		async time<T>(work: () => Promise<T>): Promise<T> {
			const start = now();
			try {
				return await work();
			} finally {
				summary = add(summary, now() - start);
			}
		},
		read(): LatencyReadout {
			return {
				requests: summary.count,
				meanMs: summary.count > 0 ? summary.mean : null,
				deviationMs: standardDeviation(summary)
			};
		},
		reset() {
			summary = empty;
		}
	};
}

// const api = createLatencyMeter();
// const response = await api.time(() => fetch('/api/orders'));
// const { requests, meanMs, deviationMs } = api.read();

Nothing here belongs to React or Svelte. Create the meter once, outside any component, and read it on an interval into state. If the dashboard needs a 95th percentile, this can’t give it; that needs a histogram. The tests use a fake clock, check a meter with no requests, one, and many, and time work that throws.

07 / Make the call

Mean and spread, and only those.

Reach for it when readings arrive faster than you want to store them and the mean and spread are the questions: sensor feeds, request timings, a score over millions of events, numbers sent between machines. Running totals would cost the same, but they are only safe when you know the spread is not tiny next to the readings themselves, and Welford doesn’t need you to know.

The mean and deviation describe typical readings badly when a few are wild. In the lab’s A gust spike preset, one reading moves the mean about 8 Pa and the deviation from 0.76 Pa to 23 Pa. If a single reading can do that, look at the readings or a median, not only the spread.

The answers are floating-point. Welford is accurate, not exact, so compare a variance with a tolerance, never with ===. And pick sample or population on purpose: River’s stats.Var, the lesson’s code, and the WebRTC formula don’t all divide by the same thing.

SourcesDocumentation, source, and the paper’s record, checked 14 September 2026

08 / Take the idea with you

Explain a running spread without saying “Welford.”

“Keep how many readings you’ve seen, their average, and a running sum of squared distances. For each new reading, nudge the average toward it by its distance divided by the count, and add its distance from the old average times its distance from the new one. Divide the sum by one less than the count for the variance.”

Before moving on, find a number in your work that you average: response times, build durations, order values. Ask whether you store every value just to show the mean and spread, and whether you also need a percentile, which a summary can’t give.

Connections to follow nextRelated lessons

Copy the complete example, add a sixth reading of 101,330 Pa to the minute, and predict the new mean and squares before you run it.

Back to applied algorithms →