P95 (95th Percentile)

performance

P95 (95th Percentile)

Definition & Overview

P95 (the 95th percentile) is a statistical measure indicating the value below which 95% of observations in a dataset fall. In the context of system performance, P95 latency means that 95% of requests complete within that time, while 5% take longer. It provides a more realistic picture of user experience than the average (mean), which can be misleadingly low when a few extremely slow requests inflate the average while most requests are fast.

Percentile metrics are essential for SLA (Service Level Agreement) definition because they directly map to user experience. Saying "our P95 latency is 200ms" means "19 out of 20 users experience response times at or below 200ms." This is far more meaningful than "our average latency is 50ms," which could hide the fact that 5% of users wait 3 seconds. Industry practice uses P50 (median) for typical experience, P95 for SLA targets, and P99 for worst-case monitoring.

The Prismatic Platform enforces strict P95 latency requirements across all user-facing endpoints. The page load performance standard mandates total page load under 250ms, server-side render under 100ms, LiveView mount under 150ms, and handle_event under 50ms. These are measured at P95, meaning they must hold for 95% of requests, not just the average case. Violations trigger merge blocks under the NO MERCY doctrine.

Technical Deep Dive

Computing P95 from a dataset requires sorting the values and finding the value at the 95th percentile rank. For N observations sorted in ascending order, P95 is at position 0.95 * (N - 1). When the position is not an integer, linear interpolation between the two surrounding values provides a more accurate estimate.

For streaming data where sorting is not feasible, approximate percentile algorithms are used. The t-digest algorithm maintains a compact sketch of the data distribution that can answer percentile queries with bounded error. HDR Histogram provides exact percentile computation for integer-valued data (like latencies in microseconds) with configurable precision and O(1) recording time.

defmodule PrismaticMonitoring.PercentileTracker do
  @moduledoc """
  Tracks P50, P95, and P99 latency percentiles for platform endpoints.
  Uses a sliding window approach with periodic computation.
  """

  use GenServer

  @type percentiles :: %{
    p50: number(),
    p95: number(),
    p99: number(),
    count: non_neg_integer(),
    min: number(),
    max: number()
  }

  @window_size 60_000
  @compute_interval 5_000

  # Client API

  @spec record(atom(), number()) :: :ok
  def record(endpoint, latency_ms) do
    GenServer.cast(__MODULE__, {:record, endpoint, latency_ms})
  end

  @spec get_percentiles(atom()) :: {:ok, percentiles()} | {:error, :no_data}
  def get_percentiles(endpoint) do
    GenServer.call(__MODULE__, {:get_percentiles, endpoint})
  end

  @spec check_sla(atom()) :: {:ok, :compliant} | {:error, :violation, map()}
  def check_sla(endpoint) do
    case get_percentiles(endpoint) do
      {:ok, percentiles} ->
        threshold = sla_threshold(endpoint)

        if percentiles.p95 <= threshold do
          {:ok, :compliant}
        else
          {:error, :violation, %{
            endpoint: endpoint,
            p95: percentiles.p95,
            threshold: threshold,
            excess_ms: percentiles.p95 - threshold
          }}
        end

      error ->
        error
    end
  end

  # Server Implementation

  @impl GenServer
  def init(_opts) do
    schedule_computation()
    {:ok, %{raw: %{}, computed: %{}}}
  end

  @impl GenServer
  def handle_cast({:record, endpoint, latency}, state) do
    now = System.monotonic_time(:millisecond)
    entry = {now, latency}

    raw = Map.update(state.raw, endpoint, [entry], &[entry | &1])
    {:noreply, %{state | raw: raw}}
  end

  @impl GenServer
  def handle_call({:get_percentiles, endpoint}, _from, state) do
    result = Map.get(state.computed, endpoint, :no_data)

    case result do
      :no_data -> {:reply, {:error, :no_data}, state}
      percentiles -> {:reply, {:ok, percentiles}, state}
    end
  end

  @impl GenServer
  def handle_info(:compute, state) do
    now = System.monotonic_time(:millisecond)
    cutoff = now - @window_size

    computed =
      state.raw
      |> Enum.map(fn {endpoint, entries} ->
        # Filter to window
        recent = Enum.filter(entries, fn {ts, _} -> ts > cutoff end)
        values = Enum.map(recent, &elem(&1, 1))

        percentiles =
          if length(values) >= 10 do
            sorted = Enum.sort(values)
            n = length(sorted)

            %{
              p50: percentile_at(sorted, n, 0.50),
              p95: percentile_at(sorted, n, 0.95),
              p99: percentile_at(sorted, n, 0.99),
              count: n,
              min: hd(sorted),
              max: List.last(sorted)
            }
          else
            :no_data
          end

        {endpoint, percentiles}
      end)
      |> Map.new()

    # Prune old entries
    pruned_raw =
      state.raw
      |> Enum.map(fn {endpoint, entries} ->
        {endpoint, Enum.filter(entries, fn {ts, _} -> ts > cutoff end)}
      end)
      |> Map.new()

    schedule_computation()
    {:noreply, %{state | raw: pruned_raw, computed: computed}}
  end

  defp percentile_at(sorted, n, p) do
    rank = p * (n - 1)
    lower = Enum.at(sorted, floor(rank))
    upper = Enum.at(sorted, ceil(rank))
    frac = rank - floor(rank)
    Float.round(lower + frac * (upper - lower), 2)
  end

  defp sla_threshold(endpoint) do
    case endpoint do
      :page_load -> 250.0
      :server_render -> 100.0
      :live_view_mount -> 150.0
      :handle_event -> 50.0
      :health_check -> 10.0
      _ -> 500.0
    end
  end

  defp schedule_computation do
    Process.send_after(self(), :compute, @compute_interval)
  end
end

The sliding window approach ensures that percentile calculations reflect recent performance rather than historical averages. A 60-second window (configurable) provides responsive alerting while smoothing momentary spikes. The computation runs every 5 seconds, providing near-real-time percentile updates for the monitoring dashboard.

Architecture & Implementation

P95 tracking is integrated into the platform's telemetry pipeline. Every HTTP request, LiveView event, and OSINT tool execution emits a telemetry event with duration metadata. The PercentileTracker GenServer subscribes to these events and maintains per-endpoint latency distributions. The computed percentiles are exposed to the monitoring dashboard via Phoenix PubSub and to the CI quality gate via a Mix task.

The performance standard enforcement operates at multiple levels. During development, mix performance.check validates P95 latency against thresholds using recorded benchmark data. In CI, integration tests include performance assertions that fail if P95 exceeds limits. In production, telemetry-based P95 monitoring triggers alerts at 80% of threshold (early warning) and blocks deployments at 100% of threshold.

Production telemetry uses the :telemetry library with handlers that feed into the PercentileTracker. The overhead of percentile tracking is minimal: recording a data point is O(1), and the periodic computation (sorting up to a few thousand entries) takes less than 1ms. This ensures monitoring does not degrade the performance it is measuring.

Usage in Prismatic Platform

CI quality gate enforcement:

defmodule Mix.Tasks.Performance.Check do
  @moduledoc """
  Validates P95 latency against platform performance standards.
  Exits with code 1 if any endpoint violates its SLA threshold.
  """

  use Mix.Task

  alias PrismaticMonitoring.PercentileTracker

  @sla_targets %{
    page_load: 250,
    server_render: 100,
    live_view_mount: 150,
    handle_event: 50,
    health_check: 10
  }

  @impl Mix.Task
  def run(_args) do
    Application.ensure_all_started(:prismatic_web)

    violations =
      @sla_targets
      |> Enum.filter(fn {endpoint, threshold} ->
        case PercentileTracker.get_percentiles(endpoint) do
          {:ok, %{p95: p95}} -> p95 > threshold
          _ -> false
        end
      end)

    if Enum.empty?(violations) do
      Mix.shell().info("All P95 latency targets met.")
    else
      Enum.each(violations, fn {endpoint, threshold} ->
        {:ok, %{p95: p95}} = PercentileTracker.get_percentiles(endpoint)
        Mix.shell().error("VIOLATION: #{endpoint} P95=#{p95}ms > #{threshold}ms")
      end)

      Mix.raise("Performance check failed: #{length(violations)} SLA violations")
    end
  end
end

P95 is the primary performance metric driving the platform's quality standards, providing a direct, user-centric measure of response time that protects against the misleading optimism of averages.

Cross-References

  • P99 - Stricter tail latency metric
  • IPS - Throughput metric complementing latency percentiles
  • Page Load - User-facing metric governed by P95 SLA
  • IQR - Statistical dispersion measure related to percentiles
  • Telemetry - Data collection feeding percentile computation

Created by Tomas Korcak (korczis) | GitHub