Performance Metrics
Percentiles: p50, p95, p99
Read aloud is unavailable in this browser.
Beyond Averages: The Full Picture
Imagine a restaurant that advertises "average wait time: 10 minutes." Sounds reasonable. But what if 90% of customers wait 5 minutes while 10% wait 55 minutes? The average is still 10 minutes—but the experience for that unlucky 10% is terrible. Would you dine there if you knew you had a 1-in-10 chance of waiting nearly an hour?
This is the fundamental problem with averages in performance metrics. They hide the tail, obscure variability, and give a false sense of confidence. In system performance, the tail is often where the most important stories hide—the users who had a terrible experience, the requests that timed out, the edge cases that reveal architectural weaknesses.
Percentiles solve this problem. They describe the entire distribution of performance, letting you understand not just the typical case but every level of the experience spectrum—from the best to the worst.
This page will transform how you think about performance data. You'll learn what percentiles are mathematically, why they matter more than averages, how to use them in practice, and how to set meaningful SLOs using percentile-based targets. By the end, you'll never again be satisfied with a single "average latency" number.
Why Averages Fail for Performance Metrics
Before understanding what percentiles offer, we must understand why averages are insufficient—and even dangerous—for performance analysis.
The Mathematical Problem
The arithmetic mean (average) is calculated as:
average = sum(all_values) / count(all_values)
This single number collapses an entire distribution into one point, losing all information about variability, shape, and outliers. Two completely different distributions can have identical averages:
| Distribution A (Consistent) | Distribution B (Variable) |
|---|---|
| Request 1: 100ms | Request 1: 50ms |
| Request 2: 100ms | Request 2: 50ms |
| Request 3: 100ms | Request 3: 50ms |
| Request 4: 100ms | Request 4: 50ms |
| Request 5: 100ms | Request 5: 50ms |
| Request 6: 100ms | Request 6: 50ms |
| Request 7: 100ms | Request 7: 50ms |
| Request 8: 100ms | Request 8: 50ms |
| Request 9: 100ms | Request 9: 50ms |
| Request 10: 100ms | Request 10: 550ms |
| Average: 100ms | Average: 100ms |
Both distributions show 100ms average latency. But Distribution A is perfectly consistent—every user gets 100ms. Distribution B has 90% of users at 50ms (twice as fast!) but 10% at 550ms (terrible experience). These systems have radically different user experiences despite identical averages.
The Outlier Domination Problem
Averages are sensitive to extreme values. A single outlier can dramatically skew the average:
- 99 requests at 50ms + 1 request at 5 seconds = 99ms average
- Without the outlier: 99 requests at 50ms = 50ms average
One bad request (1% of traffic) doubles the reported average latency. Is the system twice as slow? No—99% of users had an excellent experience. The average hides this.
You "optimize" your system and see average latency drop from 200ms to 150ms—a 25% improvement! But what actually happened? The tail got slightly better (p99 dropped from 5s to 4s) while the median got worse (p50 increased from 100ms to 140ms). More users now have a worse experience than before, even though the average improved.
Understanding Percentiles Mathematically
A percentile (also called quantile) is a value below which a given percentage of observations fall. If the 95th percentile (p95) of latency is 200ms, it means:
- 95% of requests completed in ≤200ms
- 5% of requests took >200ms
Percentile Calculation
To find the p-th percentile:
- Sort all observations from smallest to largest
- Find the value at position (p/100) × n, where n is the total count
- If the position isn't an integer, interpolate between adjacent values
Common Percentiles in System Performance
| Percentile | Also Known As | What It Represents | Typical Use |
|---|---|---|---|
| p50 | Median | The "typical" experience—half faster, half slower | Baseline performance indicator |
| p75 | Third quartile | Upper bound for most users | General health indicator |
| p90 | 90th percentile | 9 in 10 users are better than this | Common SLO target |
| p95 | 95th percentile | 19 in 20 users are better than this | Aggressive SLO target |
| p99 | 99th percentile | 99 in 100 users are better than this | Premium tier SLO target |
| p99.9 | Three nines | 999 in 1000 users are better than this | Critical systems SLO |
| p99.99 | Four nines | 9999 in 10000 users are better than this | Trading/real-time systems |
Why p50 (Median) vs Mean?
The median is the "middle" value—half of observations are above it, half below. Unlike the mean, the median is:
- Robust to outliers: A single 5-second request doesn't move the median
- Representative of typical experience: The median user's actual experience
- Stable over time: Less noisy in monitoring dashboards
For most system performance contexts, the median is a better measure of "typical" than the mean.
Distribution Shape Matters
Percentiles together describe the distribution shape:
- Tight distribution: p50, p90, p99 are close together (e.g., 50ms, 55ms, 70ms)
- Long-tailed: p50 is low, p99 is high (e.g., 50ms, 500ms, 5s)
- Bimodal: Two distinct "humps" in the distribution (cache hits vs. misses)
Seeing p50=50ms, p99=5s tells you the system is highly variable—the tail is 100× worse than the median.
Reading and Interpreting Percentile Data
Understanding percentile data requires practice. Let's work through how to read and interpret common percentile presentations.
Percentile Profiles
A percentile profile is a summary of key percentile values. Here's an example API response time profile:
| Metric | Value | Interpretation |
|---|---|---|
| p50 (Median) | 45ms | Half of all requests complete in under 45ms |
| p75 | 62ms | 75% of requests complete in under 62ms |
| p90 | 98ms | 90% of requests complete in under 98ms |
| p95 | 156ms | 95% of requests complete in under 156ms |
| p99 | 423ms | 99% of requests complete in under 423ms |
| p99.9 | 1,247ms | 99.9% complete in under 1.25 seconds |
| Mean | 78ms | Arithmetic average (less meaningful) |
What This Profile Tells Us:
- The typical experience is good: 45ms median is snappy
- Most users are happy: 90% get <100ms
- The tail is long: p99 is 9× the median, p99.9 is 28× the median
- Mean is misleading: 78ms suggests worse typical performance than the 45ms median
Reading the Gaps
The gaps between percentiles reveal where problems concentrate:
- p50 to p90 (45ms → 98ms): Reasonably tight, 2× degradation for worst 50%→10%
- p90 to p99 (98ms → 423ms): Large gap (4×), significant tail starting here
- p99 to p99.9 (423ms → 1,247ms): 3× worse for the extreme tail
This pattern suggests something is causing occasional severe delays—perhaps GC pauses, lock contention, or cold cache hits. Investigating requests in the p99-p99.9 range would be valuable.
Quick health check: Calculate p99/p50. A healthy system typically has p99/p50 < 10×. Above 10× suggests serious tail latency problems. Above 50× indicates the tail is dominating and needs urgent attention. Our example: 423ms/45ms ≈ 9.4×—marginal but not critical.
Why Percentiles Matter Even More at Scale
The importance of tail percentiles increases with scale. At low volume, the tail affects few users. At high volume, the tail affects many—and can become the dominant experience.
The Scale Multiplication Effect
| Request Volume | p99 Users Affected | p99.9 Users Affected | p99.99 Users Affected |
|---|---|---|---|
| 1,000/day | 10 | 1 | 0.1 (rare) |
| 100,000/day | 1,000 | 100 | 10 |
| 10,000,000/day | 100,000 | 10,000 | 1,000 |
| 1,000,000,000/day | 10,000,000 | 1,000,000 | 100,000 |
At 1,000 requests per day, p99 affects 10 users—probably acceptable. At 1 billion requests per day (Google/Facebook scale), p99 affects 10 million users daily. Suddenly that "only 1%" tail represents a city's worth of unhappy users.
The Session Probability Problem
Consider a user session with 100 API calls (typical for a modern web app). If any single call is slow, the session feels slow. What's the probability of at least one slow call?
P(at least one slow) = 1 - P(all fast)^n
P(at least one slow) = 1 - (1 - p)^100
For different "slow" thresholds (beyond p99):
| Individual Slow Rate | Calls per Session | P(Session Has Slow Call) |
|---|---|---|
| 1% (p99 threshold) | 100 | 63.4% |
| 0.1% (p99.9 threshold) | 100 | 9.5% |
| 0.01% (p99.99 threshold) | 100 | 1.0% |
| 1% (p99 threshold) | 50 | 39.5% |
| 1% (p99 threshold) | 20 | 18.2% |
With 100 API calls per session and 1% slow rate, 63% of sessions experience at least one slow call! Your p99 isn't the "rare case"—it's the majority experience at the session level. This is why high-scale systems obsess over p99.9 and p99.99.
Percentiles in Fan-Out Architectures
Modern systems often use fan-out patterns where one request triggers multiple parallel sub-requests. Understanding how percentiles behave in fan-out is critical for realistic latency planning.
The Fan-Out Amplification Effect
When you fan out to N backends in parallel, the aggregate latency is the maximum of all individual latencies (you wait for the slowest). This means tail latencies aggregate badly:
P(aggregate > threshold) = 1 - P(all < threshold)^N
= 1 - (percentile/100)^N
Example: Search Fan-Out
A search query fans out to 100 index shards in parallel. Each shard has independent latency with p99=200ms. What's the aggregate p99?
| Individual Percentile | Individual Latency | Aggregate Probability Below | Aggregate Effective Percentile |
|---|---|---|---|
| p99 | 200ms | (0.99)^100 = 36.6% | ~p37 (only 37% aggregate faster) |
| p99.9 | 500ms | (0.999)^100 = 90.5% | ~p90 |
| p99.99 | 1s | (0.9999)^100 = 99.0% | ~p99 |
The Shocking Result:
With 100-way fan-out, the individual p99 becomes the aggregate p37! Two-thirds of aggregate requests will exceed what you thought was your p99 target. To achieve aggregate p99, you need individual p99.99 performance.
Implications for System Design:
- Fan-out multiplies tail requirements: For N parallel calls, target individual p(100-1/N×100) to achieve aggregate p99
- Reduce fan-out when possible: Fewer parallel calls = less tail amplification
- Use hedged requests: Send duplicate requests to reduce effective tail
- Set aggressive per-shard timeouts: Don't let one slow shard delay the entire aggregate
- Consider partial results: Return best-effort results if some shards timeout
You cannot combine percentiles across systems by averaging them. The p99 of system A + p99 of system B ≠ p99 of the combined system. Percentiles don't aggregate mathematically like sums or averages. You must measure the combined distribution directly or use approximation techniques like t-digest.
Setting SLOs Using Percentiles
Percentiles are the correct language for Service Level Objectives (SLOs). An SLO should specify a percentile target, not an average target.
SLO Structure
A well-formed percentile SLO has three components:
[Percentage] of [requests/operations] should complete in under [threshold]
Examples:
- 99% of API requests should complete in under 300ms
- 99.9% of database queries should complete in under 100ms
- 95% of page loads should complete in under 2 seconds
Multi-Tier SLOs
Mature systems often have multiple SLO tiers:
Latency SLO Tiers:
- p50: < 100ms (typical experience)
- p90: < 250ms (good experience)
- p99: < 1s (acceptable experience)
- p99.9: < 5s (maximum tolerable)
Different tiers serve different purposes:
- p50 SLO: Ensures typical user has a good experience
- p99 SLO: Limits worst-case for most users
- p99.9 SLO: Prevents extreme outliers from being too extreme
| System Type | Primary SLO | Secondary SLO | Rationale |
|---|---|---|---|
| Public Web API | p95 < 500ms | p99 < 2s | User-facing, competitive market |
| Internal Microservice | p99 < 100ms | p99.9 < 500ms | On critical path, must be reliable |
| Batch Processing | p50 < 10min | p99 < 1hr | Throughput matters more than tail |
| Trading System | p99.9 < 10ms | p99.99 < 50ms | Extreme latency sensitivity |
| Background Jobs | p90 < 5min | p99 < 30min | Latency matters less, completion matters |
Set initial SLOs based on observed performance with a buffer. As you optimize, tighten the SLOs. It's much easier to tighten a met SLO than to explain a missed one. A p99 < 2s SLO consistently met is better than a p99 < 500ms SLO frequently violated.
Measuring and Computing Percentiles
Computing percentiles at scale presents engineering challenges. Naive approaches that store every observation quickly exhaust memory. Let's examine practical approaches.
Naive Approach: Store Everything
collect all values → sort → extract percentiles
Memory usage: O(n) where n = number of observations
Problems:
- 1M requests/minute × 8 bytes = 8MB/minute = 480MB/hour just for storage
- Sorting for each query is expensive: O(n log n)
- Doesn't scale to streaming or long time windows
Practical Approach: Histograms
Pre-define buckets and count observations in each:
Buckets: [0-10ms, 10-25ms, 25-50ms, 50-100ms, 100-250ms, ...]
Counts: [15,000, 45,000, 82,000, 54,000, 8,000, ...]
Memory usage: O(number of buckets) = typically 20-50 values = constant
Advantages:
- Constant memory regardless of observation count
- Mergeable: can combine histograms from multiple servers
- Fast: O(1) per observation, O(buckets) to compute percentile
Disadvantages:
- Bucket boundaries are fixed, limiting precision
- Poor resolution at distribution edges if buckets poorly chosen
HDR Histogram uses logarithmic bucket sizing to maintain high precision across a wide range (e.g., 1μs to 1 hour) with configurable significant digits (typically 2-3). This gives accurate percentiles up to p99.999 with minimal memory. It's the gold standard for latency measurement libraries.
Streaming Approach: T-Digest
T-Digest is an algorithm that maintains a compact summary of a distribution with better precision at the tails:
- Stores ~100-200 "centroids" representing data clusters
- Higher precision near p0 and p100 (the tails)
- Mergeable for distributed computation
- Typically <1% error for p99 with few KB of memory
When to Use Which:
- HDR Histogram: Single-server latency measurement with known range
- T-Digest: Distributed systems needing to merge across servers
- Prometheus Histogram: When using Prometheus; uses fixed buckets
- Full Storage: Offline analysis where you need exact values
| Approach | Memory | Accuracy | Mergeable | Best For |
|---|---|---|---|---|
| Store All | O(n) | Exact | No | Small datasets, offline analysis |
| Fixed Histogram | O(buckets) | Bucket-limited | Yes | Known value ranges |
| HDR Histogram | Few KB | High (configurable) | Yes | Single-process latencies |
| T-Digest | Few KB | Very high at tails | Yes | Distributed systems |
| Sampling | O(sample size) | Statistical | Yes | Very high volume |
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.