The Prometheus Data Model: Metrics, Labels, and Metric Types
What a time series actually is in Prometheus, and the plain-text format targets expose them in.
Every part of Prometheus, scraping, storage, PromQL, alerting, operates on the same underlying structure: a time series, identified by a name and a set of labels, made up of a stream of timestamped values. This guide covers that structure and the plain-text format targets use to expose it.
A time series is a name, a set of labels, and a stream of samples
A metric name describes what’s being measured: http_requests_total, process_open_fds, node_memory_available_bytes. On its own, a metric name isn’t yet a single time series, it’s a family that labels subdivide into individual series. Labels are key-value pairs attached to a metric: method="GET", status="500", path="/api/users". The combination of a metric name plus one specific set of label values identifies exactly one time series.
http_requests_total{method="GET", status="200", path="/api/users"}
http_requests_total{method="GET", status="500", path="/api/users"}
http_requests_total{method="POST", status="200", path="/api/orders"}
These are three distinct time series sharing one metric name. Prometheus automatically attaches its own labels too, identifying which target and job a series came from, on top of whatever labels the application itself chose to expose.
A sample is one point in a series: a 64-bit millisecond timestamp (usually the moment of the scrape) paired with a 64-bit floating-point value. A series is the ongoing stream of these samples that accumulates, one per scrape, over time.
The four metric types
Prometheus’s client libraries expose four kinds of metric, each meaning something different when you come to query it:
- Counter: a value that only ever goes up (or resets to zero on restart), like a running total of requests handled. You almost never read a counter’s raw value; you compute a rate of change over it, covered in the PromQL guide.
- Gauge: a value that can go up or down, like current memory usage or an in-progress request count. Read directly, unlike a counter.
- Histogram: samples observations (request durations, response sizes) into configurable buckets, and exposes a count and sum alongside them, enabling quantile calculations (like “95th percentile latency”) after the fact from stored bucket data.
- Summary: similar to a histogram, but calculates quantiles client-side at observation time rather than from buckets. Cheaper to query, but summary quantiles can’t be aggregated across instances the way histogram buckets can, which is why histograms are the more common choice for anything you’ll aggregate across a fleet.
A native histogram type, storing full-resolution histogram data more efficiently than the older bucketed form, has been in active development for a while; check current Prometheus documentation for its present maturity before depending on it, since exactly how officially supported it is has shifted across recent releases.
The exposition format
For Prometheus to scrape a target, that target serves its current values over HTTP as plain text, in a specific, deliberately simple layout:
# HELP http_requests_total Total number of HTTP requests handled.
# TYPE http_requests_total counter
http_requests_total{method="GET", status="200"} 8420
http_requests_total{method="GET", status="500"} 12
# HELP process_open_fds Number of open file descriptors.
# TYPE process_open_fds gauge
process_open_fds 27
# HELP and # TYPE lines are comments providing documentation and the metric’s type; every other non-comment line is one series’ current value, as a metric name, its labels in curly braces, and the value itself. Because this is only ever a snapshot, one line per series showing its value right now, there’s no history in a scrape response; history only exists once Prometheus has scraped the same series repeatedly over time and stored each result.
The format’s simplicity is deliberate: it’s meant to be trivial to generate even without a client library, which is what makes it realistic to get Prometheus metrics out of almost anything, not just software with official library support.
See it for yourself
Because scraping is just an HTTP GET, you can view any target’s current metrics directly in a browser or with curl, no Prometheus server required:
curl http://localhost:9100/metrics # a node_exporter instance, for example
That output is exactly what Prometheus itself receives on each scrape, which makes it a genuinely useful first troubleshooting step: if a target isn’t showing up correctly in Prometheus, checking its raw /metrics output directly tells you whether the problem is the target or something in Prometheus’s own scrape configuration.
Where labels come from, and why they matter later
A target’s own labels (which endpoint, which method, which status code) come from however the application or exporter instrumented itself. Prometheus adds its own on top, identifying the job and specific instance a series was scraped from. Together, these labels are what PromQL’s aggregation and filtering operate on, letting you ask for “error rate across every instance” or “request rate for just this one path” against the same underlying data, which is exactly what the next guide in this series covers.
Popular Guides
-
Prometheus Architecture and the Pull Model Why Prometheus scrapes instead of receiving, and how targets, exporters, and service discovery fit together. -
Get Started with PromQL: Selecting, Rating, and Aggregating Build up from a simple metric selector to a real query: per-path error rate, computed live. -
Monitor a Host and Its Containers with node_exporter and cAdvisor Run Prometheus, node_exporter, and cAdvisor together, scrape both, and confirm the targets are healthy.