Practical infra coding

p50 and p95 latency per endpoint

mediumLog parsing and aggregation Must-do

Problem statement

A service logs each request in logfmt: space-separated key=value pairs. For each path, report the request count, the median (p50) and the 95th percentile (p95) of dur_ms. Put the endpoint with the worst p95 first; break ties by path.

Use the nearest-rank definition. Sort the n values; the p-th percentile is the value at 1-based position ceil(p / 100 * n). Lines with no dur_ms, or a value that is not a finite non-negative number, are skipped and counted.

requests.log

TEXT
ts=2026-09-20T10:00:01Z method=GET path=/api/users status=200 dur_ms=120
ts=2026-09-20T10:00:01Z method=GET path=/api/orders status=200 dur_ms=340
ts=2026-09-20T10:00:02Z method=POST path=/api/login status=200 dur_ms=95
ts=2026-09-20T10:00:02Z method=GET path=/api/users status=200 dur_ms=80
ts=2026-09-20T10:00:03Z method=GET path=/api/orders status=500 dur_ms=1020
ts=2026-09-20T10:00:03Z method=GET path=/healthz status=200
ts=2026-09-20T10:00:04Z method=POST path=/api/login status=401 dur_ms=45
ts=2026-09-20T10:00:04Z method=GET path=/api/users status=200 dur_ms=100
ts=2026-09-20T10:00:05Z method=GET path=/api/orders status=200 dur_ms=310
ts=2026-09-20T10:00:05Z method=GET path=/api/users status=200 dur_ms=abc
ts=2026-09-20T10:00:06Z method=GET path=/api/users status=200 dur_ms=95
ts=2026-09-20T10:00:06Z method=POST path=/api/login status=200 dur_ms=60
ts=2026-09-20T10:00:07Z method=GET path=/api/orders status=200 dur_ms=290
ts=2026-09-20T10:00:07Z method=GET path=/api/users status=200 dur_ms=110
ts=2026-09-20T10:00:08Z method=GET path=/api/users status=200 dur_ms=400

Examples

Example 1

Input: python solution.py requests.log

Output: /api/orders count=4 p50=310 p95=1020 /api/users count=6 p50=100 p95=400 /api/login count=3 p50=60 p95=95 skipped 2 line(s) without a usable dur_ms

Explanation: /api/users has 6 valid samples: 80, 95, 100, 110, 120, 400. p50 is position ceil(3) = 3, which is 100. p95 is position ceil(5.7) = 6, which is 400. The /healthz line has no dur_ms and one /api/users line has dur_ms=abc.

Hints

Approach

Optimal

A percentile needs the values in order, so keep every sample per endpoint and sort once.

  1. Parse each line into a dict. Look up dur_ms and path; a KeyError or ValueError means the line is skipped.
  2. Reject values that are not finite or are negative, then append them to samples[path].
  3. For each endpoint, sort its list and read off the nearest-rank p50 and p95 with ceil(p / 100 * n) - 1 as a 0-based index.
  4. Sort the rows by (-p95, path).

Sorting dominates: O(n log n) across all endpoints. Memory is O(n) because every sample is kept, which is fine for a sample file and is exactly what the follow-up asks you to fix.

ComplexityTime O(n log n)Space O(n)
Python
import math
import sys
from collections import defaultdict
def parse_logfmt(line):
"""key=value pairs separated by spaces. Tokens without '=' are ignored."""
return dict(tok.split("=", 1) for tok in line.split() if "=" in tok)
def nearest_rank(sorted_values, pct):
"""The smallest value with at least pct% of samples at or below it."""
k = math.ceil(pct / 100 * len(sorted_values))
return sorted_values[max(k, 1) - 1]
def latency_report(lines):
samples = defaultdict(list)
skipped = 0
for line in lines:
fields = parse_logfmt(line)
try:
ms = float(fields["dur_ms"])
path = fields["path"]
except (KeyError, ValueError):
skipped += 1
continue
if not math.isfinite(ms) or ms < 0: # float() also accepts "nan" and "inf"
skipped += 1
continue
samples[path].append(ms)
rows = []
for path, values in samples.items():
values.sort()
rows.append((path, len(values), nearest_rank(values, 50), nearest_rank(values, 95)))
rows.sort(key=lambda r: (-r[3], r[0])) # slowest p95 first
return rows, skipped
def main(path):
with open(path, encoding="utf-8", errors="replace") as f:
rows, skipped = latency_report(f)
for p, n, p50, p95 in rows:
print(f"{p} count={n} p50={p50:g} p95={p95:g}")
print(f"skipped {skipped} line(s) without a usable dur_ms")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "requests.log")

Follow-up questions

  • The log has a billion lines and you cannot keep every sample. How do you estimate p95? (Fixed latency buckets like a Prometheus histogram, or a sketch such as t-digest.)
  • Report p95 per endpoint per 5-minute window.
  • Can you merge two p95 values from two hosts into one global p95? (No; you need the underlying samples or mergeable histograms.)

Frequently asked questions

statistics.quantiles interpolates between neighbouring samples by default, so it can return a value that never occurred. Nearest-rank always returns a real observed latency, which is what most people mean by "p95 was 400 ms". Both are valid; say which one you use, because with small samples they give different numbers.

Latency distributions have long tails. One 1,020 ms request among fast ones barely moves the mean but is exactly what users notice. SLOs are almost always written against percentiles, which is why SRE interviews ask for p95 or p99 rather than averages.

The simple split breaks quoted values into separate tokens. Tokens without = are ignored, so dur_ms and path still parse, but a quoted value itself would be truncated. shlex.split(line) understands quotes, at some cost in speed.