skip to content

Percentile Estimation Choices

The HTML dashboard and the Aggregate Report listener use different percentile estimators, so one JTL yields two p90 figures. A favourite interview question because most teams never notice.

on this pageshow

explore

questions

5

Why does one JMeter results file give a different 90% Line in the HTML dashboard and in the Aggregate Report?

level: middleimportance: must knowfreq 58%

answer

  1. One file, two report surfaces
  2. Not a filtering difference
  3. One interpolates, one picks a sample
  4. commons-math3 EstimationType, default LEGACY

basics

~20 s

The two surfaces use different percentile estimators. JMeter's HTML dashboard uses the commons-math3 LEGACY type, which interpolates between neighbouring samples; the Aggregate Report listener returns an actually observed elapsed time. Set backend_metrics_percentile_estimator=R_3 to align them.

solid answer

~40 s

JMeter computes the two tables with unrelated code. The dashboard's Statistics table builds each percentile as a `PercentileAggregator`, which wraps a commons-math3 `DescriptiveStatistics` whose estimation type comes from the `backend_metrics_percentile_estimator` property. Its default, `LEGACY`, places the index at `p x (N + 1)` and **interpolates linearly** between the two order statistics either side, so it can print a figure no sample ever recorded. The Aggregate Report listener instead calls JMeter's own `StatCalculator.getPercentPoint`, which rounds `N x p` and walks a sorted map of observed elapsed times, always returning a time some sample actually took. With nine samples from 100 to 180 ms plus one 900 ms outlier, the dashboard prints **828 ms** and the listener prints **180 ms** for the same 90% Line. Setting `backend_metrics_percentile_estimator=R_3` makes the dashboard pick an order statistic too.

code

text · 7 lines
text
elapsed values for one label in results.jtl (ms):
100 110 120 130 140 150 160 170 180 900

HTML dashboard  90% Line = 828   (LEGACY: index 0.9 x 11 = 9.9,
                                  interpolate 180 -> 900)
Aggregate Report 90% Line = 180   (round(10 x 0.9) = 9 -> ninth value)
HTML dashboard  90% Line = 180   after backend_metrics_percentile_estimator=R_3

go deeper

for a junior

Recall that JMeter has two places a 90% Line appears - the HTML dashboard and the Aggregate Report listener - and that the same results file can show different values in them.

for a middle

Explain the mechanics: the dashboard's percentile aggregator interpolates under the LEGACY estimation type, the listener returns an observed elapsed time, and one property switches the dashboard to R_3.

for a senior

Show you would notice. Read both surfaces on a real run, spot the gap before someone quotes a number in a report, and pin the estimator on the machine that generates the report.

for a principal

Own the choice for the team: which estimator every JMeter install carries, where that property file lives, and the fact that the key also moves whatever the Backend Listener streams live.

## Two code paths over the same file A JMeter run writes one JTL. Loading that file into an **Aggregate Report** listener and generating the **HTML dashboard** from it look like two views of one dataset, but the percentile columns are produced by two unrelated pieces of code. - The dashboard's Statistics table comes from `StatisticsSummaryConsumer`. Its median and its three percentile figures are `PercentileAggregator` instances, and `PercentileAggregator` delegates to a commons-math3 `DescriptiveStatistics` built by `DescriptiveStatisticsFactory`, which calls `new Percentile().withEstimationType(...)`. - The Aggregate Report listener (`StatVisualizer`, sharing its table model with `StatGraphVisualizer`) calls `SamplingStatCalculator.getPercentPoint`, which is JMeter's own `org.apache.jorphan.math.StatCalculatorLong`. No commons-math3 is involved on that path at all. ## What the LEGACY estimator does The estimation type is read from the JMeter property `backend_metrics_percentile_estimator`, and its default is `LEGACY` — commons-math3's own historical type. `LEGACY` computes a fractional index of `p x (N + 1)` over the sorted values and interpolates linearly between the order statistics on either side: ``` value = lower + fraction x (upper - lower) ``` Two consequences follow directly: 1. the printed figure need not equal any elapsed time in the file; 2. when the two neighbouring order statistics are far apart — a fast body with one slow tail sample — the interpolation drags the result a long way toward the slow one. That is exactly what the manual means when it warns the divergence is "most observable when the distribution of the timing values is spread too wide" or when too few samples were taken. ## What the Aggregate Report does `StatCalculator` keeps a `TreeMap` of distinct elapsed times to their counts. `getPercentPoint(p)` computes `Math.round(N x p)` and walks that sorted map subtracting counts until the target reaches zero, then returns **that key**. It never interpolates, so the 90% Line is always a time some sample actually took. It is also why the component reference warns that this listener needs extra memory for Median and the 90% Line, and why the Summary Report — which stores no individual samples — offers no percentile columns at all. ## The worked example Nine samples at 100, 110 ... 180 ms and one at 900 ms, all under one label: | Surface | Estimator | 90% Line | |---|---|---| | HTML dashboard, out of the box | `LEGACY` | 828 ms | | HTML dashboard, estimator switched | `R_3` | 180 ms | | Aggregate Report listener | `StatCalculator` | 180 ms | `LEGACY` puts the index at `0.9 x 11 = 9.9` — nine tenths of the way from the ninth value (180 ms) to the tenth (900 ms) — giving 828 ms. The listener rounds `10 x 0.9` to 9 and hands back the ninth value, 180 ms. Same file, same label, same requested percentile. ## Making them agree The manual's own instruction is to change the estimator: - set `backend_metrics_percentile_estimator=R_3` — note there is **no** `jmeter.reportgenerator.` prefix on this key; - `R_3` takes the index as `rint(N x p)`, an integer, so the interpolation weight is zero and an observed order statistic comes back; - the key is absent from the shipped `bin/jmeter.properties`, so you add the line to `user.properties` yourself. ## What switching does not fix - `R_3` rounds half to even while `StatCalculator` uses `Math.round` (half up), so where `N x p` lands exactly on `.5` the two can still land one order statistic apart. The manual says "more or less the same" and means it. - The estimator is read once into a `static final` field, so it is a JVM-wide choice made before the run starts, not something a plan can flip. - The dashboard's percentiles are additionally capped by `jmeter.reportgenerator.statistic_window` (20,000 samples per row by default) while the listener has no window at all — an independent second source of divergence on a large run. - The same key feeds the Backend Listener's `SamplerMetric`, so flipping it also moves the percentiles streamed live.

  • Which of the two figures should a reviewer treat as an elapsed time that really occurred?
    The Aggregate Report's. `StatCalculator.getPercentPoint` returns a key from its map of observed elapsed times, so 180 ms is a time some sample took. The dashboard's default `LEGACY` figure of 828 ms is an interpolation between two neighbouring samples and matched nothing in the file.
  • Does raising the sample count make the two figures converge?
    Usually, yes. The interpolation weight only moves the answer as far as the gap between neighbouring order statistics, and with many samples that gap shrinks. The manual frames the divergence as worst when values are widely spread or too few samples were taken. It is a reduction, not a guarantee.
  • Where does the dashboard's Median column sit in this?
    On the same footing. In `StatisticsSummaryData` the median is a fourth `PercentileAggregator`, so it uses the identical estimator and the identical sliding window as the three percentile columns, and it can diverge from the listener's Median in exactly the same way.

saying these in an interview costs you the question

  • Claims the dashboard and the listener read different result files
  • Calls one of the two numbers a JMeter bug
  • Assumes the dashboard silently drops failed or slow samples
  • Thinks the Aggregate Report can print a time no sample recorded
  • Says aggregate_rpt_pct1 selects the estimation formula
open as a page

In JMeter, which properties decide that reports show the 90th, 95th and 99th percentiles?

level: juniorimportance: should knowfreq 34%

basics

~20 s

The three properties aggregate_rpt_pct1, aggregate_rpt_pct2 and aggregate_rpt_pct3, defaulting to 90, 95 and 99. Each takes a float between 0 and 100, so 99.9 is valid, and they move the GUI listeners and the HTML dashboard together.

open as a page

Why can a JMeter dashboard's 99th percentile miss a slow warm-up that its Max column still shows?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The dashboard's percentile columns run over a sliding window of the most recent samples, sized by jmeter.reportgenerator.statistic_window (default 20,000 per row). Min, Max and Average are not windowed, so they still see the whole run.

open as a page

How would you pin JMeter's percentile settings so every injector and every generated report agrees?

level: principalimportance: should knowfreq 29%

basics

~10 s

Ship one properties file with every JMeter install carrying the estimator, the report window and the three percentile levels. All three are read at startup, and the JVM generating the report needs them too.

open as a page

Which JMeter property selects the HTML report's percentile estimator, and what values does it take?

level: middleimportance: nice to knowfreq 27%

basics

~10 s

The key is backend_metrics_percentile_estimator, with no jmeter.reportgenerator prefix. It accepts a commons-math3 Percentile.EstimationType constant name: LEGACY (the default) or R_1 through R_9. Nothing else parses.

open as a page