“The error rate doubled” describes a change, but not the whole incident.
A team reviews two illustrative 10-minute windows around a checkout release. Before the release, 18 of 3,600 requests returned a server error. In the comparison window after release, 36 of 3,600 did. Both requests and counts below are invented for teaching; they are not production telemetry.
The counts support a higher observed error fraction in the second window. They do not yet tell us whether the release caused it. Traffic mix, dependency health, regional routing, sampling, and a change in the event definition could all affect the comparison. First reproduce the numbers and state what each percentage is a percentage of.
- Before
- 18 failed requests ÷ 3,600 total = 0.5%.
- After
- 36 failed requests ÷ 3,600 total = 1.0%.
- Window
- Two illustrative, equal-length 10-minute periods.
- Question
- How large is the change, and what should the team check before attributing it?
A percentage is a ratio whose denominator defines the population.
A percentage expresses a fraction per hundred. For an observed request error rate, divide the number of requests that meet the stated failure condition by all requests in the chosen population, then multiply by 100:
error rate = failed requests ÷ total requests × 100
Before the release: 18 ÷ 3,600 = 0.005, then 0.005 × 100 = 0.5%. After: 36 ÷ 3,600 = 0.01, then 0.01 × 100 = 1.0%. The fraction is dimensionless: requests divided by
requests. The multiplication by 100 changes how it is displayed, not the underlying
fraction.
| Window | Failed requests | Total requests | Calculation | Observed rate |
|---|---|---|---|---|
| Before | 18 | 3,600 | 18 ÷ 3,600 × 100 | 0.5% |
| After | 36 | 3,600 | 36 ÷ 3,600 × 100 | 1.0% |
Percentage points measure the gap; relative change measures it against a baseline.
Subtract one percentage rate from another to find a percentage-point change: 1.0% − 0.5% = 0.5 percentage points. This is the absolute difference between
the rates. The “percent” unit stays attached to the rates, so their difference is measured
in percentage points.
For relative percent change, divide that difference by the starting
value: (after − before) ÷ before × 100. Here that is (1.0% − 0.5%) ÷ 0.5% × 100 = 100%. The starting rate is the denominator.
Saying “up 100%” without the baseline can sound larger or smaller than the absolute change
actually is; saying “up half a point” alone may also hide that the measured rate doubled.
| Measure | Calculation | Result | Question answered |
|---|---|---|---|
| Point change | 1.0% − 0.5% | +0.5 percentage points | How many percentage points separate the rates? |
| Relative change | (1.0% − 0.5%) ÷ 0.5% × 100 | +100% | How large is the gap compared with the starting rate? |
Arithmetic can be right while the comparison is still unfair.
Equal-length windows are a start, not a proof of comparability. Confirm that both rates count the same kind of request and the same failure outcome. Check route, region, client type, traffic source, and whether retries or health checks enter the denominator. A shifted mix can change the total rate even when each group’s rate stays constant.
Then inspect the timeline. Did the increase begin with the rollout? Were there changes to a dependency, traffic, or monitoring at the same time? Did counts come from sampled or delayed telemetry? Recalculate by a useful segment and compare raw counts. These checks separate “the dashboard number changed” from “this release caused more failures.”
Try a smaller baseline and watch the relative change move.
Keep the event definition fixed: the number of requests that returned the selected server error, among all requests in each window. The calculator recomputes each rate independently. It shows the point difference and relative change separately; it does not assess whether the windows are statistically or operationally comparable.
Enter whole request counts for two windows using the same failure definition.
This is descriptive arithmetic, not a significance test or causal diagnosis. A rate from a small or changing population may be noisy.
Make the zero baseline and invalid denominator explicit.
The implementations return rates in percent, the difference in percentage points, and a relative change when the starting rate is nonzero. A zero baseline has no defined relative percent change: dividing by zero is not a meaningful way to summarize it. The count checks also enforce the lesson’s event model, where one request is either failed or not failed.
Both implementations validate counts and keep point change separate from relative change.
export type RateComparison = {
beforePercent: number;
afterPercent: number;
percentagePointChange: number;
relativePercentChange: number | null;
};
export function compareRates(
beforeEvents: number,
beforeTotal: number,
afterEvents: number,
afterTotal: number
): RateComparison {
for (const [name, value] of [
['before events', beforeEvents],
['before total', beforeTotal],
['after events', afterEvents],
['after total', afterTotal]
] as const) {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`${name} must be a non-negative safe integer`);
}
}
if (beforeTotal === 0 || afterTotal === 0) {
throw new RangeError('each comparison window must contain at least one request');
}
if (beforeEvents > beforeTotal || afterEvents > afterTotal) {
throw new RangeError('failed requests cannot exceed total requests');
}
const beforePercent = (beforeEvents / beforeTotal) * 100;
const afterPercent = (afterEvents / afterTotal) * 100;
const percentagePointChange = afterPercent - beforePercent;
const relativePercentChange =
beforePercent === 0 ? null : (percentagePointChange / beforePercent) * 100;
return { beforePercent, afterPercent, percentagePointChange, relativePercentChange };
}
const comparison = compareRates(18, 3_600, 36, 3_600);
console.log(comparison);
package main
import (
"errors"
"fmt"
)
type RateComparison struct {
BeforePercent float64
AfterPercent float64
PercentagePointChange float64
RelativePercentChange *float64
}
func compareRates(beforeEvents, beforeTotal, afterEvents, afterTotal int64) (RateComparison, error) {
if beforeEvents < 0 || afterEvents < 0 || beforeTotal < 0 || afterTotal < 0 {
return RateComparison{}, errors.New("counts must be non-negative")
}
if beforeTotal == 0 || afterTotal == 0 {
return RateComparison{}, errors.New("each comparison window must contain at least one request")
}
if beforeEvents > beforeTotal || afterEvents > afterTotal {
return RateComparison{}, errors.New("failed requests cannot exceed total requests")
}
beforePercent := float64(beforeEvents) / float64(beforeTotal) * 100
afterPercent := float64(afterEvents) / float64(afterTotal) * 100
percentagePointChange := afterPercent - beforePercent
var relativePercentChange *float64
if beforePercent > 0 {
change := percentagePointChange / beforePercent * 100
relativePercentChange = &change
}
return RateComparison{
BeforePercent: beforePercent,
AfterPercent: afterPercent,
PercentagePointChange: percentagePointChange,
RelativePercentChange: relativePercentChange,
}, nil
}
func main() {
comparison, err := compareRates(18, 3600, 36, 3600)
if err != nil {
panic(err)
}
if comparison.RelativePercentChange == nil {
fmt.Printf("before %.2f%%, after %.2f%%, change %.2f percentage points, relative change undefined\n",
comparison.BeforePercent, comparison.AfterPercent, comparison.PercentagePointChange)
return
}
fmt.Printf("before %.2f%%, after %.2f%%, change %.2f percentage points, relative change %.1f%%\n",
comparison.BeforePercent, comparison.AfterPercent, comparison.PercentagePointChange,
*comparison.RelativePercentChange)
}
A relative change needs a nonzero, useful baseline.
Suppose a metric moves from 0 failed requests out of 3,600 to 9 out of 3,600. The rates
are 0% and 0.25%. The point change is well defined: +0.25 percentage points.
Relative percent change is undefined because the baseline rate is zero. Do not manufacture
a finite answer by silently adding an arbitrary pseudocount; if you need such a
statistical model, state and justify it separately.
Now change the comparison: the earlier window contains 3,600 requests, while the later one contains 36,000. The rate still normalizes the volumes, but the larger later sample may make the estimated fraction more stable under similar conditions. It does not automatically make the populations comparable or explain what changed. Preserve both numerators and denominators.