Instrumenting an App: Counters, Gauges, Histograms & Summaries¶
What was run¶
The real orders_app.py from Lesson 1, using all four core Prometheus client
types: Counter, Gauge, Histogram, and Summary. The app was restarted for real to observe a counter's real
behavior across a restart, and a real concurrent burst of 15 requests was fired to probe a gauge's real blind
spot against Prometheus's actual 5-second scrape interval.
Counters: real behavior across a real restart¶
REQUEST_COUNT = Counter("orders_requests_total", "Total requests", ["endpoint", "status"])
REQUEST_COUNT.labels(endpoint="/orders", status="200").inc()
# right after a real restart, the metric doesn't even exist yet:
curl -sL localhost:9301/metrics | grep "^orders_requests_total"
# (no output)
# after 3 real requests:
curl -sL localhost:9301/metrics | grep "^orders_requests_total"
# orders_requests_total{endpoint="/orders",status="200"} 3.0
A real, restarted process's counter does not resume from whatever value it held before — it starts over, because
prometheus_client's Counter object is held only in that process's memory, and a Counter metric doesn't even
appear in /metrics output until it's been incremented at least once in the current process's lifetime. This is
precisely the behavior PromQL's rate() and increase() functions are built to handle correctly (covered with
its own real demonstration in Lesson 6) — they detect a value that dropped
between two samples and treat it as a reset rather than a negative rate, which is why counters are safe to use
across restarts even though their raw value isn't.
Gauges: a real, invisible blind spot¶
# a real burst of 15 concurrent requests, completed in 0.855s:
# Prometheus's own scraped history for the same time window:
curl -s ".../api/v1/query_range?query=orders_inflight_requests&..."
# 0, 0, 0, 0, 0, 0, 0
Every single scraped data point is 0, despite a real, concurrent spike where INFLIGHT genuinely held a
value as high as 7 (confirmed separately with a direct, manual scrape taken during the burst, bypassing
Prometheus's own 5-second schedule). Prometheus's periodic scrape simply never happened to land during the
roughly one-second window the spike was real — the gauge's value was correct the entire time, but Prometheus's
sampling of it was too infrequent to catch it. A gauge only tells you the value Prometheus last observed; it
makes no promise about peaks that happened entirely between two scrapes.
How It Actually Works¶
Each Prometheus metric type encodes a real, specific promise about how its value behaves, and picking the wrong type for what you're measuring produces real, observable problems:
- Counter: a value that only ever increases (or resets to zero on restart). Correct for "how many times has X
happened" — request counts, error counts, bytes sent. Never use it for something that can legitimately decrease
(queue depth, active connections) —
prometheus_client'sCounterdoesn't even expose a way to decrement it, by design. - Gauge: a value that can go up or down, representing a snapshot at the moment of each scrape — exactly what this lesson's blind spot demonstrates the real limit of. Correct for "what is X right now" — memory usage, queue depth, in-flight requests — but only ever as accurate as your scrape interval allows; a gauge cannot tell you about anything that happened and fully resolved between two scrapes.
- Histogram: buckets observations into predefined ranges (the
le="0.05",le="0.1"buckets seen in Lesson 1's raw output) and exposes_bucket,_sum, and_countseries. Aggregatable across many instances — covered in depth, with a real quantile-math demonstration, in Lesson 7. - Summary: computes quantiles (like p99) client-side, inside the application process, before they're ever
scraped. This real
orders_request_duration_summary_secondsmetric looks similar to the histogram above, but its quantiles cannot be meaningfully aggregated across multiple instances — each instance's p99 is already computed from only that instance's own requests, and averaging several instances' p99s doesn't produce a correct overall p99 (the same mathematical problem Lesson 7 demonstrates for naively averaged histograms). This is the real, practical reason histograms are the more common choice for latency in multi-instance systems, despite needing a PromQL function (histogram_quantile()) to extract a quantile after the fact rather than getting one directly.
Common mistakes¶
- Using a
Gaugeto count something that only ever increases, losing the restart-safe, rate()-friendly behavior aCounterprovides for free. - Trusting a
Gauge's scraped history as a complete record of a value's real behavior over time — as shown above, it is a sampled snapshot, not a continuous recording, and real spikes shorter than your scrape interval can be completely invisible to it. - Reaching for a
Summarybecause its quantiles look simpler to get directly, without realizing that choice blocks you from ever computing a correct cross-instance quantile later — this is a design decision that's expensive to change after the metric is already in production and being relied on.
Exercise¶
Lower Prometheus's scrape_interval in prometheus.yml from 5s to 1s, restart Prometheus, and re-run this
lesson's exact 15-request burst. Query orders_inflight_requests over that time window again and report whether
the real spike is now visible in Prometheus's own scraped history — and if it's still invisible (or only partially
visible), explain, from real numbers, why.