Blogp99 Latency Is Your Brand: Why Averages Lie About AI UXHit · Load & Latency

p99 Latency Is Your Brand: Why Averages Lie About AI UX

JK
James Kim · March 2026 · 10 min read

TL;DR

Your average latency is a number almost none of your users experience. The users who churn, complain, screenshot, and tweet are the ones who hit the tail, the p99 and p99.9, and at any real scale that tail is not an edge case. Google found that a service with a p99 of 10ms could see request latency balloon to 140ms under real fan-out, a 14x amplification. And in a fan-out of 100 services each with a 1% straggler rate, roughly 63% of top-level requests are delayed by at least one straggler. The tail is the experience for your power users, and it is the experience your competitor benchmarks against. Averages were built to hide it.

The average is the lie everyone agreed to believe

Picture a status meeting. The slide says "average response time: 240ms." Everyone nods. Meanwhile, one in fifty of your most engaged users, the ones who make the most requests, the ones whose lifetime value is highest, is regularly waiting two seconds, and a smaller slice is waiting eight. None of that is on the slide, because the average ate it.

The classic illustration, repeated across latency literature, is brutal in its simplicity: take 100 requests where 99 finish in 10ms and one takes 4 seconds. The average is about 50ms, a number that looks healthy and that not a single user experienced, 99 of them saw 10ms and one waited four full seconds. As the explainer on p99 latency from Aerospike stresses, at high volume that "rare" 1% is not rare at all: a service handling a million requests a day serves 10,000 sluggish responses every single day.

Here is the uncomfortable arithmetic of engagement: your heaviest users hit the tail most often. A user who makes one request a session sees your p99 once every hundred sessions. A power user making a hundred requests a session sees it almost every session. So your tail latency is, disproportionately, the lived experience of the exact people you most want to keep. That is why the headline of this post is not a metaphor: p99 latency is your brand, because it is the latency your best customers actually feel.

The core insight: Averages describe the system. Percentiles describe the user. You operate a system, but you are judged by users, so you must be measured in percentiles. The mean is the one statistic guaranteed to be optimistic about the experience of your most valuable accounts.

The canonical reason tails matter: fan-out

The definitive treatment of this is Jeff Dean and Luiz Barroso's "The Tail at Scale" (Communications of the ACM), and its central result is brutal for anyone building fan-out systems. If a single request depends on responses from many servers, the slowest of those servers sets your latency. The math: with one backend, a 1-in-100 slow response (p99) means 1% of user requests are slow. But fan out to 100 backends and the probability that at least one is slow becomes 1 - 0.99^100 ≈ 63%. Your p99 backend just became the median user experience, the exact 63% figure reproduced in modern writeups on tail-latency amplification.

# Tail amplification by fan-out
# If each of N parallel calls is independently slow with probability p,
# the request is slow if ANY call is slow:
P_slow_request = 1 - (1 - p) ** N

# p = 0.01 (each call's p99), the user-facing slow rate:
N=1   -> 1.0%
N=10  -> 9.6%
N=50  -> 39.5%
N=100 -> 63.4%
N=500 -> 99.3%

Modern AI products are fan-out machines. A single chat turn can hit an embedding model, a vector store, a reranker, a tool API, and the LLM itself, and an agent loop multiplies that by the number of steps. Every one of those hops contributes a tail, and the request inherits the worst one. This is why a system that looks healthy on averages can feel chronically unreliable: the architecture is mathematically engineered to surface tails.

Why the tail exists at all

Tails are not bugs you can simply fix; they are emergent properties of shared, contended systems. "The Tail at Scale" enumerates the usual suspects, and they all show up in AI serving:

  • Queueing. The most fundamental one. As utilization climbs toward 100%, queueing delay does not rise linearly, it explodes. A server at 80% utilization has a far longer tail than one at 50%, even though the average barely moved. This is basic queueing theory and the reason "we have headroom on average" is a dangerous sentence.
  • Shared resource contention. On a batching LLM server, a burst of long-context requests inflates per-token time for everyone in the batch.
  • Garbage collection, background tasks, maintenance. Periodic stalls that hit a random unlucky request.
  • Cold paths. A cache miss, a cold model replica, a connection that has to be re-established.

The implication is that you do not eliminate the tail, you manage it, and you measure it under the conditions that create it: high concurrency, mixed payloads, sustained load. A tail measured on a quiet system is fiction. Mitigations exist, request hedging, for instance, has been shown to cut p99 substantially, as in the reporting on adaptive hedged requests reducing p99 latency by 74%, but you cannot tune what you have not measured.

Queueing in one sentence: latency is roughly proportional to 1 / (1 - utilization), so going from 50% to 90% utilized does not raise tail latency by 1.8x, it raises it by 5x. Capacity headroom is not waste; it is your tail budget.

How averages actively deceive

Consider two services. Service A returns every request in exactly 300ms. Service B returns 99% of requests in 50ms and 1% in 25 seconds. Both can advertise an average near 300ms. Service A is a calm, predictable experience. Service B is a service where one in a hundred interactions is a catastrophe, a payment that hangs, a generation that times out, a voice agent that goes silent mid-sentence. The average says they are equivalent. Your users would tell you they are nothing alike.

This is why mature SRE practice, codified in Google's SRE Book chapter on Service Level Objectives, defines availability and latency SLOs in percentiles, not means. An SLO like "99% of requests complete under 500ms over a rolling 28 days" is honest in a way "average under 500ms" never is, because it makes an explicit, countable promise about the tail.

Measuring the tail honestly

Two traps make most tail measurements useless. First, too few samples. To estimate p99 with any confidence you need on the order of thousands of samples; a load test that fires 200 requests cannot even see p99, let alone trust it. Second, averaging the percentiles. You cannot average the p99s of ten one-minute windows to get the hour's p99, percentiles do not average. You must aggregate the raw samples (or use a mergeable sketch like t-digest / HDR histogram).

import numpy as np

def tail_report(samples_ms):
    n = len(samples_ms)
    if n < 2000:
        print(f"WARNING: {n} samples is too few to trust p99")
    p50, p90, p99, p999 = np.percentile(samples_ms, [50,90,99,99.9])
    print(f"p50 {p50:.0f}  p90 {p90:.0f}  p99 {p99:.0f}  p99.9 {p999:.0f} ms")
    # The number that matters: how much worse is the tail than the median?
    print(f"tail amplification p99/p50 = {p99/p50:.1f}x")
    # Honest SLO check
    slo_ms, target = 500,0.99
    within = np.mean(np.array(samples_ms) <= slo_ms)
    print(f"{within*100:.2f}% within {slo_ms}ms (SLO {target*100:.0f}%): "
          + ("PASS" if within >= target else "FAIL"))

For client-side, real-path measurement of a streaming endpoint, capture each request's TTFT and total under concurrency and feed those into the same percentiles. The browser gives you the genuine user-facing number, including every proxy and balancer hop the request really traverses:

// Fire N concurrent streaming requests, collect tail samples
async function tailProbe(url, body, headers, concurrency, rounds) {
  const samples = [];
  for (let r = 0; r < rounds; r++) {
    const batch = Array.from({ length: concurrency }, async () => {
      const t0 = performance.now();
      const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
      const reader = res.body.getReader();
      let ttft = null;
      while (true) {
        const { done } = await reader.read();
        if (ttft === null) ttft = performance.now() - t0;
        if (done) break;
      }
      samples.push({ ttft, total: performance.now() - t0 });
    });
    await Promise.all(batch);
  }
  return samples;   // thousands of points -> trustworthy p99
}

Gate on the tail, not the mean

If your CI gate asserts on average latency, it will wave through the exact regressions that hurt users most, the ones that fatten the tail while leaving the mean untouched. Assert on percentiles instead:

assert p99_ttft_ms  < 800,  f"p99 TTFT regressed: {p99_ttft_ms}ms"
assert p999_ttft_ms < 2500, f"p99.9 tail blew out: {p999_ttft_ms}ms"
assert (p99_ttft_ms / p50_ttft_ms) < 6, "tail amplification too high"

That third assertion is the one teams forget. A system whose p99 is a tight 3x of its median is predictable; one whose p99 is 15x its median is a slot machine, and your power users are pulling the lever a hundred times a day.

The browser is the unfair advantage here. Tail latency is created across the whole path, gateway, balancer, autoscaler, batching server, so it has to be measured end to end, at concurrency, against the real authenticated endpoint. A browser-native load tool fires thousands of real requests through your real edge using your real session, producing a p99 you can actually trust instead of one synthesized in a clean lab. That is what Hit was built to deliver.

The bottom line

Averages were invented to make systems look calm. Your users do not live in the average, they live in the tail, and your most valuable users live there most often because they make the most requests. Fan-out architecture, the default shape of every modern AI product, mathematically amplifies that tail until the p99 backend becomes the median experience. Measure with enough samples to see p99 and p99.9, aggregate raw samples instead of averaging percentiles, and gate your releases on the tail. Your brand is not your average. It is your p99.

Pressure-Test Your AI Before Production Does

Hit fires browser-native, streaming-aware load at your LLM and API endpoints, TTFT, inter-token latency, tokens/sec, and cost per request, with no account and no script.

Try Hit Free →
James Kim James Kim writes about AI quality engineering at alt.qa, built by TheWorkCompany.