Building a Tiny Time-Series Cache in Rust

June 10, 2026 · 0 views

rustperformancesystems

I needed to keep the last hour of metrics in memory for a dashboard — a few thousand points per series, appended constantly, queried occasionally. The obvious answer is a VecDeque. The fun answer is building it yourself and measuring why the obvious answer is fine.

The shape of the problem

Time-series appends have a beautiful property: they're monotonic. New data always goes at the end, old data expires from the front. That's a ring buffer:

pub struct Ring<T> {
    buf: Box<[Option<T>]>,
    head: usize,
    len: usize,
}

impl<T> Ring<T> {
    pub fn push(&mut self, value: T) {
        let idx = (self.head + self.len) % self.buf.len();
        self.buf[idx] = Some(value);
        if self.len == self.buf.len() {
            self.head = (self.head + 1) % self.buf.len(); // overwrite oldest
        } else {
            self.len += 1;
        }
    }
}

The measurement that humbled me

I benchmarked my ring against VecDeque expecting to lose. I won — by 4%. Four percent, for a weekend of work. Then I tried splitting timestamps and values into two parallel arrays and won by 38%:

pub struct Series {
    times: Ring<u64>,
    values: Ring<f64>,
}

Structure-of-arrays beat my clever data structure by an order of magnitude more than my clever data structure beat the standard library. Range queries scan only timestamps until they find the window — half the memory traffic, twice the cache hits.

What I actually learned

  • The standard library is fast. Beat it with layout, not logic.
  • criterion benchmarks with confidence intervals are the only benchmarks worth reading.
  • The prefetcher rewards predictable access like a dog rewards a full pocket.

Would I ship the custom ring? No. I shipped VecDeque with split arrays. But I wouldn't trade the weekend — you don't really own knowledge you haven't measured.

Comments

No comments yet — though the observant have been known to find a way.