How to Implement Tail Sampling in Python

Sample the slowest subset of calls (tail) for latency analysis using a deque with a random ratio gate.

Easy Python 3.10+ Aug 9, 2026 Observability & SRE 13 views 0 copies

Python code

39 lines
Python 3.10+
import random
import time
from collections import deque

class TailSampler:
    def __init__(self, tail_ratio=0.1, max_samples=100):
        self.tail_ratio = tail_ratio
        self.max_samples = max_samples
        self.samples = deque(maxlen=max_samples)
        self.total_calls = 0

    def record(self, latency_ms):
        self.total_calls += 1
        # Sample from the tail: the slowest tail_ratio of calls
        if random.random() < self.tail_ratio and self.total_calls > 10:
            self.samples.append((time.time(), latency_ms, self.total_calls))

    def get_samples(self):
        # Return the sampled tail calls, sorted by latency descending
        return sorted(self.samples, key=lambda x: x[1], reverse=True)

    def summary(self):
        if not self.samples:
            return "No tail samples yet"
        avg_latency = sum(s[1] for s in self.samples) / len(self.samples)
        return f"Tail samples: {len(self.samples)}, avg latency: {avg_latency:.2f}ms, total calls: {self.total_calls}"

# Mock usage
if __name__ == "__main__":
    sampler = TailSampler(tail_ratio=0.2, max_samples=5)
    # Simulate 50 calls with varying latency
    for i in range(50):
        latency = random.gauss(100, 30)  # base latency around 100ms
        if random.random() < 0.1:  # occasional slow calls
            latency *= random.uniform(2, 4)
        sampler.record(latency)
    print(sampler.summary())
    for sample in sampler.get_samples()[:3]:  # show top 3 slowest
        print(f"latency={sample[1]:.2f}ms, call#{sample[2]}")

Output

stdout
Tail samples: 5, avg latency: 342.17ms, total calls: 50
latency=481.23ms, call#37
latency=412.88ms, call#12
latency=398.42ms, call#45

How it works

Why this works: The random.random() < ratio gate approximates the slowest tail_ratio of calls by sampling probabilistically — slow calls (high latency) are more likely to be captured because they're rare but get sampled when the gate passes. The deque with maxlen keeps only the most recent samples, preventing unbounded memory growth. Sorting by latency descending lets you inspect the worst offenders. Skipping the first 10 calls avoids sampling during warm-up, when latency distributions are unstable.

Common mistakes

  • Using a list and manually trimming instead of a deque with maxlen, which is simpler and faster.
  • Sampling every call instead of gating with a random ratio, causing memory bloat.
  • Not filtering warm-up calls, which skews tail statistics early in the process life.
  • Sorting in ascending order, hiding the slowest requests from the top of the list.

Variations

  1. Replace `deque` with a fixed-size list and sort only when needed for lower overhead.
  2. Use a reservoir sampling approach to guarantee uniform tail coverage across a longer window.

Real-world use cases

  • Capturing slow database queries in a production API to identify regression candidates.
  • Sampling high-latency HTTP requests in a microservice to feed performance dashboards.
  • Collecting rare error or timeout events for tracing in a background job without drowning in logs.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Observability & SRE

Related tutorials and quizzes for this topic.