---
title: "Gauge vs counter: Prometheus metric types explained"
date: "2026-07-29"
author: "Al Brown"
category: "Engineering"
excerpt: "A counter only increases and is read through rate(); a gauge moves up and down and is read directly. How the four Prometheus metric types differ, why counters reset, and how to pick between them."
---

# Gauge vs counter: Prometheus metric types explained

A counter is a Prometheus metric whose value only increases, or resets to zero on restart, and is meaningful through its rate of change. A gauge is a metric whose value moves up and down and is meaningful as read. They are two of the four core metric types Prometheus defines, alongside histograms and summaries.

## What is a counter metric?

A counter is a cumulative metric representing a single monotonically increasing value. The running total climbs for as long as the process lives and starts again from zero when it restarts. Raw counter values are rarely useful on their own; what matters is the rate of change, which turns the running total into events per second.

Counters count things that happen: requests served, errors returned, bytes transmitted, jobs completed. Canonical examples are `http_requests_total` and `node_network_receive_bytes_total`; the [OpenMetrics specification](https://prometheus.io/docs/specs/om/open_metrics_spec/) standardises the `_total` suffix for counter sample names. The [Prometheus metric types documentation](https://prometheus.io/docs/concepts/metric_types/) is explicit about the boundary: "Do not use a counter to expose a value that can decrease." A count of currently running processes belongs in a gauge, because processes finish.

## What is a gauge metric?

A gauge is a metric that represents a single numerical value that can arbitrarily go up and down. Unlike a counter, a gauge is meaningful as read: memory in use, queue depth, active connections, temperature. A gauge answers "what is the level right now?" rather than "how often is this happening?"

Typical gauges include `process_resident_memory_bytes`, `node_memory_MemAvailable_bytes`, and in-flight request counts. Client libraries let a gauge be set, incremented, or decremented, whereas a counter only increments. The query semantics differ too: gauges aggregate directly, so `avg()`, `max()`, and `sum()` across instances produce meaningful values, and `delta()` or `deriv()` describe how a gauge changed over a window.

## What is the difference between a gauge and a counter?

The difference is monotonicity. A counter only increases and measures how often something happens; its absolute value depends on when the process last restarted, so only its rate of change carries meaning. A gauge moves in both directions and measures the current state of something; its absolute value is the measurement.

That one property determines which client-library operations exist (counters have no decrement), which PromQL functions apply (`rate()` for counters, `delta()` for gauges), and what a restart does to the data (a counter falls to zero, a gauge simply reports the new true level).

## What metric types does Prometheus support?

Prometheus supports four core metric types: counters, gauges, histograms, and summaries. Histograms count observations into configurable buckets; summaries compute quantiles on the client over a sliding window. Prometheus v2.40 (November 2022) added native histograms, which use exponential bucketing and need no manual bucket configuration.

| Type | Can it decrease? | How you read it | OpenTelemetry equivalent | Example |
|---|---|---|---|---|
| Counter | No (resets to zero on restart) | `rate()`, `increase()` | Counter (monotonic Sum) | `http_requests_total` |
| Gauge | Yes | Directly: `avg()`, `max()`, `delta()` | Gauge or UpDownCounter | `process_resident_memory_bytes` |
| Histogram | No (bucket counts only grow) | `histogram_quantile()` over buckets | Histogram | `http_request_duration_seconds` |
| Summary | Quantile values move; count and sum only grow | Precomputed quantiles, read directly | None (legacy compatibility only) | `rpc_duration_seconds` |

The OpenTelemetry column follows the [Prometheus and OpenMetrics compatibility spec](https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/): an OTel Counter exports as a Prometheus counter, an OTel Gauge as a gauge, and an UpDownCounter (a sum that can decrease, such as items in a queue) also lands as a Prometheus gauge, because Prometheus has no non-monotonic counter. For latency data the mapping matters twice over: the histogram's [bucket layout determines percentile accuracy](/resources/engineering/opentelemetry-histograms), and [percentiles rather than averages](/resources/engineering/percentiles-vs-averages) are the standard read on that data because latency distributions are right-skewed.

## Why do counters reset and how do I handle it?

Counters reset because they live in process memory: when the process restarts, the running total starts again from zero. PromQL's `rate()` and `increase()` absorb resets automatically: any decrease in a counter's raw value is interpreted as a reset, and the calculation adjusts for the break in monotonicity.

The [PromQL function documentation](https://prometheus.io/docs/prometheus/latest/querying/functions/) states it directly for both functions: "Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for." Two practical rules follow from that mechanism:

- **Always `rate()` before you aggregate.** `sum(rate(...))` sees each series' resets individually; `rate(sum(...))` sums away the drops that mark a reset and miscounts.
- **Never `rate()` a gauge.** Legitimate decreases look like resets to the heuristic; use `delta()` or `deriv()` for gauges instead.


```promql
# Correct: derive the per-series rate, then aggregate
sum by (service) (rate(http_requests_total[5m]))
```

## When should I use a gauge vs a counter?

Use a counter when you care about how often something happens and the value never legitimately goes down: requests, errors, bytes, completed jobs. Use a gauge when you care about the current level of something that rises and falls: memory usage, queue depth, active sessions, disk space remaining.

Two questions settle almost every case:

- **Can the value go down in normal operation?** If yes, it is a gauge.
- **Is the rate what you actually chart or alert on?** If yes, it is a counter.

Some quantities are worth instrumenting both ways: `queue_length` as a gauge tells you the backlog now, while `items_enqueued_total` and `items_dequeued_total` as counters tell you throughput on each side. The distinction maps onto alerting practice: of the [four golden signals](/resources/engineering/golden-signals), traffic and errors are counter-shaped, saturation is gauge-shaped, and latency belongs in histograms.

## Working with counters and gauges in ClickStack

[ClickStack](https://clickhouse.com/clickstack), the ClickHouse-based observability stack, ingests metrics through the OpenTelemetry Collector, so the type semantics above carry through unchanged: sums keep their `IsMonotonic` and aggregation-temporality metadata, and gauges arrive as point-in-time values. Each data point is stored as a raw row in ClickHouse rather than a pre-downsampled series, so rates and levels can be re-derived over any window at query time. Against a simplified version of the schema, a per-service rollup looks like this:


```sql
SELECT
    toStartOfMinute(TimeUnix) AS minute,
    ResourceAttributes['service.name'] AS service,
    max(Value) AS cumulative_requests
FROM otel_metrics_sum
WHERE MetricName = 'http.server.request.count'
GROUP BY minute, service
ORDER BY minute
```

HyperDX, ClickStack's UI, charts these metrics next to the logs and traces from the same services, so a counter spike links directly to the requests behind it. ClickStack is open source; the [getting started guide](https://clickhouse.com/docs/clickstack/overview) covers the metrics schema these queries run against.


## FAQ

### Question 1: What is a histogram metric?

A histogram counts observations, such as request durations, into configurable buckets, exposing a `_bucket` series per bound plus `_sum` and `_count` totals. Quantiles like p99 are computed at query time with `histogram_quantile()`. Because bucket counts are themselves counters, histograms aggregate correctly across instances and time windows.### Question 2: Can a counter ever go down?

A counter's raw value decreases only when the process restarts and the count begins again from zero. PromQL treats any decrease as a reset, never as real data. If a value genuinely falls during normal operation, model it as a gauge, or as an UpDownCounter in OpenTelemetry.### Question 3: Is a Prometheus summary the same as a histogram?

No. A summary computes quantiles on the client over a sliding window and streams the finished values, which cannot be aggregated across instances; averaging per-host p99s is statistically invalid. A histogram ships raw bucket counts instead, so quantiles are computed at query time over any set of series.### Question 4: Can I use rate() on a gauge?

No. `rate()` assumes monotonic input and interprets every decrease as a counter reset, so a normally fluctuating gauge produces spurious spikes. Use `delta()` for the change in a gauge over a window, or `deriv()` for its per-second trend based on linear regression.