Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Understanding Latency Percentiles (p50, p95, p99)

Recommended resources

Hello! Welcome back to your course on building a rate limiter engine.

In our last session, you conducted a vital performance comparison between your in-memory and Redis-based rate limiters. You gathered raw data on throughput and latency using the LoadTestUtility, which gave you a first look at the p50, p95, and p99 latency percentiles.

Today, we will dive deeper into those numbers. Simply calculating metrics isn't enough; the real skill lies in interpreting them to understand system behavior and diagnose problems. This lesson will equip you with the knowledge to translate your benchmark results into a compelling narrative about your rate limiter's performance characteristics—an essential component for your portfolio project.

Your learning outcome for this lesson:

You will learn to calculate and interpret latency percentiles (p50, p95, p99) from test results.



1. Why Averages Lie and Percentiles Tell the Truth

You've probably seen "average response time" used as a performance metric. While simple, it can be dangerously misleading. A system's performance is rarely uniform; outliers—a few very slow requests—can disproportionately skew the average, masking the experience of the majority of users.

To understand this better, let's start with a short video that explains the shortcomings of traditional metrics.

Percentile Tail Latency Explained (95%, 99%) Monitor Backend performance with this metric

Watch the beginning of this video from Hussein Nasser to understand why metrics like minimum, maximum, and especially average latency, are often insufficient for evaluating backend performance.

Watch from the start until 01:54.

The core issue is that latency distributions are often "long-tailed." Most requests are fast, but a small number can be very slow due to factors like garbage collection (GC) pauses, network hiccups, or lock contention.

This graph powerfully illustrates the concept. While the average latency might look stable and low, the p99 latency reveals periodic spikes, representing poor performance for a small but significant fraction of requests.

Average Latency vs. p99 Latency Graph
This graph compares average latency (blue) with p99 latency (red) over 24 hours. Notice how the average remains low and stable, while the p99 reveals significant performance degradation that would otherwise be invisible.

The OneUptime article "P50 vs P95 vs P99 Latency Explained" provides an excellent numerical example demonstrating how a few slow requests can inflate the average.

P50 vs P95 vs P99 Latency Explained: What Each Percentile Tells You

To solidify this concept, please read the section 'Why Not Just Use Average Latency?' from the OneUptime blog.

Read the section titled 'Why Not Just Use Average Latency?'. Pay close attention to the example calculation that shows how the average can be nearly 2.5 times higher than the median.

This is why Site Reliability Engineers (SREs) and performance engineers rely on percentiles. They provide a much clearer, distribution-aware view of system performance.



2. Defining and Calculating Percentiles

Now that we agree on the why, let's formalize the what and how.

  • P50 (Median): The latency value that 50% of requests are faster than. This represents the typical user experience.
  • P95: 95% of requests are faster than this value. It's an indicator of the experience for the vast majority of users and helps identify "tail latency."
  • P99: 99% of requests are faster than this value. This metric is sensitive to the worst-case performance and often exposes outliers and systemic issues.

This video provides a great walkthrough of the concept and calculation process.

Mastering Latency Metrics: P90, P95, P99 | System Design

The following video from ByteMonk gives clear definitions for each percentile and shows a step-by-step example of how to calculate them from a list of request times.

Watch the sections from 00:47 to 03:17. This covers the definitions of P90, P95, and P99, and then demonstrates the calculation for P99 with a sample dataset.

The calculation process is straightforward:

  1. Collect the latency for every request.
  2. Sort the latencies in ascending order.
  3. Calculate the index for the desired percentile.

This latency histogram shows where these percentiles might fall in a real-world distribution.

Latency Distribution with P95 and P99 for 100,000 Requests
A histogram showing the latency distribution for 100,000 requests. The vast majority are fast (left side), but the p95 and p99 marks capture the performance of the slower requests in the 'tail' of the distribution.

Refining the Calculation in Java

In the previous lesson, our printLatencyPercentiles method used a simple index calculation: (int) (total * 0.95). While this works, the exact definition of a percentile can have nuances. Let's look at a more formal approach.

The Stack Overflow discussion below explores the correct way to implement the "Nearest Rank" method for calculating percentiles, which is a common standard.

Calculate percentile from a long array? - java - Stack Overflow

This Stack Overflow thread provides a precise discussion on implementing percentile calculations in Java. It's a great example of moving from a 'good enough' implementation to a more formally correct one.

Read the answer by user 'ajb' that starts with 'According to Wikipedia...'. This answer explains the formal 'Nearest Rank' method and corrects the common mistake of using floor (implicit in (int) casting) instead of ceiling for the index calculation.

Based on that discussion, here is a slightly more robust version of our percentile calculation method. It uses Math.ceil and subtracts 1 to get the correct 0-based array index, aligning with the formal definition.

You can update the printLatencyPercentiles method in your LoadTestUtility.java with this improved logic.

private static void printLatencyPercentiles(Queue<Long> latencies, double... percentiles) {
    if (latencies.isEmpty()) {
        System.out.println("No latency data recorded.");
        return;
    }

    List<Long> sortedLatencies = new ArrayList<>(latencies);
    Collections.sort(sortedLatencies);

    System.out.println("\nLatency Percentiles (ms):");
    for (double p : percentiles) {
        if (p < 0.0 || p > 1.0) {
            throw new IllegalArgumentException("Percentile must be between 0.0 and 1.0");
        }
        // Using the Nearest Rank method, adjusted for 0-based indexing
        int index = (int) Math.ceil(p * sortedLatencies.size()) - 1;
        if (index < 0) {
            index = 0; // Handle edge case for small lists or p=0
        }
        long latencyNanos = sortedLatencies.get(index);
        System.out.printf("  p%.1f:          %.3f\n", p * 100, latencyNanos / 1_000_000.0);
    }
}

// In your main method, you can call it like this:
// printLatencyPercentiles(latencies, 0.50, 0.95, 0.99);

Note: This version is parameterized to calculate any percentile you provide.



3. From Numbers to Insights: Interpreting Your Results

Now for the most important part: what do the numbers mean? A set of percentile values tells a story about your system's health. The OneUptime article you reviewed earlier contains two exceptionally useful tables for this.

P50 vs P95 vs P99 Latency Explained: What Each Percentile Tells You

Let's return to the OneUptime article. The next two sections provide a practical framework for interpreting percentile data and mapping it directly to potential architectural problems.

First, study the table under 'Typical Interpretation Framework'. Then, carefully read the section 'How Percentiles Map to Architecture Problems' and its accompanying table. This is the key to your analysis.

Let's put this into the context of your rate limiter project.

Observation from your tests... Likely Cause (from the table) Your Project's Context
P50 for Redis is higher than P50 for in-memory Upstream dependency blocking This is the baseline cost of the network round-trip to the Redis server. Every single request pays this price.
P99 for Redis is much higher than its P50 Contention, queue buildup, network jitter This reflects the variability of the network. While most requests to Redis are fast, some take longer due to network congestion or server load.
P99 spikes under high load (for any algorithm) GC pauses, lock contention If you see this, it could be the JVM garbage collector pausing the application, or for your lock-based algorithms, threads waiting for a lock.
All percentiles climb together under heavy load Global resource saturation Your CPU is maxed out, or another system-wide resource is exhausted. The system can't keep up with the request rate.

By using this framework, you can move from saying "Redis is slower" to a much more insightful analysis: "The Redis implementation introduces a baseline latency overhead of X ms, as seen in the p50 metric, due to network I/O. Furthermore, it exhibits higher tail latency (p99) under load, suggesting that network variability and Redis server contention are key factors at scale."



4. Your Turn: Analyze Your Benchmark Data

Take a moment to apply these concepts. Look at the benchmark results you collected in the previous lesson for your in-memory and Redis-based token bucket limiters.

Answer these questions for yourself:

  1. Baseline Cost: What is the difference in p50 latency between the in-memory and Redis implementations? This difference quantifies the fundamental overhead of going off-box for state management.
  2. Tail Behavior: Compare the gap between p50 and p99 for both implementations. Is the gap proportionally wider for the Redis version? What does this tell you about the predictability of its performance?
  3. Diagnosis: Using the "How Percentiles Map to Architecture Problems" table, how would you diagnose the performance characteristics you observed?

This reflective exercise is exactly the kind of analysis you should include in your project's README.md to demonstrate a deep understanding of the system you built.


Conclusion

Congratulations! You can now not only calculate but also skillfully interpret latency percentiles. This is a critical skill for any engineer working on performance-sensitive systems. You have the tools to analyze your load test results and articulate the trade-offs of your design decisions with clarity and data-backed evidence.

Key Takeaways:

  • Average latency is often misleading; percentiles (p50, p95, p99) provide a far more accurate picture of the user experience.
  • p50 represents the typical case, while p95 and p99 are crucial for understanding tail latency and system behavior under stress.
  • There's a formal method for calculating percentiles, and using a correct implementation is important for accurate results.
  • Most importantly, you can map specific percentile patterns to common architectural problems, turning raw data into actionable insights.

Next Steps

So far, our load tests have focused on performance. But do they prove the rate limiter is actually correct? In our next lesson, we will shift focus from performance interpretation to functional correctness. You will learn to write assertions verifying that the HTTP 200 and 429 response counts from your load test match the configured limits, ensuring your rate limiter behaves exactly as expected under pressure.