Skip to content
About
Main content

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 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

With 30% of scrapes lost at a 60 s interval, in what share of hours is the gauge's spike seen at all?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    counter-vs-gauge-under-lost-scrapes.py45 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat it changes
--loss0.3 Share of scrapes dropped.
--intervals15, 60 Scrape intervals (s).
--seeds200 Simulated hours (seeds).

Our run

200 simulated hours per interval

200 simulated hours per interval
Scrape interval (s)Counter rate, minCounter rate, maxHours the spike was seenSpikes seen
1510010034.5%24.1%
6010010010%6.99%
Reference runApple M2 · 8 cores · 8 GB
Output · uv run counter-vs-gauge-under-lost-scrapes.py
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 hour
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What 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 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.