How to calculate percentile in Real-Time Analytics: step by step
Learn how to calculate percentiles in Real-Time Analytics to monitor latency and performance metrics in streaming. Knowing how to calculate percentiles (e.g. P50, P95, P99) in real time helps identify degradations that the average does not show: for example, an average of 120 ms can hide that P95 = 450 ms, which means 5% of requests are very slow and affect the user experience.
Prerequisites
- Account with access to a service that supports streaming queries (e.g.: Azure Data Explorer/ADX, also applicable to other engines that support KQL).
- Telemetry data with a timestamp and a numeric field (e.g. latency_ms). Ideally events have event time and ingestion time to manage latency and reordering.
- Basic knowledge of KQL (filters, extend, summarize).
- Adequate ingestion capacity — for example 1k–10k events/s for small setups; for larger volumes consider partitioning and compression.
- Retention policy and windows defined (e.g. 7 days of data, minute-level aggregations for real-time dashboards).
Step 1: Structure the stream with timestamps and partitions
It is essential to ensure events have correct timestamps (event time) and, if necessary, a partition key (e.g. region or service). Events with incorrect timestamps create windows with zeros or overlaps. For production streaming, apply watermarking of 30s–2m depending on the producers' clock reliability. Partitions such as service or region allow calculating percentiles per segment without crossing heterogeneous traffic.
// Exemplo: dados simulados para testar
let telemetry = datatable(Timestamp:datetime, service:string, latency_ms:double)
[
datetime(2026-08-21 10:00:01), "api-a", 120.5,
datetime(2026-08-21 10:00:02), "api-a", 95.0,
datetime(2026-08-21 10:00:03), "api-b", 300.2,
datetime(2026-08-21 10:00:04), "api-a", 110.1
];
telemetry
Step 2: Choose time window and strategy (disjoint vs sliding)
Decide whether you want disjoint (tumbling) windows or sliding windows. Tumbling windows (e.g.: 1 minute) provide simple aggregates and are efficient for periodic reports. Sliding windows (e.g.: 5-minute window with a 1-minute step) provide smoother, continuous percentiles, but cost more because they overlap calculations — typically ~N times more work where N is the number of steps per window. For example, a 5m sliding window with a 1m step implies calculating 5 times more aggregations than a 1m tumbling window.
// Tumbling of 1 minute
telemetry
| where Timestamp > ago(15m)
| summarize percentiles(latency_ms, 50, 95, 99) by bin(Timestamp, 1m), service
// Sliding of 5 minutes with 1 minute step (use recurring bin ranges)
telemetry
| where Timestamp > ago(15m)
| serialize
| extend window_start = bin(Timestamp, 1m)
| summarize percentiles(latency_ms, 50, 95, 99) by window_start, service
| order by window_start asc
Step 3: Use the percentiles function correctly
The percentiles function (or percentileif/percentile depending on the engine) calculates percentiles efficiently. In ADX, percentiles accepts multiple percentiles as arguments. For large volumes, consider approximate algorithms (TDigest, sketches) or reducing precision to lower cost and latency. E.g.: use an approximation that yields a typical error of ±1–2% on quantiles for large samples.
// Percentiles P50, P95, P99 by service per minute
telemetry
| where Timestamp > ago(30m)
| summarize percentiles(latency_ms, 50, 95, 99) by bin(Timestamp, 1m), service
| project Timestamp, service, P50=latency_ms_50, P95=latency_ms_95, P99=latency_ms_99
Step 4: Handle cardinality and outliers
Percentiles can be unreliable with few samples. Define a confidence threshold: for example, cnt >= 10 for basic visibility, cnt >= 30 for reasonable statistics and cnt >= 100 for high confidence. Filter invalid values (negative, ultra-high due to error) and flag windows with low counts. For extreme outliers, you can truncate values above an SLA (e.g.: latency_ms > 60s) or apply winsorizing.
// Filtrar valores negativos, calcular contagem e aplicar limiar mínimo de amostras
telemetry
| where Timestamp > ago(30m) and latency_ms >= 0 and latency_ms < 60000
| summarize cnt=count(), percentiles(latency_ms, 50, 95, 99) by bin(Timestamp, 1m), service
| where cnt >= 10
| project Timestamp, service, cnt, P50=latency_ms_50, P95=latency_ms_95, P99=latency_ms_99
Step 5: Aggregate and export to real-time dashboards
Export the results to a dashboard (e.g. Power BI, Grafana) or write to an aggregates table for historicals. In ADX you can use the Power BI connector for near real-time data or store aggregates with .set-or-append / update policy to avoid redoing calculations. Evaluate cost: minute-level aggregations for 100 services over 24h result in ~144k windows/day; optimize by reducing resolution or using sketches.
// Exemplo simplificado: escrever para uma tabela de agregados (pseudo-sintaxe)
let aggreg =
telemetry
| where Timestamp > ago(30m) and latency_ms >= 0
| summarize cnt=count(), percentiles(latency_ms, 50, 95, 99) by bin(Timestamp, 1m), service;
// Em ADX, podes usar .set-or-append para gravar numa tabela existente
// .set-or-append Aggregates <| aggreg
Verify the result
Confirm you have rows for each window and that cnt (count) makes sense. Compare percentiles with a histogram (bins of 10–50 ms) to validate the shape of the distribution. Common errors: events with timestamps in the future/before causing empty windows, low counts and NaN percentiles, and windows with anomalous values due to retries. If P95/P99 vary a lot in a short time, investigate causes (deploy, GC, overload).
Conclusion
Calculating percentiles in Real-Time Analytics allows measuring the real user experience and detecting regressions that the average does not show. Next steps: experiment with sliding windows for continuous visibility, use TDigest/sketches to reduce costs at high volumes, and integrate with alerts (e.g.: alert if P95 > 300 ms for 3 consecutive windows). Decide which percentile to use according to your SLA — P95 is useful for general latencies, P99 for rare spikes.