Counter vs. gauge under lost scrapes
Simulate a modelNeeds Python
Problem
A counter only ever increases, so the rate between any two surviving samples is exact: a lost scrape loses resolution, not information. A gauge reports the value at the moment of the scrape, so anything that happens between scrapes is invisible, and a lost scrape is a lost observation. Short spikes are what gauges miss.
Simulate a process whose counter increases at 100 events per second, and a queue-length gauge that spikes to 10,000 for 5 seconds once per minute. "Scrape" both every 15 s, then every 60 s, and drop 30% of the scrapes at random. The script repeats each setting over 200 simulated hours, with the scrape schedule at a random offset to the spikes.
The question. Does the rate computed from the counter survive lost scrapes, and how often is the gauge's spike seen?
Predict
Expect the counter rate to stay at 100/s even with lost scrapes. The gauge maximum is often 50 (the spike is never seen), especially at 60 s intervals.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
counter-vs-gauge-under-lost-scrapes.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Counter against gauge under lost scrapes. A counter rises at 100 events per second; a queue-length gauge spikes to 10,000 for 5 seconds once a minute. Both are scraped every 15 s and every 60 s for an hour, with 30% of scrapes dropped at random, over many random seeds: the rate computed from the counter, and whether the gauge's spike was ever seen. """ import argparse, json, random, statistics p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--loss", type=float, default=0.3, help="Share of scrapes dropped.") p.add_argument("--intervals", default="15,60", help="Scrape intervals (s).") p.add_argument("--seeds", type=int, default=200, help="Simulated hours (seeds).") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() def rate_from_counter(samples): # samples: list of (t, value) that survived (t0, v0), (t1, v1) = samples[0], samples[-1] return (v1 - v0) / (t1 - t0) counter = lambda t: 100 * t # accumulator gauge = lambda t: 10_000 if (t % 60) < 5 else 50 # spike for 5 s each minute rows = [] for interval in map(int, args.intervals.split(",")): rates, seen, seen_share = [], 0, [] for seed in range(args.seeds): random.seed(seed); offset = random.randrange(interval) # scrapes are not aligned with the spike ts = [t for t in range(offset, 3600, interval) if random.random() > args.loss] rates.append(rate_from_counter([(t, counter(t)) for t in ts])) hits = sum(gauge(t) > 50 for t in ts); seen += hits > 0; seen_share.append(hits / 60) rows.append({"interval_s": interval, "rate_mean": statistics.mean(rates), "rate_min": min(rates), "rate_max": max(rates), "hours_spike_seen_pct": seen / args.seeds * 100, "spikes_seen_pct": statistics.mean(seen_share) * 100}) for r in rows: print(f"every {r['interval_s']:>2} s, {args.loss:.0%} lost: counter rate {r['rate_min']:.1f}-{r['rate_max']:.1f}/s; " f"gauge spike seen in {r['hours_spike_seen_pct']:.0f}% of hours, {r['spikes_seen_pct']:.1f}% of the 60 spikes per hour") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"intervals": rows}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run counter-vs-gauge-under-lost-scrapes.py uv run counter-vs-gauge-under-lost-scrapes.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --loss0.3 | Share of scrapes dropped. |
| --intervals15, 60 | Scrape intervals (s). |
| --seeds200 | Simulated hours (seeds). |
Our run
200 simulated hours per interval
| Scrape interval (s) | Counter rate, min | Counter rate, max | Hours the spike was seen | Spikes seen |
|---|---|---|---|---|
| 15 | 100 | 100 | 34.5% | 24.1% |
| 60 | 100 | 100 | 10% | 6.99% |
every 15 s, 30% lost: counter rate 100.0-100.0/s; gauge spike seen in 34% of hours, 24.1% of the 60 spikes per hour
every 60 s, 30% lost: counter rate 100.0-100.0/s; gauge spike seen in 10% of hours, 7.0% of the 60 spikes per hourWhat we found
The counter's rate came out at exactly 100 per second in every hour, at both intervals. The gauge's spike was seen in 34.5% of hours at a 15 s interval and 10% at 60 s; of the sixty spikes in an hour, 24.1% and 6.99% were observed. With a regular schedule, whether the spike is ever seen depends mostly on the phase between scrapes and spikes, not on luck.
Read it
Instrument anything you might need to sum or rate as a counter, and for bursty quantities keep a counter or histogram of what happened between scrapes (a maximum since last scrape, a count of time above a threshold), not only the current value. A gauge answers "what is it now", which is rarely the question an incident asks.