Engineering Notes
Home/DevOps & Cloud/Observability/Metrics
DevOps & Cloud · Observability

Metrics

Metrics are cheap numeric time series that power dashboards and alerts. Learn the four metric types, instrument a service, pull vs push, the PromQL queries you'll use daily, and why labels, not events, drive cost.

☁️ DevOps & Cloud● Intermediate⏱ 4 min read▶ 1 interactive🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Counters, gauges, histograms and summaries
  • Instrumenting a service and the /metrics endpoint
  • Pull vs push collection
  • PromQL essentials and label cardinality

📚 Before you start

1What a Metric Is

A metric is a numeric measurement sampled over time and stored as a time series: name{labels} → [(t1, v1), (t2, v2), …]. Metrics are cheap to store and fast to query because they're aggregated at the source — you count requests, you don't store each one.

text
http_requests_total{service="checkout", route="/v1/orders", method="POST", status="201"}  10342
http_requests_total{service="checkout", route="/v1/orders", method="POST", status="500"}     17

2The Four Metric Types (Prometheus / OpenMetrics)

TypeBehaviourExampleQuery with
Counteronly goes up (resets to 0 on restart)http_requests_total, bytes_sent_totalrate(), increase()
Gaugegoes up and downqueue_depth, memory_bytes, in_flight_requestsraw value, avg_over_time()
Histogramcounts observations into cumulative buckets + _sum + _counthttp_request_duration_secondshistogram_quantile()
Summaryclient-side quantiles (p50/p99) per instancelegacycan't be aggregated across instances

Prefer histograms for latency and sizes: quantiles computed at query time over all instances. Summaries' pre-computed p99s can't be averaged meaningfully.

3Instrumenting a Service

ts
import client from "prom-client";
const httpDuration = new client.Histogram({
  name: "http_request_duration_seconds",
  help: "HTTP request latency",
  labelNames: ["route", "method", "status"],
  buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
app.use((req, res, next) => {
  const end = httpDuration.startTimer();
  res.on("finish", () => end({ route: req.route?.path ?? "unmatched", method: req.method, status: String(res.statusCode) }));
  next();
});
app.get("/metrics", async (_req, res) => { res.type(client.register.contentType).send(await client.register.metrics()); });

Exposition format scraped by Prometheus:

text
# HELP http_request_duration_seconds HTTP request latency
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{route="/v1/orders",method="POST",status="201",le="0.1"} 812
http_request_duration_seconds_bucket{route="/v1/orders",method="POST",status="201",le="0.25"} 990
http_request_duration_seconds_bucket{route="/v1/orders",method="POST",status="201",le="+Inf"} 1000
http_request_duration_seconds_sum{route="/v1/orders",method="POST",status="201"} 84.2
http_request_duration_seconds_count{route="/v1/orders",method="POST",status="201"} 1000

4Pull vs Push

  • Pull (Prometheus): the server scrapes /metrics on every target every N seconds, discovered via Kubernetes/Consul/EC2 service discovery. A failed scrape is itself a signal (up == 0).
  • Push (OTLP, StatsD, CloudWatch PutMetricData, Pushgateway): the app sends metrics. Needed for short-lived jobs and across network boundaries.
  • OpenTelemetry metrics can be exported either way; the OTel Collector bridges.

5PromQL Essentials

promql
# request rate per route (per second, over the last 5 minutes)
sum by (route) (rate(http_requests_total[5m]))

# error ratio
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))

# p99 latency across all instances
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

# saturation: memory used vs limit per pod
container_memory_working_set_bytes / on(pod) kube_pod_container_resource_limits{resource="memory"}

rate() handles counter resets (a restart drops the counter to 0; rate detects the decrease and compensates).

6Labels and Cardinality

Every unique label combination is a separate series. Series count = product of label cardinalities:

text
route (40) × method (4) × status (10) × instance (30) × le buckets (10) = 480,000 series

That's already a lot for one metric. Never label with user IDs, request IDs, email addresses, full URLs, or error messages — unbounded values belong in logs/traces. Use route templates (/users/:id), status classes when possible, and drop unused labels at scrape time (relabeling).

Under the HoodHow Prometheus Stores Series▾

Prometheus's TSDB keeps the most recent ~2 hours of samples in an in-memory head block, backed by a write-ahead log for crash recovery. Each series has its own chunks; samples are compressed with the Gorilla scheme: timestamps as delta-of-delta (scrapes are regular, so most deltas-of-deltas are 0 → 1 bit), values as XOR with the previous value (slowly changing floats share most bits) — about 1.3 bytes per sample. Every 2 hours the head is cut into an immutable block on disk with an inverted index (label=value → posting list of series IDs); queries intersect posting lists to find series, then decode only the needed chunks. Old blocks are compacted into larger ones. Memory use is dominated by the number of active series in the head (each costs a few KB), which is why cardinality, not sample count, is what takes Prometheus down. Long-term storage (Thanos, Mimir, Cortex) ships these blocks to object storage.

A service increments counters and observes histogram buckets, exposes them on /metrics, Prometheus scrapes them into a time series database with a head block and inverted index, and PromQL computes rates, error ratios and histogram quantiles for dashboards and alerts
A service increments counters and observes histogram buckets, exposes them on /metrics, Prometheus scrapes them into a time series database with a head block and inverted index, and PromQL computes rates, error ratios and histogram quantiles for dashboards and alerts

⚠️ Common Pitfalls

  1. High-cardinality labels — memory explosion.
  2. Averages for latency — hide the tail; use histogram quantiles.
  3. Summaries aggregated across instances — mathematically meaningless.
  4. Using rate() on gauges or raw counters without rate().
  5. Badly chosen histogram buckets — p99 estimates are only as precise as the bucket boundaries around them.
  6. Metric names without units/suffixes (_seconds, _bytes, _total).

📋 Quick Reference

NeedUse
how many / how fastcounter + rate()
current levelgauge
latency distributionhistogram + histogram_quantile()
error ratiorate(errors) / rate(total)
namingsnake_case, unit suffix, _total for counters
labelsbounded values only

🧠 Key takeaways

  • Types: counter (only up; use rate), gauge (up/down), histogram (buckets → quantiles), summary.
  • Expose /metrics and let Prometheus pull; push only for short-lived jobs (Pushgateway) or OTLP.
  • Core queries: rate(), error ratios, histogram_quantile over summed buckets.
  • Cost is driven by series count (cardinality): never label by unbounded values.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What metric types does Prometheus use?core▾

Counter (monotonically increasing, e.g. requests_total; use rate()), gauge (up/down value, e.g. memory), histogram (bucketed observations for latency percentiles, aggregatable), summary (client-side quantiles, not aggregatable).

Follow-ups interviewers ask
  • Why prefer histograms over summaries? You can aggregate histograms across instances.
Q2How does Prometheus collect metrics?core▾

It pulls (scrapes) /metrics endpoints at intervals, discovering targets via service discovery (Kubernetes), stores time series locally, and evaluates recording and alerting rules. Short-lived jobs use the Pushgateway.

Follow-ups interviewers ask
  • Long-term storage? Thanos, Cortex/Mimir, or managed services.
Q3What is cardinality and why does it matter?senior▾

The number of unique label combinations. Labels with unbounded values (user IDs, full URLs, request IDs) create millions of series, exploding memory and cost. Use bounded labels like route templates and status classes.

Follow-ups interviewers ask
  • How do you find high-cardinality metrics? Prometheus TSDB status page or count by (__name__) queries.
Q4Which metrics would you expose for an HTTP service?core▾

RED: request rate (http_requests_total by route/method/status), errors (5xx), duration histogram; plus saturation (in-flight requests, pool usage, queue depth), runtime metrics (GC, memory, threads), and business metrics.

Follow-ups interviewers ask
  • How do you compute p99 in PromQL? histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)).
Q5What's OpenTelemetry's role in metrics?core▾

A vendor-neutral API/SDK and collector for metrics (and traces/logs), letting you instrument once and export to Prometheus, Datadog, CloudWatch, etc.

Follow-ups interviewers ask
  • What's the OTel Collector? A pipeline that receives, processes and exports telemetry.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. How do you get requests per second from a counter?
rate handles resets.
2. Correct way to get fleet-wide p99?
Quantiles don't average.
3. Which label is dangerous?
Unbounded cardinality.
4. A gauge is best for…
Values that go up and down.