A rate-measuring counter managed with a single variable
1. Introduction: counters vs. rates
total_requests += 1.
2. Conventional implementations
2.1 Using buckets
[12, 9, 10, 13, ...] // image of the last 60 seconds of buckets
2.2 Using exponential decay
value(last): the value at the time of the last updatelast: the time of the last updatenow: the current timeduration: the time scale of the decay (time constant / effective window width)
last_updated)" and "the value at that time (value_at_last_updated)."
3. The proposed method: managing exponential decay with a single variable
3.1 Overview
pointer_time)."
pointer_time) from which the current value can be reconstructed using the current time and the decay time scale.
pointer_time is not the actual time an event occurred, but a parameter that expresses the position on the exponential-decay curve.
pointer_timeis in the future relative to now: the value is largepointer_timeis close to now: the value is close to 1pointer_timeis in the past relative to now: the value has decayed
3.2 How to count up
- Decode (reconstruct): compute the decayed value at the current time \(t\)
- Increment (add): add the increment \(x\) to the reconstructed value
- Encode (update): from the new value, back-calculate and store the
pointer_timeto be used in the next computation
pointer_time.
pointer_time that represents the new value.
pointer_time.
3.3 Why is the update time no longer needed?
last (the time of the last update) cancels out and disappears. In other words, the two pieces of information—"the time of the last update" and "the value at that time"—can be consolidated into the single variable pointer_time.
3.4 The relationship between duration and half-life
duration used in this article is defined not as a half-life but as the integral of the exponential-decay kernel—that is, the effective window width. If the weight for elapsed time \(a\) is \(w(a) = \exp(-a / \mathrm{duration})\), then the total area of the weights is duration itself, as shown below.
duration = 1 gives a value that reads naturally as a per-second rate, duration = 60 as a per-minute rate, and duration = 3600 as a per-hour rate.
duration = 60 does not have a "half-life of 60 seconds"; it is a counter with a half-life of about 41.6 seconds. Here, to make the rate readable, we use duration as the effective window width rather than the half-life.
4. Implementation: storing it as a fixed-point time
pointer_time into an integer and store it in an AtomicI64, rather than sharing it directly as a floating-point number. Because the state fits in a single variable, reading and updating can be completed with a single CAS (Compare-And-Swap).
4.1 Storage format and scale factor
pointer_time in seconds by a constant scale factor.
scale = 1_000_000, the storage unit is one microsecond. Since the maximum value of i64 is about \(9.22 \times 10^{18}\), even with this scale factor the representable time span is about 290,000 years. For the use case of storing seconds elapsed from the origin of a monotonic clock, overflow is usually not a problem.
pointer_time moves per addition is roughly \(\tau / v\). In steady state, \(v \simeq \mathrm{rate} \times \tau\), so the change in the integer value is roughly as follows.
scale = 10_000 is practical up to a few thousand events per second per counter, but for use cases exceeding 10,000 events per second the effect of quantization begins to show. For a general-purpose implementation, using scale = 1_000_000 as the default gives a good balance between resolution and overflow headroom.
4.2 Management with an atomic variable
AtomicI64. The duration and the scale factor are configuration values of the counter, not state that is atomically rewritten on every update.
use std::sync::atomic::{AtomicI64, Ordering};
const SCALE: f64 = 1_000_000.0;
pub struct RateCounter {
pointer_time: AtomicI64,
duration_secs: f64,
}
impl RateCounter {
pub fn new(duration_secs: f64) -> Self {
assert!(duration_secs.is_finite() && duration_secs > 0.0);
Self {
pointer_time: AtomicI64::new(i64::MIN),
duration_secs,
}
}
}
i64::MIN. With scale = 1_000_000, this corresponds to a pointer_time about 290,000 years in the past, so decoding it to the current value yields essentially 0. The advantage is that you do not need a separate branch for the empty state; you can handle it with the ordinary decay computation alone.
4.3 Decode, increment, and re-encode
pointer_time in seconds and reconstruct the value from its difference with the current time. Since the initial value i64::MIN is simply treated as a very old time, no special branch is needed.
fn decode_value(raw: i64, now_secs: f64, duration_secs: f64) -> f64 {
let pointer_time = raw as f64 / SCALE;
((pointer_time - now_secs) / duration_secs).exp()
}
fn encode_time(pointer_time: f64) -> i64 {
(pointer_time * SCALE)
.round()
.clamp(i64::MIN as f64, i64::MAX as f64) as i64
}
pointer_time. If another thread updated it first, the CAS fails, so you redo the same computation using the new value.
impl RateCounter {
pub fn value_at(&self, now_secs: f64) -> f64 {
let raw = self.pointer_time.load(Ordering::Relaxed);
decode_value(raw, now_secs, self.duration_secs)
}
pub fn increment_at(&self, now_secs: f64, x: f64) {
assert!(now_secs.is_finite());
assert!(x.is_finite() && x > 0.0);
let mut raw = self.pointer_time.load(Ordering::Relaxed);
loop {
let old_value = decode_value(raw, now_secs, self.duration_secs);
let new_value = old_value + x;
let new_pointer_time = now_secs + self.duration_secs * new_value.ln();
let new_raw = encode_time(new_pointer_time);
match self.pointer_time.compare_exchange_weak(
raw,
new_raw,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return,
Err(actual) => raw = actual,
}
}
}
}
Ordering::Relaxed is usually sufficient. Only when you want the counter value to express a happens-before relationship with the event-processing body itself should you consider a stronger ordering.
duration_secs is a positive finite value and that now_secs and the increment x are also finite. As long as these conditions hold, the initial value i64::MIN is naturally treated as a very old pointer_time, and it returns to an ordinary value on the first addition.
4.4 Implementation notes
Instant taken at process start.
use std::time::Instant; let origin = Instant::now(); let now_secs = origin.elapsed().as_secs_f64();
exp at decode time may underflow to 0. In this case the value can be regarded as having decayed to essentially 0, so for most use cases it is not a problem. The initial value i64::MIN also takes advantage of this property.
pointer_time * 1_000_000 in an AtomicI64, initialize it to i64::MIN, and decode, add, and re-encode in a CAS loop.
5. Mathematical background
pointer_time. Moreover, the increment operation is equivalent to a log-sum-exp computation.
6. Summary
pointer_time).
- The current value can be reconstructed with \(\exp((\mathrm{pointer\_time} - \mathrm{now}) / \mathrm{duration})\)
- Addition is completed in three steps: decode the stored value, add, and re-encode
- It can be managed with an atomic variable such as
AtomicI64
7. References
- Cohen, E., & Strauss, M. J. (2006). "Maintaining Time-Decaying Stream Aggregates". Journal of Algorithms, 59, 19-36. A foundational reference for treating time-decaying stream aggregates in general.
- Cormode, G., Korn, F., & Tirthapura, S. (2008). "Exponentially Decayed Aggregates on Data Streams". ICDE 2008. A reference that organizes techniques for handling exponentially decayed aggregates over data streams, close to the mathematical background of this article.
- Cormode, G., Shkapenyuk, V., Srivastava, D., & Xu, B. (2009). "Forward Decay: A Practical Time Decay Model for Streaming Systems". ICDE 2009. It introduces the idea of expressing decay forward from a fixed point in time, useful as background for an easy-to-implement time-decay model.
- Datar, M., Gionis, A., Indyk, P., & Motwani, R. (2002). "Maintaining Stream Statistics over Sliding Windows". SIAM Journal on Computing. A classic reference on sliding-window stream statistics, relevant to the comparison with bucket- and window-width-based methods.
- NIST/SEMATECH Engineering Statistics Handbook, "EWMA Control Charts". It explains the basic idea of EWMA and the practical background of a monitoring technique that gives exponentially decreasing weight to past observations.
- Dropwizard Metrics documentation, "Meters". An example of rate measurement in a practical metrics library, useful as a reference for a design that handles 1-, 5-, and 15-minute moving averages in addition to the mean rate.
- SciPy documentation,
scipy.special.logsumexp. A standard function for numerically stable computation of log-sum-exp, helpful for understanding the addition in the logarithmic domain in this article.
Last Modified: June 19, 2026