Observability

A counter reset can invent a traffic spike

Handle counter resets before aggregating independent series. Our PromQL fixture reports 15 requests per minute from the original replicas, but a raw-sum recording rule turns the same inputs into 115.

Executed engine
Promtool 3.15.1 on Darwin/arm64
Regression scope
5 two-replica cases; 10 verified vector values
Counterexample
15 per minute becomes a false 115
Missing-data check
One usable sample yields no rate result
Two paper tally wheels send separate streams of geometric tokens into a tray; one wheel shows a reset motif.
Conceptual illustration of preserving separate counter histories before combining their contributions.

The same scrapes report 15 or 115 requests per minute

Replica A moves from 100 to 110. Replica B moves from 50 to 5 after a reset. A query that handles each counter before combining them reports 15 requests per minute in our fixture. A query that first records their raw sum reports 115. No extra requests were added to the input.

This is a synthetic two-replica trace, not a reported production outage. We reproduced it with Prometheus 3.15.1's promtool, using the real PromQL engine. The symptom comes from the order of operations: adding independent counters can destroy the history that reset detection needs.

The investigation starts with the samples and ends with a regression test. It also includes controls where the two expressions agree. A graph that looks reasonable through one restart is insufficient evidence that its aggregation is safe.

Follow the values through the recording rule

At minute 1, the raw sum is 150. At minute 2, it is 115. That combined series falls even though A continued counting normally. A counter function applied afterward sees only the fall in the aggregate. It cannot tell which part belonged to the continuing replica.

Process the replicas separately instead. A contributes an increase of 10 between the relevant samples. B's observed fall is handled as a reset, leaving 5 after the reset. In this deliberately aligned fixture, the resulting rates add to 15 per minute. The failed ordering treats the combined history as one counter and produces the spurious 115.

Prometheus's function documentation describes reset adjustment and explicitly puts rate() before aggregation. The recommendation protects information, rather than merely satisfying a preferred query style.

Per-replica rate calculation preserves A and B reset histories and totals 15 per minute; taking a rate of their raw sum reports 115.
Executed visible-drop fixture. The lower path discards the boundary between independently resettable counters. View full-size figure.

This is also why dropping an instance label is not automatically a cardinality optimization. The Prometheus data model identifies a time series by its metric name and label set. Independently resettable emitters need distinguishable input histories until the counter operation has consumed those histories. The cardinality article covers identity counts. It does not authorize merging raw counters.

Reproduce five cases in the actual query engine

The downloadable Python runner writes the negative-control recording rule and test fixture, invokes an installed promtool and writes verified results. Save the files in an empty writable directory. The runner uses Python's standard library and makes no network requests.

Our run used the official Prometheus 3.15.1 release for Darwin/arm64. Its downloaded package checksum matched the release checksum before execution. Use the appropriate official package for your platform. The published evidence describes the platform actually tested.

Illustrative procedure
python3 experiment.py --promtool /path/to/promtool

To run the two YAML files directly, keep them in the same directory and run promtool test rules counter.test.yml. The rule-testing documentation defines the input-series and expression-test format. These tests run locally without starting a monitoring server.

The first test group supplies samples at seconds 0, 60 and 120. Each series repeats its starting value at the first two points, then receives the final value. Evaluation occurs at second 120 with a two-minute range. The range selector excludes its left boundary, so the relevant raw samples are those at 60 and 120. A separate group provides just one usable sample and expects an empty rate vector.

The two tested expressions are:

Illustrative procedure
60 * sum by (scenario) (rate(requests_total[2m]))
60 * rate(service:requests:raw_sum[2m])

scenario is a synthetic label separating the five datasets. The second expression reads a recording rule that sums the raw replicas by scenario every minute. It is an intentional negative control, not a configuration recommendation. Multiplying by 60 converts the per-second rate into a per-minute display unit.

A visible fall is only one way to lose the reset

Promtool-verified query results for five synthetic counter histories; rate values shown per minute
Synthetic caseA at seconds 60 and 120B at seconds 60 and 120Rate then sum, per minuteRaw sum then rate, per minute
No reset100 to 11050 to 602020
Visible aggregate fall100 to 11050 to 515115
Reset hidden by A's growth100 to 20050 to 510555
Unchanged aggregate100 to 14550 to 5500
Both replicas reset100 to 250 to 355
Five verified cases compare rate before sum with raw sum before rate: 20 and 20, 15 and 115, 105 and 55, 50 and 0, 5 and 5.
Expected vectors confirmed by Promtool 3.15.1. Values are rounded for display; these are synthetic query results, not production traffic measurements. View full-size figure.

In the third case, A's growth more than offsets B's observed fall. The total rises from 150 to 205. Applying the counter function to that total misses B's reset contribution and reports 55 rather than 105. The defect can undercount as well as create a spike.

The fourth case is quieter still: both aggregate samples equal 150. The flawed path reports zero, while the per-series path reports 50. An unchanged raw total does not establish that no work occurred.

Both expressions have the same displayed result in the no-reset and simultaneous-reset controls. Agreement there does not rescue the other cases. The engine verified ten expected vector values across the two expressions, plus one empty-vector expectation for the single-sample group. Table and chart values are rounded for display; the full floating-point expectations remain in results.json. One confirmed value is 5.000000000000001, rather than the decimal display value 5. No numeric tolerance was widened to make that comparison pass.

Distinguish an estimate from an event ledger

rate() estimates a per-second rate over a range and extrapolates toward its boundaries. Its result is not generally the raw endpoint difference divided by the requested duration. These sample times were chosen to make the counterexample inspectable, not to define an implementation of PromQL in Python.

A counter can also reset and grow past its previous value between scrapes. The two observed values then need not reveal the reset. Applying rate() first preserves the available per-series evidence. It cannot recreate observations that were never collected. Additional restart context may help an investigation, but it does not turn the samples into a complete event history.

Do not replace a missing series with zero merely to make the panel look continuous. The single-sample test returns no rate result. It does not certify zero traffic. Missing scrapes, disappeared targets and a changed label set need their own checks before absence becomes a service-level conclusion.

The fixture covers ordinary float counters. Native histograms, out-of-order samples, duplicated collection and high-availability deduplication were not tested. Its results are query-correctness evidence for the supplied samples, not a production traffic estimate or a benchmark of Prometheus.

Trace the first aggregation boundary before changing the dashboard

Inspect the expression behind the panel and every recording rule it references. A correctly written final query cannot recover independent histories from an upstream rule that already stored only the raw sum. Locate the first point where emitters become indistinguishable.

A useful diagnostic sequence is to inspect each original series around the restart, compare the recorded aggregate at those same timestamps and evaluate the corrected expression against the original inputs. Keep the evaluation time and range identical. Changing the time window while changing the aggregation makes the result harder to explain.

Prometheus recording rules evaluate expressions into new time series. When replacing a raw-counter aggregate with an aggregated rate, use a name that makes the new meaning and units explicit. Do not mix the two meanings under one metric name and then read across the change as though the history were uniform.

Check downstream consumers before switching: a panel expecting requests per second, an alert with a threshold and a derived ratio can all depend on the old rule. A rolling change needs to identify which consumers still use the earlier series. The SLO burn-rate walkthrough addresses ratio populations and windows. Corrected counter handling is an input requirement for that later calculation.

Close the investigation with a failure the rule must explain

Retain the visible-fall and unchanged-sum cases in a local regression fixture. Include the no-reset control so a fix does not merely special-case restarts. Verify both the expected labels and values, since an apparently correct total can still combine the wrong population.

Use fresh inputs representing your own sampling cadence, restarts and missing-data behavior before changing a live rule. This lab evaluates a recording rule at a fixed one-minute interval; it does not reproduce scheduler delays, scrape jitter or a production deployment. Keep those integration checks separate from the deterministic fixture.

The Prometheus practice guide gives the operational rule: preserve counter semantics before aggregation. For a dashboard investigation, the concrete next step is to recover the original per-instance samples, replay the failing timestamps and identify the earliest rule that discarded the reset boundary.

Sources

Documentation checked .

  1. PromQL counter functions
  2. Prometheus rule unit tests
  3. Prometheus data model
  4. Prometheus recording rules
  5. The Zen of Prometheus
  6. Prometheus3.15.1 release
  7. PromQL range selector boundaries

Continue the conversation

Comments (0)

    Leave a comment

    Your name and comment stay in this page and are cleared after the spam check.

    10–2,000 characters. Keep the discussion relevant to this article.

    Spam protection verification
    Spam protection loads when you begin the form.

    JavaScript is required to use this form and its spam protection.