Observability Best Practices for Production Systems

By Oleksandr Andrushchenko — Published on
0 Likes
0 Dislikes
Best Practices in Observability
Best Practices in Observability

Production observability is not achieved by collecting every available log, metric, and trace. Large systems can generate enormous telemetry volumes while still leaving engineers unable to answer basic questions during an incident: which users are affected, where latency is introduced, which dependency is failing, and what changed before the problem started.

A useful observability architecture is designed around operational questions, service boundaries, failure modes, and user-visible behavior. Metrics reveal system-wide behavior, traces connect work across distributed services, and structured logs provide detailed event context. These signals become significantly more useful when they share consistent service, request, deployment, and business identifiers.

Production observability therefore requires engineering decisions about instrumentation, cardinality, sampling, retention, alerting, correlation, security, capacity, and cost. The objective is not maximum telemetry. It is enough high-quality telemetry to detect failures early, investigate them quickly, and understand system behavior without making the observability platform itself prohibitively expensive or unreliable.

Table of Contents

Challenges in Production Observability

Observability becomes harder as systems scale because telemetry volume, service count, cardinality, and operational complexity grow together. More instrumentation can improve visibility, but it also increases storage cost, query latency, collector load, and the risk of overwhelming engineers with noisy data. Distributed architectures add further challenges: trace context can break across queues or custom protocols, retries can multiply signals, asynchronous workflows make end-to-end latency harder to measure, and partial failures may appear differently across regions or replicas.

Challenges in Production Observability
Challenges in Production Observability

The practical goal is therefore not complete visibility into every event, but enough trustworthy, correlated telemetry to explain important production behavior without making the observability platform too expensive, noisy, or fragile to operate.

Design Observability Around Operational Questions

Instrumentation should begin with questions that engineers need to answer during production operation rather than with a list of everything that can technically be measured.

For a user-facing API, important questions might include:

  • Is the service available?
  • Which operations are failing?
  • How much traffic is affected?
  • Which region or deployment version is affected?
  • Where is request latency spent?
  • Which dependency is responsible for failures?
  • Are retries amplifying downstream traffic?
  • Is the service approaching a capacity limit?

For asynchronous processing, different questions matter:

  • How old is the oldest unprocessed message?
  • Is processing throughput keeping up with incoming work?
  • How many messages are being retried?
  • Are messages accumulating in a dead-letter queue?
  • Are workers blocked on external dependencies?

These questions lead naturally to telemetry:

Operational Question
        |
        v
Required Signal
        |
        +--> Metric
        |
        +--> Trace
        |
        +--> Structured Log
        |
        v
Dashboard / Query / Alert

For example, measuring only queue depth cannot answer whether a queue is unhealthy. A high-throughput system may legitimately contain hundreds of thousands of messages.

Adding message age and processing rate creates a much stronger model:

queue_depth = 800,000
oldest_message_age = 3 sec
processing_rate = 250,000/sec

Likely healthy


queue_depth = 80,000
oldest_message_age = 18 min
processing_rate = 1,200/sec

Likely unhealthy

Observability should therefore describe behavior, not simply resource counts.

Question Primary Signal Supporting Signal
Is the API failing? Error-rate metric Structured error logs
Where is latency spent? Distributed trace Dependency latency metrics
Is a queue falling behind? Oldest-message age Depth and throughput
Which version introduced errors? Version-tagged metrics Logs and deployment events
Why is the database slow? Query and pool metrics Trace spans and slow-query logs

Standardize Telemetry Across Services

Observability becomes difficult when every service uses different metric names, log fields, severity conventions, and trace attributes. Standardization allows dashboards, alerts, queries, and incident tooling to work across the entire platform.

A production telemetry contract might require every service to expose:

service.name
service.version
deployment.environment
cloud.region

trace_id
span_id

http.method
http.route
http.status_code

dependency.name
dependency.operation

Business-specific systems can add bounded domain dimensions such as:

carrier
payment_provider
workflow
message_type
operation

Consistent naming also makes cross-service dashboards possible. If one service exposes request_duration, another api_latency, and another http_time, platform-level queries become unnecessarily complicated.

Preserve Correlation Context

Distributed systems need identifiers that connect telemetry generated by different components.

Client Request
      |
 trace_id=abc123
      |
      v
 API Gateway
      |
      v
Order Service
      |
      +--------------------+
      |                    |
      v                    v
Payment Service       Inventory Service
      |                    |
      v                    v
 PostgreSQL              Redis

The same trace identifier should appear in relevant traces and logs throughout the request path.

A structured log might contain:

{
  "timestamp": "2026-08-23T15:04:17.412Z",
  "level": "ERROR",
  "service": "payment-service",
  "version": "v42",
  "region": "us-east-1",
  "trace_id": "abc123",
  "span_id": "def456",
  "operation": "authorize_payment",
  "provider": "payment-provider-a",
  "error_type": "provider_timeout",
  "duration_ms": 3002
}

A trace search can identify the failing request, and the trace_id can then retrieve detailed logs from every participating service.

For implementation details, see Structured Logging for Distributed Systems and Distributed Tracing Across Microservices.

Control Cardinality

One of the most expensive observability mistakes is putting unbounded values into metric labels.

This is dangerous:

http_requests_total{
    user_id="982741923",
    request_id="abc123",
    endpoint="/users/982741923"
}

Every unique label combination creates another time series. Millions of users or request identifiers can therefore create millions of active series.

Prefer bounded dimensions:

http_requests_total{
    service="user-service",
    route="/users/{id}",
    method="GET",
    status_class="2xx",
    region="us-east-1"
}

High-cardinality identifiers still have value, but they belong primarily in logs and traces.

Attribute Metrics Logs Traces
Service Yes Yes Yes
Region Yes Yes Yes
HTTP route template Yes Yes Yes
User ID No When justified When justified
Request ID No Yes Yes
Trace ID No Yes Native
Raw URL Usually no Carefully Carefully

Measure Service Behavior, Dependencies, and Saturation

Production telemetry should cover three layers: service outcomes, dependencies, and constrained resources.

For request-driven services, a useful baseline is:

Traffic
Errors
Latency
Saturation

Traffic describes workload. Errors describe failed outcomes. Latency describes service quality. Saturation identifies approaching capacity limits.

But saturation should measure where work actually waits:

HTTP request queue
DB connection pool
thread pool
worker pool
message queue
rate limiter
disk I/O queue

For example:

CPU = 58%
DB connections = 100 / 100
DB pool waiters = 1,420
p99 latency = 2.7 sec

CPU looks healthy, but the service is saturated on database connections.

Dependency telemetry is equally important. For every important remote dependency, track:

  • request rate;
  • latency distribution;
  • timeout rate;
  • error rate;
  • retry rate;
  • circuit-breaker state where applicable;
  • concurrency or connection utilization.

This makes it possible to distinguish:

service is slow because its own CPU is saturated

from

service is slow because it is waiting on a dependency

Metrics should emphasize signals that affect operational decisions rather than accumulating measurements simply because they are easy to expose. See Metrics That Actually Matter for a deeper treatment.

Design Telemetry for Distributed Systems

Distributed systems introduce retries, queues, caches, replicas, fan-out, asynchronous processing, and partial failures. Observability needs to expose these behaviors directly.

Consider a synchronous request:

API
 |
 +--> Service A
       |
       +--> Service B
       |     |
       |     +--> PostgreSQL
       |
       +--> Service C
             |
             +--> Redis

End-to-end latency alone cannot explain which dependency dominates the critical path. Distributed tracing provides that structure.

Now consider asynchronous processing:

API
 |
 v
Queue
 |
 +--> Worker A
 |
 +--> Worker B
 |
 +--> Worker C

Request latency no longer describes the complete workflow. Important signals include:

enqueue_rate
processing_rate
queue_depth
oldest_message_age
processing_duration
retry_rate
dead_letter_rate

Retries must also be observable as separate attempts.

Original operation
      |
      +--> Attempt 1: timeout
      |
      +--> Attempt 2: timeout
      |
      +--> Attempt 3: success

If telemetry records only the final success, a system suffering from severe dependency instability can appear healthy.

Useful counters include:

operations_total = 10,000
attempts_total = 27,000
retries_total = 17,000
timeouts_total = 8,400

The operation success rate may remain high while the retry rate reveals substantial hidden pressure.

Cache behavior should similarly include:

cache_hits
cache_misses
cache_hit_ratio
cache_load_duration
cache_evictions
cache_errors

A sudden hit-ratio reduction can explain downstream database saturation even when the cache itself reports no errors.

Sampling, Retention, and Cost Control

Telemetry volume grows with traffic, service count, event size, cardinality, and retention. Observability cost can therefore become a material infrastructure expense.

Assume a platform handles:

100,000 requests/sec

If every request generates 15 spans:

100,000 × 15
= 1,500,000 spans/sec

Storing every trace may be unnecessary and expensive.

Sampling can reduce volume:

Incoming traces
      |
      v
Sampling
      |
      +--> normal successful requests: 1%
      |
      +--> slow requests: 100%
      |
      +--> failed requests: 100%
      |
      +--> selected critical flows: 100%

Simple head sampling makes the decision when a trace starts. It is inexpensive but cannot know whether the request will later become slow or fail.

Tail sampling waits until more of the trace is available, enabling policies such as:

Keep if error = true
Keep if latency > 2 sec
Keep if critical workflow = true
Sample ordinary successful requests at 1%

The trade-off is additional collector memory, latency, and operational complexity.

Strategy Advantages Disadvantages Best Fit
Store everything Maximum diagnostic coverage High storage and ingestion cost Small systems or short retention
Head sampling Simple and inexpensive May discard interesting failures High-volume general tracing
Tail sampling Can preserve errors and slow traces Requires buffering and collectors Large production platforms
Adaptive sampling Responds to workload characteristics More difficult to reason about Very high or variable volume

Retention should also reflect operational value.

Recent high-resolution metrics
        |
        +--> 15-30 days

Detailed searchable logs
        |
        +--> shorter operational window

Archived logs
        |
        +--> cheaper storage

Detailed traces
        |
        +--> sampled, shorter retention

The exact windows depend on incident patterns, compliance requirements, traffic volume, and cost.

Telemetry cost itself should be observable:

bytes_ingested_by_service
active_metric_series
log_events_by_service
trace_spans_by_service
storage_by_signal
query_volume

This identifies services responsible for unexpected growth.

Connect Observability to Alerting and Incident Response

Observability provides evidence. Alerting decides when evidence requires action.

Page-worthy alerts should generally describe meaningful service degradation:

availability below objective
error budget burning rapidly
critical API error rate elevated
queue processing delay exceeds objective
critical dependency unavailable

Infrastructure signals such as CPU, memory, and connection utilization are often better as diagnostic context unless they reliably predict imminent user impact.

Alerts should include enough information to start investigation immediately:

Service: booking-service
Region: us-east-1
Severity: page

Booking p95:
8 min

Success rate:
64%

Queue oldest message:
11 min

Recent deployment:
v74

Dashboard:
...

Runbook:
...

During an incident, responders should be able to move through correlated telemetry:

Alert
  |
  v
Dashboard
  |
  v
Affected service / region / version
  |
  v
Example failing trace
  |
  v
Related structured logs
  |
  v
Dependency / resource metrics
  |
  v
Root cause hypothesis

For pipeline architecture, grouping, routing, inhibition, and noise control, see Designing Monitoring and Alerting Pipelines.

Observability Failure Scenarios

The observability platform is itself a distributed production system. It needs monitoring, capacity planning, redundancy, and failure handling.

Collector failure. Applications may continue operating while telemetry disappears. Collection success, export failures, and dropped telemetry should therefore be monitored independently.

Backend unavailable. Agents and collectors need bounded buffering. Unlimited local buffering can fill disks and create a second outage.

Application
    |
    v
Collector
    |
 backend unavailable
    |
    v
Bounded buffer
    |
    +--> backend recovers --> flush
    |
    +--> buffer full --> drop by policy

Telemetry storm. An exception loop can generate millions of identical logs. Rate limiting, aggregation, and per-service ingestion limits prevent one workload from consuming the entire platform.

Cardinality explosion. A deployment may accidentally introduce user_id, full URLs, or request identifiers into metric labels. Active-series monitoring and label policies should detect the growth before storage and query performance collapse.

Trace sampling hides an incident. Uniform low-rate sampling may discard rare failures. Error-aware or tail-based sampling can preserve diagnostically important traces.

Storage fills. Retention enforcement and capacity alerts should activate well before hard limits. Telemetry loss during an incident significantly increases recovery time.

Regional network partition. A centralized telemetry backend may lose visibility into one region. Regional buffering or collection tiers can preserve data until connectivity returns.

Observability backend slows down. Expensive high-cardinality queries can compete with ingestion. Query isolation, limits, recording rules, and separate capacity for interactive investigation reduce this risk.

Production Design Example

Consider a multi-service logistics platform that calculates shipping rates, creates shipments, and asynchronously books them with external carriers.

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

The observability architecture separates application instrumentation from telemetry storage:

Applications / Workers
        |
        +--> Metrics
        +--> Logs
        +--> Traces
        |
        v
Telemetry Collectors
   /        |        \
  v         v         v
Metrics    Logs     Traces
Backend   Backend   Backend
  |         |         |
  +---------+---------+
            |
            v
     Dashboards / Queries
            |
            +--> Alert Evaluation
            |
            +--> Incident Investigation

Request flow. Every synchronous request carries a trace context through the gateway and downstream services. Metrics aggregate traffic, latency, and errors by service, route, region, and bounded dependency dimensions.

Write flow. When a shipment is created, logs contain the workflow identifier and trace context. The enqueue operation creates a trace span, while queue metrics record publication rate and failures.

Asynchronous flow. Booking workers propagate the workflow context from the message. Processing duration, retries, carrier latency, and queue age are recorded separately.

Suppose Carrier A becomes slow:

Carrier A latency
120 ms --> 6 sec
      |
      v
worker attempts become slower
      |
      v
retry rate increases
      |
      v
worker throughput falls
      |
      v
queue oldest-message age rises
      |
      v
booking SLO degrades

Metrics reveal the scope:

carrier_a_p99 = 6.2 sec
carrier_a_timeout_rate = 31%
booking_retry_rate = 44%
booking_queue_age = 9 min

A trace reveals the request behavior:

booking.process                 14.4 sec
 |
 +-- carrier-a attempt 1         5.0 sec timeout
 |
 +-- retry backoff               1.0 sec
 |
 +-- carrier-a attempt 2         5.0 sec timeout
 |
 +-- retry backoff               2.0 sec
 |
 +-- carrier-a attempt 3         1.4 sec success

Structured logs provide detailed carrier responses and retry decisions.

Failure flow. If Carrier A remains degraded, the system should expose circuit-breaker state, rejected requests, retry behavior, queue growth, and any traffic routed to alternatives. The telemetry should show both the initiating dependency problem and its propagation through the booking pipeline.

Scaling. Observability capacity is planned separately for metrics samples per second, active series, log bytes per second, trace spans per second, and query workload. Traffic growth does not automatically translate equally across these dimensions.

Deployment. Service version is attached as a bounded telemetry dimension so canary and stable versions can be compared during rollout. Deployment events are available on operational dashboards.

Monitoring the monitoring system. Collectors expose queue utilization, dropped telemetry, export latency, backend errors, and memory utilization. Storage systems expose ingestion rate, active series, storage utilization, compaction health, and query latency.

Ready-to-Use Example

A production service can standardize metric instrumentation so every endpoint exposes consistent request, error, and latency signals.

from time import perf_counter
from typing import Callable

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


app = FastAPI()

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

REQUEST_DURATION = Histogram(
    "http_request_duration_seconds",
    "HTTP request duration",
    ["service", "method", "route"],
)

SERVICE_NAME = "shipment-service"


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

    response = await call_next(request)

    route = request.scope.get("route")
    route_template = (
        getattr(route, "path", "unknown")
        if route is not None
        else "unknown"
    )

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

    REQUESTS.labels(
        service=SERVICE_NAME,
        method=request.method,
        route=route_template,
        status_class=status_class,
    ).inc()

    REQUEST_DURATION.labels(
        service=SERVICE_NAME,
        method=request.method,
        route=route_template,
    ).observe(perf_counter() - started)

    return response

The route template is used instead of the raw URL. A request to /shipments/918273 therefore contributes to /shipments/{id} rather than creating a high-cardinality metric dimension.

Structured logging should use the same service and correlation context:

import json
import logging
from dataclasses import asdict, dataclass
from datetime import datetime, timezone


logger = logging.getLogger("shipment-service")


@dataclass(frozen=True)
class LogContext:
    service: str
    version: str
    region: str
    trace_id: str


def log_carrier_timeout(
    context: LogContext,
    carrier: str,
    duration_ms: int,
) -> None:
    payload = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "level": "ERROR",
        **asdict(context),
        "event": "carrier_timeout",
        "carrier": carrier,
        "duration_ms": duration_ms,
    }

    logger.error(json.dumps(payload))

For asynchronous workloads, instrumentation should capture backlog age rather than queue depth alone:

from prometheus_client import Counter, Gauge, Histogram


BOOKINGS_PROCESSED = Counter(
    "booking_messages_processed_total",
    "Booking messages processed",
    ["carrier", "result"],
)

BOOKING_DURATION = Histogram(
    "booking_processing_duration_seconds",
    "Booking processing duration",
    ["carrier"],
)

BOOKING_QUEUE_AGE = Gauge(
    "booking_queue_oldest_message_seconds",
    "Age of the oldest unprocessed booking message",
)

BOOKING_RETRIES = Counter(
    "booking_retries_total",
    "Booking retry attempts",
    ["carrier", "reason"],
)

A production alert can then operate on user-relevant delay:

groups:
  - name: booking-pipeline

    rules:
      - alert: BookingPipelineDelayed

        expr: |
          booking_queue_oldest_message_seconds > 300

        for: 5m

        labels:
          severity: page
          team: logistics-platform

        annotations:
          summary: "Booking pipeline delayed for more than 5 minutes"
          runbook: "/runbooks/booking-pipeline-delay"

The observability platform itself should expose capacity signals:

telemetry:
  collectors:
    monitor:
      - queue_utilization
      - export_error_rate
      - dropped_spans
      - dropped_logs
      - memory_utilization

  metrics_backend:
    monitor:
      - samples_ingested_per_second
      - active_series
      - query_latency
      - storage_utilization

  logs_backend:
    monitor:
      - bytes_ingested_per_second
      - rejected_events
      - query_latency
      - storage_utilization

  traces_backend:
    monitor:
      - spans_ingested_per_second
      - spans_dropped
      - sampling_rate
      - storage_utilization

This closes an important operational gap: observability cannot be trusted unless the telemetry pipeline itself is observable.

Common Mistakes

Mistake Production Impact Better Approach
Collecting everything High cost with little improvement in incident response. Instrument around operational questions and failure modes.
Using different conventions per service Cross-service queries and dashboards become difficult. Define a shared telemetry contract.
Using raw URLs as metric labels Metric cardinality grows with resource identifiers. Use route templates.
Putting user IDs in metrics Millions of time series can be created. Keep high-cardinality identifiers in logs or traces.
Logging unstructured strings Queries require parsing inconsistent text. Use structured fields and stable event names.
Missing trace context in logs Request-level investigation requires manual correlation. Include trace and span identifiers.
Monitoring only averages Tail latency and localized failures remain hidden. Use distributions and percentiles.
Monitoring queue depth only Healthy large queues and unhealthy small queues look similar. Track age and throughput.
Ignoring retries Dependency instability and traffic amplification remain hidden. Measure attempts, retries, timeouts, and outcomes.
Monitoring CPU but not waiting work Connection or worker saturation can be missed. Instrument constrained pools and queues.
Sampling all traces uniformly Rare failures may disappear from trace storage. Prefer error-aware or tail sampling where justified.
Keeping the same retention for everything Storage cost grows without proportional operational value. Set retention by signal value and resolution.
Ignoring telemetry cost by service One workload can unexpectedly dominate the observability bill. Measure ingestion and storage ownership.
Paging on every anomaly Alert fatigue reduces trust in the system. Page primarily on actionable service impact.
Not monitoring collectors Missing telemetry may look like a healthy system. Monitor export failures, queues, and dropped data.
Unlimited telemetry buffering Backend outages can fill application or node disks. Use bounded buffers and explicit drop policies.

Production Checklist

  • Define operational questions: know which questions telemetry must answer during incidents.
  • Standardize service names: use the same identity across metrics, logs, and traces.
  • Record deployment versions: make regressions comparable between releases.
  • Record regions: make localized failures immediately visible.
  • Use route templates: never use resource-specific URLs as metric dimensions.
  • Control metric cardinality: review every new label for bounded values.
  • Propagate trace context: preserve correlation across synchronous service calls.
  • Propagate context through queues: maintain workflow correlation across asynchronous boundaries.
  • Include trace IDs in logs: support direct trace-to-log navigation.
  • Use structured logs: keep event names and field types consistent.
  • Protect sensitive data: prevent credentials, tokens, and unnecessary personal data from entering telemetry.
  • Measure traffic: establish workload and throughput baselines.
  • Measure errors: distinguish expected failures from system failures.
  • Measure latency distributions: include tail latency rather than averages alone.
  • Measure saturation: monitor queues, pools, concurrency, and waiting work.
  • Measure dependencies: track downstream latency, errors, and timeouts.
  • Measure retries: detect hidden amplification.
  • Measure queue age: identify asynchronous processing delays.
  • Measure processing throughput: compare incoming and completed work.
  • Measure dead-letter traffic: detect permanently failed asynchronous work.
  • Measure cache effectiveness: track hit ratio, load latency, and errors.
  • Choose trace sampling deliberately: preserve diagnostically important traffic.
  • Set retention intentionally: match retention to operational and compliance value.
  • Track telemetry volume by service: make cost ownership visible.
  • Monitor active metric series: detect cardinality explosions early.
  • Monitor collector queues: identify telemetry backpressure.
  • Monitor dropped telemetry: make data loss explicit.
  • Use bounded buffers: prevent observability failures from exhausting application resources.
  • Monitor observability storage: alert before hard capacity limits.
  • Monitor query latency: ensure telemetry remains usable during incidents.

Conclusion

Effective production observability comes from deliberate instrumentation rather than maximum telemetry volume. Metrics should expose service outcomes and saturation, traces should reveal distributed request paths, and structured logs should preserve detailed context. Consistent correlation fields allow those signals to function as one investigation system rather than three independent data stores.

The architecture also has practical limits. High-cardinality metrics can overwhelm storage, excessive logging can dominate infrastructure cost, full trace collection can become impractical at scale, and observability backends can fail during the same traffic spikes they are expected to diagnose. Sampling, retention policies, bounded dimensions, backpressure, and capacity planning are therefore part of observability design.

Key Takeaway: Design observability around the questions needed to operate the system. Standardize telemetry across services, correlate metrics, traces, and logs, measure user-visible outcomes and waiting work, expose retries and dependencies, control cardinality and retention, and treat the observability pipeline itself as a production system with explicit reliability, capacity, security, and cost constraints.

Comments (0)