Metrics That Actually Matter

By Oleksandr Andrushchenko — Published on
0 Likes
0 Dislikes
Metrics That Actually Matter
Metrics That Actually Matter

Production systems can expose thousands of metrics while still failing to answer the most important operational question: is the system working correctly for users? CPU usage, memory consumption, thread counts, cache statistics, database metrics, and container telemetry all provide useful context, but collecting everything does not automatically create useful monitoring.

The strongest metric strategy starts with system behavior and works inward. Request traffic, errors, latency, and saturation reveal whether a service is healthy; dependency and infrastructure metrics then explain why that behavior is changing. This keeps dashboards and alerts focused on production outcomes rather than arbitrary measurements.

Metrics also have architectural costs. Cardinality consumes memory and storage, incorrect aggregation hides failures, averages conceal tail latency, and badly designed alerts create operational noise. Production metric design therefore requires the same attention to scalability, failure modes, and cost as the application being monitored.

Table of Contents

Metrics Should Answer Production Questions

A metric is useful when it helps answer a production question or supports a decision. Recording a measurement simply because it is available usually produces dashboards full of data that nobody uses during an incident.

Useful questions include:


Are users successfully completing requests?

Is the service becoming slower?

Which operation is failing?

Which region is affected?

Is a dependency degrading?

Is the queue accumulating work?

Is the database approaching capacity?

Did the latest deployment change behavior?

How much traffic can the current capacity handle?

This creates a hierarchy of signals:


                 User Experience
                       |
                       v
          Traffic / Errors / Latency
                       |
                       v
               Service Saturation
                       |
                       v
              Dependencies
                       |
                       v
              Infrastructure

Metrics near the top describe symptoms experienced by users. Metrics near the bottom usually help diagnose causes.

For example:


SYMPTOM

checkout p99 latency = 2.8 sec
           |
           v
CAUSE INDICATOR

db_pool_waiters = 1,920
           |
           v
RESOURCE CONDITION

database connections = 100%

Alerting only on the database connection count can create noise because high utilization is not necessarily a problem. Alerting on degraded checkout latency identifies a user-visible symptom; connection-pool metrics then explain the cause.

This symptom-versus-cause distinction is central to practical production monitoring. Google SRE similarly recommends focusing user-facing monitoring on latency, traffic, errors, and saturation, while using lower-level signals to understand causes. :contentReference[oaicite:0]{index=0}

Four Core Service Signals

Most request-driven services need a small group of metrics before they need hundreds of infrastructure measurements. Traffic, errors, latency, and saturation provide a useful baseline because together they describe demand, correctness, performance, and remaining capacity.

Signal Production Question Typical Metrics
Traffic How much work is entering the system? Requests/sec, messages/sec, jobs/sec
Errors How much work is failing? Error rate, rejected requests, failed jobs
Latency How long does work take? p50, p95, p99 duration
Saturation How close is the system to its limits? Queue age, pool utilization, CPU, memory

Traffic

Traffic measures demand placed on the system. The correct unit depends on the workload.


API:
requests / second

Message consumer:
messages / second

Payment platform:
payments / second

Logistics platform:
shipments / minute

Database:
queries / second

Streaming pipeline:
events / second

Traffic should normally be segmented by bounded dimensions that materially change system behavior:


service
operation
region
status
version

Raw request rate alone can be misleading when requests have different computational costs. Ten thousand lightweight reads may consume fewer resources than one thousand expensive search requests.

Production systems should therefore identify the unit of work that best represents actual demand.

Advantages:

  • provides the denominator for error rates;
  • supports capacity planning;
  • reveals traffic shifts between regions and operations;
  • helps correlate resource saturation with workload growth.

Disadvantages:

  • request counts may hide workload complexity;
  • too many dimensions create cardinality problems;
  • traffic alone says nothing about correctness or performance.

Errors

Error metrics should represent failed outcomes rather than only exceptions.

A request can fail without throwing an exception:


HTTP 429
HTTP 503
payment declined because dependency unavailable
queue message moved to DLQ
request rejected by overload protection
deadline exceeded
partial response returned

Absolute error counts are useful, but error rate usually provides better context:


errors
---------------- = error rate
total requests

Consider:


Service A:
100 failures / 10,000,000 requests
= 0.001%

Service B:
100 failures / 1,000 requests
= 10%

The same number of errors represents radically different production conditions.

Error metrics should also distinguish failures by meaningful bounded categories:


timeout
dependency_error
validation_error
rate_limited
internal_error
overloaded

Do not put raw exception messages into metric attributes. Exception messages can contain IDs, URLs, dynamic values, or user input and therefore create uncontrolled cardinality.

Latency

Latency is a distribution, not a single number.

An average can hide the requests that matter most operationally:


9,900 requests = 50 ms
  100 requests = 5 sec

The average is approximately 100 ms, which looks reasonable. The tail tells a different story:


p50 = 50 ms
p95 = 50 ms
p99 = 5 sec

Production latency monitoring should therefore use histograms or another distribution-preserving representation rather than recording only averages. Histograms aggregate populations of measurements while retaining enough information to analyze their distribution. :contentReference[oaicite:1]{index=1}

Latency should also be separated by outcome when useful:


Successful request:
2.4 sec

Failed validation:
4 ms

If both are aggregated together, a spike in fast failures can make average latency improve while the service is becoming less reliable.

Advantages:

  • directly represents user experience;
  • tail latency exposes saturation early;
  • supports SLOs and regression detection;
  • helps compare deployments and regions.

Disadvantages:

  • histograms require sensible bucket or aggregation design;
  • percentiles become noisy with very small sample sizes;
  • high-dimensional latency metrics can become expensive.

Saturation

Saturation measures how close a constrained resource is to preventing additional work from being processed efficiently.

CPU utilization is one example, but many application bottlenecks appear elsewhere:


database connection pool
thread pool
worker concurrency
queue backlog
queue oldest-message age
memory
disk capacity
network bandwidth
file descriptors
rate-limit quota
external API concurrency

The most useful saturation metric is often the waiting work, not just resource utilization.


Database pool:

active connections = 100 / 100
waiting requests = 0

vs

active connections = 100 / 100
waiting requests = 2,400

Both report 100% utilization, but only the second clearly demonstrates insufficient capacity.

For asynchronous systems, queue age is often more operationally meaningful than queue depth:


Queue A:
100,000 messages
oldest = 4 sec

Queue B:
2,000 messages
oldest = 45 min

Queue A may simply process enormous throughput. Queue B is clearly failing to keep up.

Google's SRE guidance similarly treats saturation as a measure of constrained resources and notes that latency increases can be an early indicator of approaching saturation. :contentReference[oaicite:2]{index=2}

Application vs Infrastructure Metrics

Infrastructure metrics are important, but they should not replace application metrics. A service can have healthy CPU and memory while every request fails because a dependency is unavailable.


CPU                 42%
Memory              58%
Disk                31%

Looks healthy
     |
     v

Payment success rate
     12%

System is broken

A useful monitoring hierarchy separates three layers:

Layer Examples Purpose
User / business Checkout success, booking success, notifications delivered Measure whether the system fulfills its purpose
Application RPS, latency, errors, queue age, dependency latency Measure service behavior
Infrastructure CPU, memory, disk, network, connections Explain resource conditions

All three are useful, but they answer different questions.

For a payment service:


BUSINESS

payment_success_rate
        |
        v
APPLICATION

payment_request_p99
provider_error_rate
db_pool_waiters
        |
        v
INFRASTRUCTURE

cpu_utilization
memory_usage
network_errors

Alerts should generally begin as close to the user-visible symptom as practical. Lower-level metrics then support diagnosis and capacity prediction.

Counters, Gauges, and Histograms

Metric type should match the behavior being measured. Using the wrong representation can make queries confusing or produce misleading results.

Counters represent values that accumulate:


requests_total
payments_failed_total
bytes_sent_total
jobs_completed_total

Operational queries normally calculate their rate of change:


requests_total

1000 --> 1600 over 60 sec

request rate = 10/sec

Gauges represent a current value:


active_connections
queue_depth
memory_bytes
workers_running

Histograms represent distributions:


request_duration
database_query_duration
message_processing_duration
request_size
response_size

OpenTelemetry's metric model provides counters, up/down counters, gauges, and histograms for these different measurement semantics. :contentReference[oaicite:3]{index=3}

Metric Type Best For Common Mistake
Counter Events accumulating over time Using it for a value that decreases
Gauge Current state Trying to derive event throughput from snapshots
Histogram Latency and size distributions Replacing the distribution with an average

Metrics should describe stable concepts. A metric named around one implementation detail may become useless when the implementation changes, while a metric representing a service outcome remains valuable.

Metric Cardinality and Dimensional Design

Cardinality is one of the most important scalability properties of a metrics system. Each unique combination of metric attributes can create another time series that must be maintained, stored, and queried.

Suppose an HTTP metric contains:


method:
5 values

route:
100 values

status:
10 values

region:
4 values

version:
3 values

The theoretical number of combinations is:


5 × 100 × 10 × 4 × 3
= 60,000 time series

Add a million possible user IDs:


60,000 × 1,000,000

= potentially enormous cardinality

Actual combinations may be lower, but the dimensional model is fundamentally unsafe.

OpenTelemetry describes cardinality as the number of unique attribute combinations associated with a metric and notes that each combination requires separate aggregation state. High-cardinality values such as user IDs or raw URL paths can therefore create substantial memory and storage pressure. :contentReference[oaicite:4]{index=4}

Good metric attributes are normally bounded:


GOOD

method = GET | POST | PUT | DELETE

region = us-east | us-west | eu-west

status_class = 2xx | 4xx | 5xx

operation = create_shipment | get_shipment

Dangerous attributes are usually unbounded:


BAD

user_id
request_id
trace_id
order_id
shipment_id
raw_url
exception_message
SQL query text

Those values belong naturally in logs or traces, where high-cardinality event context is expected.

Modern metric SDKs may enforce cardinality limits to protect application memory, but hitting the limit can degrade dimensional accuracy. In OpenTelemetry, overflowed combinations can be aggregated into an overflow data point; totals remain available, while queries grouped or filtered by the discarded attributes can undercount. :contentReference[oaicite:5]{index=5}

That means cardinality protection is a safety mechanism, not a substitute for good metric design.

Metrics During Production Failures

Metrics should be designed around how systems actually fail. The most useful signals often describe queues, waiting work, dependency behavior, and successful outcomes rather than only infrastructure utilization.

Database slowdown.


request_p99
220 ms --> 2.4 sec

db_query_p99
40 ms --> 70 ms

db_pool_wait_p99
5 ms --> 1.9 sec

The database queries themselves remain reasonably fast. The actual bottleneck is acquiring connections.

Worker capacity failure.


incoming messages = 12,000/sec
processed messages = 9,000/sec

queue depth:
10K --> 500K

oldest message:
2 sec --> 11 min

Queue age demonstrates the user-visible processing delay more clearly than CPU alone.

Dependency outage with retries.


incoming API traffic = stable

provider requests:
8,000/sec --> 21,000/sec

provider error rate:
0.2% --> 34%

retry rate:
0.1% --> 162%

The retry metric reveals traffic amplification that could turn a dependency failure into a cascading failure.

Deployment regression.


version=v71
p99=240 ms
errors=0.3%

version=v72
p99=1.8 sec
errors=4.9%

Deployment version should be a bounded dimension so releases can be compared directly during rollout.

Monitoring pipeline failure. Missing data is not automatically zero. Dashboards and alerts must distinguish a healthy zero from telemetry that stopped arriving.

Production Design Example

Consider a logistics platform where shipment requests trigger rating and carrier booking operations.


                         Clients
                            |
                            v
                       API Gateway
                            |
                            v
                    Shipment Service
                     /            \
                    v              v
             Rating Service   Booking Service
                    |              |
                    v              v
                 Cache          Queue
                                   |
                                   v
                            Booking Workers
                                   |
                                   v
                              Carrier APIs
                                   |
                                   v
                              PostgreSQL

The metric strategy starts at the user-facing boundary.

Shipment API:


shipment_requests_total
shipment_request_duration
shipment_errors_total
active_requests

Rating:


rating_requests_total
rating_duration
rating_errors_total
cache_hit_ratio
carrier_rate_request_duration

Booking:


booking_requests_total
booking_success_total
booking_failure_total
booking_duration
booking_retry_total

Queue:


booking_queue_depth
booking_queue_oldest_message_seconds
booking_messages_processed_total
booking_messages_failed_total

Database:


db_pool_active
db_pool_waiters
db_pool_wait_duration
db_query_duration
db_errors_total

Suppose carrier A begins responding slowly.


carrier latency increases
          |
          v
worker throughput falls
          |
          v
queue age increases
          |
          v
booking completion latency increases
          |
          v
customer-facing SLO degrades

The metrics show the failure propagating through the architecture.

Capacity planning can use the same signals. Assume peak workload is:


12,000 bookings/sec

worker throughput:
80 bookings/sec/worker

The theoretical minimum is:


12,000 / 80
= 150 workers

Running exactly 150 workers leaves no room for bursts, failures, slower carriers, deployments, or uneven workload distribution.

At 30% headroom:


150 × 1.30
= 195 workers

Production capacity should still be validated with queue age and latency rather than assuming throughput remains linear as concurrency increases.

Monitoring. Alerts focus on booking success rate, user-facing latency, and queue age. CPU, database pools, carrier latency, retries, and worker utilization provide diagnostic context.

Scaling. Worker autoscaling should consider backlog and queue age in addition to CPU because I/O-bound workers can be overloaded while CPU remains low.

Deployment. Service version is included as a bounded metric dimension during rollout so new and old instances can be compared.

Ready-to-Use Example

A FastAPI service can instrument a small set of high-value metrics without exposing request-specific identifiers as dimensions.

import time
from dataclasses import dataclass

from fastapi import FastAPI, Request, Response
from prometheus_client import Counter, Gauge, Histogram

app = FastAPI()


@dataclass(frozen=True)
class MetricLabels:
    service: str
    region: str


LABELS = MetricLabels(
    service="shipment-service",
    region="us-east",
)


REQUESTS = Counter(
    "http_requests_total",
    "Total HTTP requests",
    ["service", "region", "method", "route", "status_class"],
)

REQUEST_DURATION = Histogram(
    "http_request_duration_seconds",
    "HTTP request duration",
    ["service", "region", "method", "route"],
    buckets=(
        0.025,
        0.05,
        0.1,
        0.25,
        0.5,
        1.0,
        2.5,
        5.0,
    ),
)

ACTIVE_REQUESTS = Gauge(
    "http_active_requests",
    "Currently executing HTTP requests",
    ["service", "region"],
)


@app.middleware("http")
async def metrics_middleware(
    request: Request,
    call_next,
) -> Response:
    started = time.perf_counter()

    ACTIVE_REQUESTS.labels(
        service=LABELS.service,
        region=LABELS.region,
    ).inc()

    try:
        response = await call_next(request)

        status_class = f"{response.status_code // 100}xx"

        REQUESTS.labels(
            service=LABELS.service,
            region=LABELS.region,
            method=request.method,
            route=request.url.path,
            status_class=status_class,
        ).inc()

        return response

    finally:
        duration = time.perf_counter() - started

        REQUEST_DURATION.labels(
            service=LABELS.service,
            region=LABELS.region,
            method=request.method,
            route=request.url.path,
        ).observe(duration)

        ACTIVE_REQUESTS.labels(
            service=LABELS.service,
            region=LABELS.region,
        ).dec()

There is one important production issue in this implementation: request.url.path may contain dynamic identifiers.


/shipments/1001
/shipments/1002
/shipments/1003
...

That creates one label value per shipment. Metrics should use the normalized route template instead:


/shipments/{shipment_id}

The production implementation should therefore record framework route names or normalized route templates rather than raw URLs.

Business outcomes should also have dedicated counters:

BOOKINGS = Counter(
    "shipment_bookings_total",
    "Shipment booking outcomes",
    ["carrier", "result"],
)


def record_booking(
    carrier: str,
    success: bool,
) -> None:
    BOOKING_RESULT = (
        "success"
        if success
        else "failure"
    )

    BOOKINGS.labels(
        carrier=carrier,
        result=BOOKING_RESULT,
    ).inc()

The carrier label is appropriate only if the set of carriers is reasonably bounded. Shipment ID would not be appropriate.

Queue monitoring should expose both depth and age:

from dataclasses import dataclass


@dataclass(frozen=True)
class QueueState:
    depth: int
    oldest_message_age_seconds: float


QUEUE_DEPTH = Gauge(
    "booking_queue_depth",
    "Number of pending booking messages",
)

QUEUE_AGE = Gauge(
    "booking_queue_oldest_message_seconds",
    "Age of the oldest pending booking message",
)


def record_queue_state(state: QueueState) -> None:
    QUEUE_DEPTH.set(state.depth)
    QUEUE_AGE.set(
        state.oldest_message_age_seconds
    )

Depth describes accumulated work; age describes how long that backlog affects processing latency. Monitoring both prevents misleading conclusions for naturally high-volume queues.

Common Mistakes

Mistake Production Impact Better Approach
Collecting metrics without operational questions Dashboards become large but provide little diagnostic value. Start from user and system questions.
Monitoring only CPU and memory User-facing failures can remain invisible. Start with traffic, errors, latency, and saturation.
Using average latency Tail latency disappears. Use histograms and percentiles.
Mixing successful and failed latency blindly Fast failures can make latency appear healthier. Segment outcomes when interpreting latency.
Alerting on absolute error counts Normal traffic growth produces misleading alerts. Use error rates and SLO impact where appropriate.
Using request IDs as labels Cardinality explodes. Keep request IDs in logs and traces.
Using raw URLs as labels Dynamic path parameters create unbounded series. Use normalized route templates.
Using exception messages as labels Dynamic text creates uncontrolled cardinality. Use bounded error categories.
Monitoring queue depth without queue age High-throughput healthy queues can look overloaded. Measure oldest-message age as well.
Ignoring retry traffic Retry amplification remains invisible. Measure attempts, retries, and original operations separately.
Treating missing telemetry as zero Monitoring failures can look like healthy inactivity. Detect missing or stale series explicitly.
Creating one metric per business entity Metric storage scales with customers or objects. Aggregate outcomes and use logs for entity-level debugging.
Alerting on every internal anomaly On-call engineers become desensitized to alerts. Page primarily on urgent, actionable service impact.
Ignoring metric pipeline health Dashboards silently become incomplete. Monitor ingestion, dropped series, scrape failures, and cardinality overflow.

Production Checklist

  • Measure traffic: identify the workload unit that represents actual system demand.
  • Measure errors: include failed outcomes beyond exceptions.
  • Calculate error rates: relate failures to total traffic.
  • Measure latency distributions: preserve p50, p95, and p99 behavior.
  • Separate important outcomes: avoid hiding fast failures inside successful latency.
  • Measure saturation: track constrained resources and waiting work.
  • Monitor queue age: do not rely on queue depth alone.
  • Monitor connection-pool waiting: pool utilization alone can miss contention severity.
  • Measure dependency latency: identify external bottlenecks independently from application latency.
  • Measure dependency errors: distinguish internal failures from downstream failures.
  • Measure retries: detect traffic amplification during incidents.
  • Measure business outcomes: track critical operations such as successful bookings or payments.
  • Use bounded labels: review every metric dimension for expected cardinality.
  • Normalize routes: never expose dynamic IDs through URL labels.
  • Keep IDs out of metrics: request, trace, user, shipment, and order IDs belong elsewhere.
  • Use stable error categories: avoid exception messages as dimensions.
  • Track service versions: compare rollout behavior between deployments.
  • Track regions: make localized failures visible.
  • Monitor cardinality: detect unexpected series growth.
  • Monitor metric overflow: ensure cardinality limits are not silently degrading dimensional queries.
  • Detect stale telemetry: distinguish missing metrics from legitimate zero values.
  • Monitor ingestion health: track exporter failures, rejected data, and collection lag.
  • Load-test metrics: validate instrumentation under production-level cardinality and throughput.
  • Review dashboard usage: remove metrics and panels that no longer support operational decisions.
  • Review alerts: every page should correspond to an expected human action.
  • Use metrics for capacity planning: correlate throughput with latency and saturation.
  • Preserve capacity headroom: do not size production systems only for observed average traffic.
  • Track telemetry cost: measure storage and ingestion growth caused by new dimensions.

Conclusion

Useful production metrics describe system behavior rather than merely exposing everything that can be measured. Traffic shows demand, errors show correctness, latency shows performance, and saturation shows how close the system is to its limits. Business and application signals identify symptoms, while dependency and infrastructure metrics provide the context needed to diagnose them.

The difficult engineering work is choosing representations and dimensions that remain meaningful at scale. Histograms preserve latency distributions, bounded labels make segmentation practical, queue age exposes processing delay, and carefully selected saturation metrics reveal bottlenecks before resources completely fail.

Key Takeaway: Measure outcomes first and causes second. Start with traffic, errors, latency, and saturation, add business metrics for critical workflows, and use infrastructure metrics to explain degraded behavior. Keep dimensions bounded, preserve latency distributions, measure waiting work and retries, and treat metric cardinality and telemetry health as production capacity concerns.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)