go 162 lines · 3 tabs

Time-Bucketed Metric Aggregation With a Concurrent Ring of Windows in Go

Shared by codesnips Aug 2026
3 tabs
package metrics

import "time"

type bucket struct {
	start time.Time
	count int64
	sum   float64
	min   float64
	max   float64
}

type Window struct {
	Start time.Time
	Count int64
	Sum   float64
	Min   float64
	Max   float64
}

func (b *bucket) reset(start time.Time) {
	b.start = start
	b.count = 0
	b.sum = 0
	b.min = 0
	b.max = 0
}

func (b *bucket) observe(v float64) {
	if b.count == 0 || v < b.min {
		b.min = v
	}
	if b.count == 0 || v > b.max {
		b.max = v
	}
	b.count++
	b.sum += v
}

func (b *bucket) snapshot() Window {
	return Window{
		Start: b.start,
		Count: b.count,
		Sum:   b.sum,
		Min:   b.min,
		Max:   b.max,
	}
}

func (w Window) Avg() float64 {
	if w.Count == 0 {
		return 0
	}
	return w.Sum / float64(w.Count)
}
3 files · go Explain with highlit

This snippet shows how streaming metric samples are aggregated into fixed-width time buckets (tumbling windows) so that a high-throughput ingest path stays cheap while readers get coarse per-window summaries. The core idea is to avoid storing every raw sample: instead each sample is folded into a running bucket keyed by the truncated timestamp, and only the compact aggregate (count, sum, min, max) survives.

In bucket.go, a bucket holds the aggregate state for one window plus the start time it covers. observe updates the running totals in place, and snapshot freezes the current state into a Window value that is safe to hand to callers. Keeping bucket mutable and Window immutable draws a clear line between internal accumulation and external reads.

In aggregator.go, Aggregator owns a ring of buckets of length size, which caps memory regardless of how long the process runs — old windows are overwritten as time advances. bucketStart truncates a timestamp down to the window boundary using integer division, giving every sample in the same interval an identical key. Observe computes that boundary, finds the ring slot with a modulo of the window index, and lazily resets the slot when it belongs to an older window (b.start != start). A single sync.Mutex guards the ring; the critical section is tiny — just a few field updates — so contention stays low even under heavy fan-in from many goroutines.

Windows walks the ring and returns a snapshot slice of only the live, non-empty buckets, sorted by start time, so a scrape endpoint sees a stable view. Because the ring is fixed, a slow reader can never cause unbounded growth; the trade-off is that windows older than size intervals are silently dropped.

In main.go, several goroutines simulate concurrent producers calling Observe, while the main goroutine periodically calls Windows and prints each window's average. This mirrors a real collector where request-latency samples arrive continuously and a metrics handler exposes rolled-up windows. The pattern suits rate, latency, and throughput dashboards where per-sample fidelity is unnecessary and bounded memory matters more than completeness.


Related snips

Share this code

Here's the card — post it anywhere.

Time-Bucketed Metric Aggregation With a Concurrent Ring of Windows in Go — share card
Link copied