Histogram
Suppose you've promised that requests finish within 10 ms, 50 ms, or at worst 100 ms — and you want to know how many landed in each band. A histogram answers "how are my measurements distributed ?" when you already know the ranges you care about: you pick the boundaries up front, and each recorded value lands in exactly one bucket.
Histogram makes those bucket counts mergeable: every device counts its own measurements, any two histograms merge into one, and the merged histogram is exactly what one machine would have counted had it seen everything. No central aggregator, no double-counting when a message is delivered twice.
Converges to: the per-bucket counts (and running sum) of every value recorded on every replica — exactly, not approximately.
Merging loses nothing
Each bucket's count is a GCounter (every replica owns its own slot, merged by maximum), and merging two histograms merges the counts bucket by bucket. That makes the merge idempotent, commutative, and associative — a true CRDT, robust to kuilt's drop/duplicate/reorder delivery — and lossless: the merged histogram equals the histogram of the combined stream, in any merge order, merged any number of times.
The boundaries are a cluster-wide constant: two histograms merge only if their boundaries match exactly — fix them once per deployment, like HyperLogLog's precision and DDSketch's accuracy. N boundaries define N + 1 buckets with upper-inclusive edges: the first bucket is everything up to and including the first boundary, and anything past the last boundary falls into a final catch-all bucket.
Code examples
Count request latencies against SLA thresholds:
Two servers merge their histograms losslessly:
Details worth knowing
sumis mergeable too — carried as a pair of grow-only double counters (positive and negative contributions), so the mean (sum / count) survives any merge order.min/maxare deliberately not carried: they aren't products of grow-only counters, and would need their own min-/max-register lattices.Deltas are tiny.
record()returns a patch touching a single bucket cell (plus a sum cell) — the same minimal sparse fragment idiom as the zoo's sketches. Re-delivered patches never inflate counts, because each cell is a per-replicaGCounterslot.OTel interop. The state is structurally an OpenTelemetry
HistogramDataPoint— explicitboundsplusbucket_counts,count, andsum. The OTLP mapping itself lives with the metrics exporter.
When to prefer something else
You can't guess the range up front (latency spanning orders of magnitude) — use
DDSketch, whose logarithmic buckets auto-cover whatever appears at uniform relative precision. Guess your explicit boundaries wrong and the interesting tail lands in one giant low-resolution bucket.You need exact totals, not a distribution — use
GCounter/PNCounter.You need the current level, not a distribution — use
Gauge.