← Back to playground

πŸ“‘The Metrics Pipeline

Metrics are how a service watches itself β€” billions of events crushed into cheap, queryable numbers. This is the pipeline that carries one number from your code to an alert, and the engines that store it along the way: Prometheus, InfluxDB, TimescaleDB, and the scaling tier (Thanos, Mimir, VictoriaMetrics) β€” plus OpenTelemetry, the vendor-neutral layer tying it all together. Built from primary sources, written against the current releases.

Prometheus InfluxDB TimescaleDB VictoriaMetrics

TL;DR

  • A metric is an aggregation. It is a numeric value, sampled over time, carrying a name and a set of labels. That makes metrics cheap to store and trivial to sum or average β€” but lossy: you can know that 4% of checkouts failed in the last minute without knowing which ones. That's the opposite trade-off from logs and traces, which keep every event but cost far more.
  • It's a pipeline, not a product. The journey is instrument β†’ collect β†’ store β†’ query β†’ visualize β†’ alert. Most "tools" own several of those stages at once, which is why naΓ―vely comparing them is confusing.
  • The three you've heard of sit at different layers. Prometheus is a whole vertical β€” it scrapes, stores, queries (PromQL) and alerts β€” but is single-node by design. InfluxDB is a purpose-built push TSDB (v3 stores columnar Parquet and answers in SQL). TimescaleDB is just a storage layer β€” a PostgreSQL extension, so it speaks full SQL and joins against your business tables.
  • One constraint rules everything: cardinality. A series is one unique label-set; put an unbounded label (a user ID, a request ID) on a metric and you multiply your series into the millions. Unbounded identifiers belong on logs and traces, never on a metric.
  • OpenTelemetry is the connective tissue. OTLP decouples how you instrument from where you store, so you can swap Prometheus for InfluxDB for a vendor without re-instrumenting. Prometheus 3.x even ingests it natively.

The metrics pipeline

Follow a single number β€” say, the duration of one checkout request β€” from the line of code that records it to the page that wakes someone up. It passes through six stages, and almost every tool in this space is really a bundle of two or three of them.

  • Instrument. Your code creates a metric (a counter, a gauge, a histogram) using a client library or an OpenTelemetry SDK and updates it as the program runs.
  • Collect. An agent gathers the numbers β€” either by scraping an HTTP endpoint your app exposes (pull) or by receiving what your app sends it (push) β€” then batches, enriches, and forwards them. This is Prometheus's server, the OpenTelemetry Collector, or Telegraf.
  • Store. A time-series database persists the samples on disk, compressing aggressively and indexing by label.
  • Query. A query engine reads it back and aggregates on demand β€” PromQL for Prometheus-shaped data, SQL for InfluxDB 3 and TimescaleDB.
  • Visualize. A dashboard layer β€” almost always Grafana β€” turns queries into graphs.
  • Alert. A rules engine evaluates queries on a schedule and fires when a condition holds, routing the notification to a human.
One number's journey: from your code to an alert expose Β· push OTLP Β· remote-write Β· line protocol read Instrument app + client SDK counter Β· gauge Β· histogram Collect scrape or receive Collector Β· Telegraf Store TSDB β€” series on disk / object store Query PromQL Β· SQL aggregate Visualize Grafana dashboards Alert rules β†’ notify
The six stages. A bare Prometheus owns four of them at once (collect, store, query, alert); InfluxDB and TimescaleDB own just the store-and-query middle; Grafana owns visualize. Knowing which stage a tool lives in is most of understanding it.

Instrumenting code

Everything starts with a measurement in your program. There are only four shapes a metric takes, and picking the right one is most of the craft.

TypeWhat it doesExamples
CounterMonotonic total; only ever increasesrequests served, errors, bytes sent
GaugeA value that goes up and downqueue depth, in-flight requests, temperature
HistogramBucketed distribution of observationsrequest latency, response sizes
SummaryClient-side quantiles over a windowlatency where you can't pick buckets

A working example, using the Prometheus Python client. The shape barely changes across languages or with an OpenTelemetry SDK β€” what matters is the discipline around labels.

Instrumenting a serviceβ€” Python Β· prometheus_client
from prometheus_client import Counter, Histogram, start_http_server

REQUESTS = Counter("http_requests_total", "Total HTTP requests",
                    ["method", "route", "status"])   # bounded labels only
LATENCY  = Histogram("http_request_seconds", "Request latency", ["route"])

@LATENCY.labels(route="/checkout").time()        # observe the duration
def handle_checkout(req):
    REQUESTS.labels("POST", "/checkout", "200").inc()   # count this request
    ...

start_http_server(9090)   # exposes GET /metrics for a scraper to pull

How to choose what to measure

Two methodologies dominate, and they're complementary:

  • RED β€” Rate, Errors, Duration, measured per request or endpoint. It tells you how happy your users are, so it's the right thing to alert on (symptoms, not causes).
  • USE β€” Utilization, Saturation, Errors, measured per resource (CPU, memory, disk). It tells you how happy your machines are, which is where you look to diagnose a symptom RED surfaced.

One modern refinement worth knowing: classic histograms force you to pick bucket boundaries up front, and each bucket is a series. Native (exponential) histograms, a stable Prometheus feature, replace fixed buckets with an automatically-scaled exponential layout β€” far better resolution at a fraction of the series cost.

Pull vs push

The oldest argument in metrics: does the collector reach out and scrape your service, or does your service send its numbers somewhere? Prometheus is the famous pull system; InfluxDB/Telegraf, OpenTelemetry, and StatsD are push. Each genuinely wins in different places.

Pull β€” the server scrapes Prometheus scrapes on a timer service /metrics service /metrics service /metrics GET /metrics every scrape yields up{} β€” health for free Push β€” the source sends app SDK short-lived job Telegraf agent OTel SDK behind NAT InfluxDB / collector receives writes write Β· OTLP works through firewalls & for ephemeral jobs
Pull gives you a free liveness signal and automatic cleanup when a target disappears; push reaches things a scraper can't β€” serverless functions, batch jobs, anything behind a firewall.

Pull (Prometheus) wins on operability: every scrape produces an up metric, so the server knows whether each target is healthy, and when service discovery says an instance is gone, its series simply stop β€” no cleanup. Push wins on reach: a function that lives for 200 ms, a nightly batch job, or anything behind a firewall can't be scraped, so it sends instead.

The Pushgateway is not a general "push mode"

Prometheus's Pushgateway exists for one narrow case: service-level batch jobs that finish before a scrape can happen. The docs warn it becomes a single point of failure, you lose the up health signal, and it never forgets a pushed series β€” they linger until you delete them by API. For genuinely push-shaped telemetry, reach for the OpenTelemetry Collector or an InfluxDB-style sink, not the Pushgateway.

The cardinality problem

If you remember one thing about operating a metrics system, make it this. A time series is one unique combination of a metric name and its label values. Every distinct combination is stored and indexed separately. So the cost of a metric isn't how often you update it β€” it's how many label combinations it can produce.

A series = one unique label combination method 2 values Γ— route 3 values Γ— status 3 values = 18 series cheap & manageable + user_id 50,000 values β†’ 900,000 series πŸ’₯ one unbounded label melts the database Rule of thumb: sum() or avg() over a metric's labels should still be meaningful. If it isn't, the label doesn't belong.
Cardinality is multiplicative. Bounded labels (a handful of routes and statuses) are fine; a single unbounded label β€” a user ID, an email, a full URL, a request ID β€” turns one metric into millions of series and is the number-one cause of a metrics outage.

This is exactly where the three pillars divide the labor. Metrics are aggregations into bounded series, so unbounded identifiers are toxic to them. Logs and traces are per-event records β€” high-cardinality IDs are precisely the dimension you want to search them on. The discipline: keep the unbounded ID on the log line, the trace span, or a Prometheus exemplar β€” and let the metric tell you, cheaply, only that something is wrong.

Storage engines compared

The middle of the pipeline β€” store and query β€” is where the well-known databases actually compete. Four worth knowing: Prometheus's built-in TSDB, InfluxDB 3, TimescaleDB, and VictoriaMetrics. Use the controls to filter rows to one dimension or highlight a single engine's column. (Both persist across reloads; Copy as Markdown always exports the full matrix.)

Filter rows
Highlight column
View
Dimension Prometheus InfluxDB 3 TimescaleDB VictoriaMetrics
What it isMonitoring system + local TSDBPurpose-built time-series DBPostgreSQL extensionTSDB + monitoring system
Core designPull-based, single-node by designColumnar engine on object storageTime-series on top of PostgresFrom-scratch Go TSDB, efficiency-first
Primary query languagePromQLSQL (+ InfluxQL)Full PostgreSQL SQLMetricsQL (PromQL superset)
Data modelmetric + labels; float / native-histogram samplesmeasurements, tags, fields (line protocol)ordinary SQL tables (hypertables)metric + labels (Prometheus-compatible)
Ingest modelPull (scrape); push via remote-write / OTLPPush β€” line protocol, TelegrafPush β€” SQL INSERT / COPYBoth β€” scrape (vmagent) or remote-write
Native scrape (pull)Yes β€” the core modelNo (Telegraf can scrape for it)NoYes β€” via vmagent
OpenTelemetry / OTLPNative OTLP receiver (3.x)Via Telegraf / OTLPVia collector β†’ SQLNative OTLP ingestion
Storage engineLocal TSDB β€” 2-hour blocks + WALColumnar Parquet in object storageHypercore β€” row + columnar in PostgresOwn on-disk columnar format
Long-term / durabilityLocal disk (~15d default); needs Thanos / Mimir for LTSObject storage = durable by constructionPostgres durability + compression + retentionNative long-term; cluster for HA
CompressionGorilla-style, very compactParquet columnarColumnar, often ~90%+High β€” a core design goal
Horizontal scaleSingle node; federate or add an LTS layerCloud Dedicated / Clustered editionsScale-up + read replicas; multi-node via cloudNative cluster (vminsert / vmstorage / vmselect)
Multi-tenancyNo β€” run a server per tenantCloud editionsPostgres roles / databasesYes β€” in cluster mode
High cardinalityStrained past millions of series / nodeImproved in v3Workable; watch index costDesigned for high churn & cardinality
Relational joinsNoLimitedYes β€” full SQL joinsNo
First-party / managedGrafana Cloud, Chronosphere, & othersInfluxDB Cloud (Serverless / Dedicated)Tiger Cloud (formerly Timescale Cloud)Managed VictoriaMetrics Cloud
LicenseApache 2.0 (CNCF graduated)MIT/Apache (Core); commercial EnterpriseApache 2.0 core; Timescale License for some featuresApache 2.0
EcosystemHuge β€” exporters, Grafana, Alertmanager, K8sTelegraf (300+ plugins)The entire Postgres ecosystemDrop-in Prometheus-compatible stack
Best-fit use caseCloud-native / Kubernetes infra & app monitoringIoT, sensor & high-volume event metricsMetrics alongside relational / business dataLarge-scale, cost-efficient Prometheus storage
Sharpest footgunThe storage is single-node β€” not the modelFlux is gone in v3; migrating off v2 is a rewriteIt is still Postgres β€” operate & tune it like PostgresSmaller community than Prometheus itself

Read the columns as philosophies. Prometheus assumes a single node is enough for one team's recent data and pushes everything else to a separate long-term layer. InfluxDB rebuilt itself around columnar files in object storage and SQL. TimescaleDB makes the radical-but-pragmatic bet that you shouldn't need a separate database at all β€” time-series is just Postgres with good partitioning. VictoriaMetrics keeps Prometheus's interface but rewrites the engine underneath for efficiency at scale.

Scaling Prometheus

Prometheus is deliberately a single binary on a single node: no clustering, no built-in long-term storage (the local disk defaults to roughly 15 days of retention), and no multi-tenancy. Its own scaling features β€” functional sharding and federation β€” only move aggregated series upward, not a complete global view. That gap created an entire ecosystem of long-term, horizontally-scalable backends that speak Prometheus's protocols.

A Β· Sidecar (Thanos) Prometheus + local TSDB Thanos sidecar object storage Thanos Querier global Grafana view HA Β· years of history B Β· Remote-write (Mimir Β· Cortex Β· VictoriaMetrics) Prometheus (agent mode) Mimir / Cortex / VM distributor + ingesters object storage PromQL query + Grafana multi-tenant Β· horizontally scaled remote_write
Two patterns for going beyond one node. The sidecar (Thanos) leaves Prometheus untouched and ships its blocks to object storage. Remote-write (Mimir, Cortex, VictoriaMetrics) streams samples into a clustered, multi-tenant backend. Both keep PromQL and Grafana unchanged.
BackendRelation to PrometheusStorageOrigin
ThanosSidecar (reads TSDB blocks) + receiveObject storageReuses Prometheus's TSDB format (CNCF)
CortexRemote-write receiverObject storageOriginal multi-tenant scaler (CNCF)
Grafana MimirRemote-write (+ OTLP)Object storageCortex fork; tested to ~1 billion series
VictoriaMetricsRemote-write and its own scrapeOwn format (object store for backups)Written from scratch in Go

Note the family split: Thanos, Cortex, and Mimir all reuse Prometheus's own code and storage format, while VictoriaMetrics is a clean-room reimplementation that merely speaks the same protocols. Mimir is the Cortex fork that Grafana hardened for extreme scale and runs behind Grafana Cloud.

OpenTelemetry & OTLP

For years, picking a metrics backend meant picking an instrumentation library β€” switching vendors meant re-instrumenting every service. OpenTelemetry (a CNCF project, and now the de-facto standard) breaks that coupling. You instrument once against the OTel SDK, emit OTLP over the wire, and decide later and separately where it lands.

The workhorse is the OpenTelemetry Collector, a small pipeline of its own: receivers take telemetry in (by push or by scraping Prometheus targets), processors batch / filter / transform it, and exporters fan it out to one or more backends. It runs either as a per-host agent or as a central gateway.

OpenTelemetry Collector: receivers β†’ processors β†’ exporters app SDK (OTLP) scrape target OpenTelemetry Collector RECEIVERS OTLP prometheus hostmetrics PROCESSORS batch filter / transform EXPORTERS prometheus rw OTLP influxdb Prometheus / Mimir InfluxDB
Instrument once, route anywhere. The Collector's receiver/processor/exporter shape lets one telemetry stream land in several backends at once β€” and lets you swap a backend without touching application code.

Interop runs both ways. The Collector's prometheus receiver scrapes existing targets; its exporters write to Prometheus, Mimir, or InfluxDB. And Prometheus 3.x now ingests OTLP natively (enable the OTLP receiver and POST to /api/v1/otlp/v1/metrics).

The temporality gotcha

OpenTelemetry can emit counters as delta (the change since last export); Prometheus expects cumulative (the running total). Mixing them silently breaks rate(). When piping OTel into Prometheus, configure cumulative temporality (or use the delta-to-cumulative conversion) β€” it's the single most common OTel-to-Prometheus footgun.

Query, visualize, alert

Querying

Two query philosophies. PromQL (Prometheus, and VictoriaMetrics' compatible MetricsQL) is a functional language built around time-series math β€” rate(), histogram_quantile(), vector matching. SQL (InfluxDB 3 and TimescaleDB) trades that purpose-built terseness for the familiarity, joins, and tooling of the relational world. A taste of PromQL, the dialect you'll meet most:

PromQLβ€” rates and quantiles, computed at query time
# per-second rate of 5xx responses over the last 5 minutes
rate(http_requests_total{status=~"5.."}[5m])

# p99 latency, reconstructed from histogram buckets
histogram_quantile(0.99, rate(http_request_seconds_bucket[5m]))

Visualizing

Grafana is the near-universal dashboard layer, and its superpower is being multi-datasource: a single dashboard can pull one panel from Prometheus, another from InfluxDB, another from a SQL database. It's why the storage decision and the visualization decision are largely independent β€” almost everything graphs in Grafana.

Alerting

Two rule types do the work. Recording rules pre-compute an expensive query on a schedule and save the result as a new series, so dashboards and alerts read something cheap. Alerting rules evaluate a condition and, when it holds, fire to Alertmanager, which deduplicates, groups, silences, and routes the notification to a human. The best-practice pairing with RED: alert on symptoms (a rising error rate users feel), then use your dashboards to find the cause.

Deployment & decision guide

The honest mapping from situation to stack:

  • Cloud-native infra & app monitoring (Kubernetes). Reach for Prometheus + Grafana + Alertmanager β€” the default, with first-class service discovery and a vast exporter catalog. Add Mimir, Thanos, or VictoriaMetrics when one node isn't enough.
  • Metrics next to relational / business data, with a SQL team. Reach for TimescaleDB β€” keep metrics and the rest of your data in one Postgres, join freely, and skip operating a second database.
  • High-volume push, IoT, or sensor telemetry. Reach for InfluxDB β€” line-protocol ingest, the Telegraf agent ecosystem, and a columnar engine built for write-heavy event streams.
  • Huge scale or tight cost on a Prometheus-shaped stack. Reach for VictoriaMetrics or Grafana Mimir β€” same PromQL and Grafana, far more series per dollar.
  • Future-proof instrumentation, undecided backend. Instrument with OpenTelemetry and route through the Collector β€” you can change your mind about storage later without touching code.
Cloud-native / Kubernetes infra? yes Prometheus + Grafana no Join metrics with relational data? yes TimescaleDB no High-volume push / IoT telemetry? yes InfluxDB no VictoriaMetrics / Mimir the cloud-native default one Postgres for everything line protocol + Telegraf Cross-cutting: instrument with OpenTelemetry to keep the backend choice reversible.
A rough triage. The first question β€” is this cloud-native infrastructure? β€” sends most teams to Prometheus; the rest sort by whether you need SQL joins, push-heavy ingest, or raw scale.

Common misconceptions

"Prometheus can't scale"

Its collection model scales fine β€” what's single-node is its local storage. The fix isn't to abandon Prometheus; it's to add a long-term layer (Thanos, Mimir, Cortex, or VictoriaMetrics) that keeps PromQL and Grafana intact.

"Just write it in Flux"

Flux is in maintenance mode and is not supported in InfluxDB 3. The current path is SQL (with InfluxQL for compatibility). Moving a v2 Flux codebase to v3 is a rewrite, not an upgrade.

"TimescaleDB is a separate time-series database"

It's a PostgreSQL extension. You operate, back up, secure, and tune it exactly like Postgres β€” which is the upside (one familiar system) and the catch (its ceilings are Postgres's ceilings).

"Add a user_id label so we can slice by customer"

That's the classic cardinality bomb: an unbounded label multiplies your series into the millions and can take the database down. Slice by customer in logs or traces; keep metric labels bounded.

"The Pushgateway turns Prometheus into a push system"

It's for service-level batch jobs only. It's a single point of failure, it drops the up health signal, and it never forgets a pushed series. For real push telemetry use the OpenTelemetry Collector or a push-native sink.

Further reading

Prometheus & the scaling tier

InfluxDB & TimescaleDB

  • InfluxDB 3 docs β€” Core / Enterprise / Cloud editions, line protocol, SQL, and the Arrow/DataFusion/Parquet engine; Telegraf for collection.
  • TimescaleDB (by Tiger Data) β€” hypertables, continuous aggregates, and the Hypercore columnar engine; the docs.

OpenTelemetry & methodology

On this site

  • ClickHouse: An Engineering Doc β€” the columnar, vectorized OLAP engine that shares this world's DNA (and is itself a popular store for very high-volume metrics and observability data).