skip to content

Metric Types and Data Model

Counter, gauge, histogram and summary, each with a label set that turns one metric name into many time series. The interview trap is cardinality — putting a user ID or request path in a label is how teams blow up their Prometheus.

on this pageshow

questions

4

In Prometheus's text exposition format, what does one metric line contain, and what does the server keep from the `# HELP` and `# TYPE` lines?

level: juniorimportance: must knowfreq 58%

answer

  1. Everything a target serves is plain text
  2. Two comment lines describe, one line measures
  3. Name, braces, value, optional timestamp
  4. HELP and TYPE are metadata, not samples

basics

~20 s

One line carries a metric name, an optional label set in braces, a float64 value and an optional millisecond timestamp. The HELP comment describes the family and the TYPE comment declares counter, gauge, histogram, summary or untyped.

solid answer

~40 s

A Prometheus target serves plain text where each sample is one line: `metric_name{label="value"} 12.7`, optionally followed by a millisecond Unix timestamp. The value parses as a `float64` and may be `NaN`, `+Inf` or `-Inf`. Two comment lines precede each metric family: `# HELP <name> <text>` and `# TYPE <name> <counter|gauge|histogram|summary|untyped>`; every other `#` line is ignored. The server parses both, but they are metadata rather than data — Prometheus keeps the latest help text and type per metric per target, exposes them through its metric-metadata API and uses them in the UI, and stores neither alongside the samples. Nothing in PromQL filters on a declared type, and the declaration changes nothing at ingest: a counter and a gauge are both stored as a series plus timestamp/value pairs.

code

text · 7 lines
text
# HELP seedorder_http_requests_total Orders API requests by route and status
# TYPE seedorder_http_requests_total counter
seedorder_http_requests_total{route="/v1/checkout",status="200"} 41823
seedorder_http_requests_total{route="/v1/checkout",status="503"} 217
# HELP seedorder_catalogue_entries Seed varieties currently listed
# TYPE seedorder_catalogue_entries gauge
seedorder_catalogue_entries 9364

go deeper

for a junior

Be ready to read a target's metrics page aloud: metric name, braces of labels, then the value. Know that # HELP is a human description and # TYPE names one of counter, gauge, histogram, summary or untyped.

for a middle

Explain that the value parses as a float64 and the optional trailing number is a millisecond Unix timestamp, and that the two comment lines belong to a whole metric family rather than to a single sample line.

for a senior

Show that the metadata is kept per target and never joins the samples, so a wrong TYPE misleads people and dashboards while changing nothing about ingestion, and that a single malformed line fails the entire scrape.

for a principal

Own the standard for the fleet: whether targets serve the classic text format or negotiate OpenMetrics, who is accountable when two services declare the same metric name with different types, and how that is caught before it ships.

## What a target actually serves A Prometheus target exposes a plain-text document over HTTP. There is no envelope, no JSON, no schema: the body is a sequence of lines, and the whole Prometheus data model is visible in them. A **sample line** has three or four parts: ``` metric_name{label="value",other="value"} 12.7 1757116800000 ``` - **metric name** — the identity of the metric family. - **label set** — zero or more `name="value"` pairs inside braces, comma separated, values double-quoted and escaped. Omit the braces entirely when there are no labels. - **value** — parsed as a `float64`. `NaN`, `+Inf` and `-Inf` are legal, and an integer is simply a float that happens to be whole. - **timestamp** — optional, an `int64` count of milliseconds since the Unix epoch. Almost every target omits it, and that is the right default: the server then stamps the sample with the scrape time, so every sample from one scrape shares one timestamp. The detail that surprises people is that the metric name is itself a label. A series is identified by its complete label set, and the name lives in the reserved label `__name__`, so `seedorder_http_requests_total{route="/v1/checkout"}` and `{__name__="seedorder_http_requests_total",route="/v1/checkout"}` name the same series. ## The two comment lines Lines beginning with `#` are comments. Two of them are special, and both belong to a **metric family** (all the series sharing one metric name), not to an individual line: - `# HELP <name> <free-form text>` — one human-readable description of the family. - `# TYPE <name> <type>` — declares the family as `counter`, `gauge`, `histogram`, `summary` or `untyped`. By convention a family's `# HELP` and `# TYPE` lines precede its samples and all of a family's samples are grouped together. Any other `#` line is an ordinary comment and is ignored. ## What the server keeps and what it discards | Element of the exposition | Where it ends up | |---|---| | metric name | stored, as the `__name__` label of the series | | labels | stored, part of the series identity | | value | stored, as a `float64` | | explicit timestamp | used if present, otherwise the scrape time is used | | `# HELP` text | kept as per-target metadata, never attached to a sample | | `# TYPE` declaration | kept as per-target metadata, never attached to a sample | | any other comment line | discarded | The metadata rows are the ones that matter in an interview. Prometheus parses `# HELP` and `# TYPE`, keeps the most recent value **per metric, per target**, exposes it over the server's metric-metadata HTTP API and uses it in the expression browser. It is never a sample. Four consequences follow: 1. **There is no history.** Only the latest description and type are available; you cannot ask what a metric's help text said last week. 2. **PromQL cannot select on it.** There is no way to write "every counter" — the declared type is not part of any series' identity. 3. **The declaration is advisory.** Declaring `counter` does not make the server reject a value that goes down, and declaring `gauge` does not stop anything from treating the series as cumulative. Storage is identical either way: a series plus timestamp/value pairs. 4. **Nothing enforces agreement.** Two targets in the same job may declare the same metric name with different types or different help text; the metadata surface just reports what each target said. So a wrong `# TYPE` is a documentation defect. It misleads humans, autocompletion and linters, and it produces silently wrong dashboards when somebody trusts it — but it is never an ingestion error. ## OpenMetrics, the negotiated variant Prometheus can negotiate a second text format, OpenMetrics, through the `Accept` header. It is deliberately close to the classic format, with a handful of differences worth recognising: it adds a `# UNIT` metadata line, it names the unspecified type `unknown` rather than `untyped`, it requires the document to end with a `# EOF` line, and it can carry an exemplar on a sample line. Everything above still applies — `# UNIT` is metadata, not data. ## Why the thinness matters in practice Because the format is this thin, most integration problems reduce to reading it. A seed-catalogue ordering service in a newly brought-up region that "exports nothing" is diagnosed by fetching the text and looking: an empty body, a body with `# HELP` and `# TYPE` lines but no samples, and a body whose label values are unescaped are three completely different bugs that look identical from a dashboard. The flip side of a format that is trivial to generate is that nothing validates your output. A line with an unquoted label value or a stray second numeric field makes the parse fail, and a failed parse fails the whole scrape — not just that line — so none of that scrape's samples land. That is why exporters that build the text by hand are worth a test that parses their own output before shipping.

  • What does Prometheus do with an explicit timestamp on an exposition line?
    The format allows an int64 millisecond Unix timestamp after the value, and the server uses it instead of the scrape time. Almost no target sets one, and the convention is to omit it: explicit timestamps interact awkwardly with staleness handling and with the server's rules about out-of-order data. The usual legitimate users are exporters translating an external system's already-timestamped readings.
  • Does declaring `# TYPE ... counter` change how Prometheus stores or queries that series?
    No. Storage is identical for every type — a series plus timestamp and float64 pairs — and no PromQL construct selects on the declared type. The declaration is advisory metadata for humans, UIs and linters. Counter semantics rest entirely on the exporter only ever increasing the value and resetting it to zero on process restart; nothing in the server enforces that.
  • A target renames a metric between two scrapes. What happens to the old series?
    Nothing renames. The old series simply stops receiving samples and gets a stale marker, and the new name is a different series with its own history starting from that scrape. The old samples remain in storage until retention removes them, so for a while both names exist and any query spanning the change has to handle both.

saying these in an interview costs you the question

  • Thinks HELP and TYPE are stored with every sample
  • Believes PromQL can filter series by their declared type
  • Expects the server to reject a counter that decreases
  • Assumes every exposition line must carry a timestamp
  • Describes the exposition body as a JSON payload
open as a page

In Prometheus, why does one histogram appear as many series in the exposition format, and why is the bucket boundary a label rather than a metric name?

level: middleimportance: should knowfreq 62%

basics

~20 s

Prometheus has no multi-value sample, so one histogram is exposed as many plain series: a <name>_bucket series per boundary carrying an le label, plus <name>_sum and <name>_count. A summary instead exposes quantile-labelled series alongside its own _sum and _count.

open as a page

Which Prometheus metric and label naming conventions do you enforce in review, and what breaks later when each is ignored?

level: seniorimportance: should knowfreq 41%

basics

~20 s

Enforce a namespace prefix, base units in the name (seconds, bytes, 0-1 ratios), a plural unit suffix, _total on a cumulative counter, and dimensions as labels, never name fragments. Label names beginning with two underscores are reserved.

open as a page

In Prometheus, what happens to a time series when its target stops exposing it, and why does it vanish from an instant query instead of reading zero?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

A Prometheus sample is one float64 value at one millisecond timestamp on one series. When a target stops exposing a series, the server appends a stale marker, so queries after it return nothing — absence is not zero.

open as a page