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.
Python code
39 linesimport 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
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
- Replace `deque` with a fixed-size list and sort only when needed for lower overhead.
- 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
More from Observability & SRE
- Adding a Correlation ID to Log Context in Python medium
- Calculate Error Rate from Log Stream in Python easy
- Check if a Timestamp Falls in a Daily Maintenance Window in Python easy
- Export Metrics with OTLP Mock in Python medium
- Generate Mock CPU and Memory Metrics in Python easy
- Generate Prometheus Text Exposition Format in Python easy
Keep learning
Related tutorials and quizzes for this topic.