interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

Observability Interview Questions and Answers

20 hand-picked Observability interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

Monitoring vs observability — what's the actual difference?

Monitoring watches for failure modes you already predicted: you define dashboards and alerts for known conditions (CPU > 80%, disk full, error rate > 1%). It answers "is the system broken?"

Observability is a property of the system: whether you can understand its internal state from the data it emits — including for failures nobody anticipated. It answers "why is it broken?" without shipping new code to find out.

The practical distinction is known-unknowns vs unknown-unknowns:

  • Monitoring: "alert me when the queue is longer than 1000." You knew to watch that.
  • Observability: "latency is up only for Android users in one region on the checkout endpoint since 14:20." You couldn't have pre-built that dashboard — you needed to slice rich, high-cardinality data at query time.

Monitoring is a subset of observability, not a competitor. You need both: monitoring to page you, observability to debug.

Explain the three pillars: metrics, logs, and traces. When do you use each?

  • Metrics — numeric time-series, aggregated and cheap to store forever. Best for trends, dashboards, and alerting: request rate, error rate, p99 latency, CPU. Low cardinality by design. Answers "is something wrong, and since when?"
  • Logs — discrete timestamped events with context. Best for detailed forensics on a specific event: the exact stack trace, the exact payload that failed. Expensive at volume. Answers "what exactly happened in this case?"
  • Traces — the path of a single request across services, as a tree of timed spans. Best for latency attribution in distributed systems: which of 12 microservices ate 800ms. Answers "where is the time going?"

The workflow that ties them together: a metric alert fires → a trace shows which service and operation is slow → logs from that service and trace ID give the exact error. Linking them with a shared trace_id is what turns three data types into one investigation.

// structured log carrying the trace context — this is the glue
{
  "ts": "2026-08-05T09:14:22.481Z",
  "level": "error",
  "service": "checkout-api",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "user_id": "u_8821",
  "msg": "payment gateway timeout",
  "duration_ms": 30012,
  "upstream": "payments-svc"
}

What are the Four Golden Signals? How do RED and USE methods differ?

Four Golden Signals (Google SRE) — what to measure on any user-facing service:

  1. Latency — how long requests take. Measure successful and failed requests separately; a fast 500 skews your average and hides the problem.
  2. Traffic — demand: requests/sec, transactions/sec.
  3. Errors — rate of failed requests (explicit 5xx, and implicit — wrong content, policy violations).
  4. Saturation — how full the system is (memory, connection pool, queue depth). The leading indicator: saturation rises before latency and errors do.

RED (Rate, Errors, Duration) is the request-centric subset — the right frame for services and microservices.

USE (Utilisation, Saturation, Errors) is resource-centric — the right frame for infrastructure: CPU, memory, disk, network interfaces.

In practice: RED for every service, USE for every resource. They're complementary views, not alternatives.

# RED for a service (PromQL)
sum(rate(http_requests_total[5m])) by (service)                                    # Rate
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
  / sum(rate(http_requests_total[5m])) by (service)                                # Errors (ratio)
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))            # Duration p99

# USE for a resource
1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) by (instance)               # Utilisation
node_pressure_cpu_waiting_seconds_total                                            # Saturation
rate(node_network_receive_errs_total[5m])                                          # Errors

How does Prometheus work? Explain the pull model, exporters, and service discovery.

Prometheus scrapes (pulls) metrics over HTTP from targets exposing a /metrics endpoint in a simple text format, stores them in a local time-series database, and evaluates rules against them.

Components:

  • Prometheus server — scraper + TSDB + rule evaluator + PromQL engine.
  • Exporters — translate something that doesn't speak Prometheus into /metrics: node_exporter (host), blackbox_exporter (probes), postgres_exporter, kube-state-metrics (K8s object state), cAdvisor (container resources).
  • Service discovery — targets come from Kubernetes, EC2, Consul, or file SD rather than a static list, so autoscaled pods are scraped automatically. This is what makes pull viable at scale.
  • Alertmanager — separate component handling routing, grouping, silencing, and delivery.
  • Pushgateway — only for short-lived batch jobs that die before they can be scraped. It's an exception, not a general push path.

Why pull beats push here: Prometheus knows the intended target list, so a target that disappears is detectable (up == 0) rather than silent; you can scrape a target manually with curl to debug; and there's no risk of a misbehaving client flooding your ingestion.

scrape_configs:
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      # only scrape pods that opt in with an annotation
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
        action: keep
        regex: true
      - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
        action: replace
        target_label: __address__
        regex: (.+)
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace

rule_files: [ '/etc/prometheus/rules/*.yml' ]
alerting:
  alertmanagers:
    - static_configs: [{ targets: ['alertmanager:9093'] }]

Counter vs Gauge vs Histogram vs Summary — which do you use when?

  • Counter — monotonically increasing, only resets to 0 on restart. For things you count: requests, errors, bytes sent. Never graph a counter raw — its absolute value is meaningless; always wrap it in rate().
  • Gauge — a value that goes up and down: memory in use, queue depth, active connections, temperature. Graph it directly; use avg/max/min.
  • Histogram — samples observations into configurable buckets (plus _sum and _count). Percentiles are calculated server-side with histogram_quantile(), which means you can aggregate across instances. This is what you want for latency.
  • Summary — calculates quantiles client-side. Cheaper to query, but the quantiles cannot be aggregated across instances (you can't average p99s), so it's rarely the right choice in a distributed system.

Default to histograms for latency and size distributions, counters for events, gauges for current state.

# Counter — ALWAYS with rate()
rate(http_requests_total[5m])
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))

# Gauge — use directly
node_memory_MemAvailable_bytes
max_over_time(queue_depth[1h])

# Histogram — aggregate buckets FIRST, then compute the quantile
histogram_quantile(0.99,
  sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))
)

# average latency from a histogram
rate(http_request_duration_seconds_sum[5m])
  / rate(http_request_duration_seconds_count[5m])

Write the PromQL you'd actually use day to day. What's the difference between rate and irate?

PromQL operates on two things: instant vectors (one value per series, now) and range vectors (a window of values, e.g. [5m]). Functions like rate() turn a range vector into an instant vector.

  • rate() — average per-second rate over the window, using all data points and correcting for counter resets. Use this for alerting and dashboards — it's smooth and stable.
  • irate() — instant rate from only the last two data points. Very responsive but spiky; good for zooming into a short volatile window, bad for alerts (it will flap).
  • increase() — total increase over the window (essentially rate × seconds).

Rule of thumb: the range window should be at least 4× the scrape interval (so 15s scrape → [1m] minimum, and [5m] is the safe default). Too short and a single missed scrape produces gaps.

Aggregation order matters: always rate() first, then sum()sum before rate breaks counter-reset handling and gives wrong numbers.

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

# p95 latency per endpoint
histogram_quantile(0.95,
  sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

# top 5 memory-hungry pods
topk(5, sum by (pod) (container_memory_working_set_bytes{namespace="prod"}))

# will the disk fill in the next 4 hours?
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0

# pods restarting
increase(kube_pod_container_status_restarts_total[1h]) > 3

# a target that stopped responding
up{job="my-api"} == 0

# week-over-week comparison
sum(rate(http_requests_total[5m])) 
  / sum(rate(http_requests_total[5m] offset 7d))

Explain SLI, SLO, SLA, and error budgets.

  • SLI (Indicator) — the measurement: "proportion of HTTP requests served in under 300ms" or "successful requests ÷ total requests". A number you can actually compute from metrics.
  • SLO (Objective) — your internal target for that SLI: "99.9% of requests succeed, measured over 30 days." This is what the team commits to and what should drive alerting.
  • SLA (Agreement) — the contractual promise to customers, with financial penalties. Always set looser than your SLO, so you breach your internal target and react long before you breach the contract.

Error budget = 100% − SLO. A 99.9% SLO over 30 days allows 43.2 minutes of failure. That budget is a currency:

  • Budget remaining → ship features, take risks, deploy often.
  • Budget exhausted → freeze risky releases and spend the sprint on reliability.

This is the point of SLOs: they turn "how much reliability is enough?" from an argument into a number, and they explicitly acknowledge that 100% is the wrong target — it's infinitely expensive and prevents shipping.

# SLI: availability (success ratio) over 30 days
sum(rate(http_requests_total{status!~"5.."}[30d]))
  / sum(rate(http_requests_total[30d]))

# error budget consumed against a 99.9% SLO
(1 - (
  sum(rate(http_requests_total{status!~"5.."}[30d]))
  / sum(rate(http_requests_total[30d]))
)) / (1 - 0.999)

# multi-window burn-rate alert (fast burn — page immediately)
- alert: ErrorBudgetFastBurn
  expr: |
    (sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))) > (14.4 * 0.001)
    and
    (sum(rate(http_requests_total{status=~"5.."}[1h])) / sum(rate(http_requests_total[1h]))) > (14.4 * 0.001)
  labels: { severity: page }
  annotations:
    summary: "Burning error budget 14x — 2% of 30d budget gone in 1h"

What makes a good alert? How do you fix alert fatigue?

A good alert is urgent, actionable, and symptom-based. Before creating one, ask: if this fires at 3am, is there something a human must do right now? If not, it's a dashboard or a ticket, not a page.

Alert on symptoms, not causes. "Checkout error rate is 5%" is worth waking someone; "CPU is 85%" is not — high CPU with healthy latency and no errors is just a well-utilised server. Cause-based alerts generate noise because they fire in situations where users are fine.

Fixing alert fatigue — the failure mode where people ignore alerts because most are noise, and the one real page gets missed:

  • Delete alerts nobody acts on. Audit which alerts fired in the last 90 days and what the responder did. "Acknowledged and closed" = delete it.
  • Add for: durations so transient blips don't page.
  • Group and inhibit — one node failing shouldn't send 40 pages; suppress downstream alerts when the upstream cause is already firing.
  • Tier severity — page vs ticket vs dashboard-only.
  • Every alert links a runbook that says what to check and what to do.
  • Alert on burn rate/SLOs rather than arbitrary thresholds.
groups:
- name: service-slos
  rules:
  - alert: HighErrorRate
    expr: |
      sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
        / sum by (service) (rate(http_requests_total[5m])) > 0.02
    for: 10m                       # must persist — no blip pages
    labels:
      severity: page
      team: payments
    annotations:
      summary: "{{ $labels.service }} error rate {{ $value | humanizePercentage }}"
      runbook_url: "https://wiki/runbooks/high-error-rate"
      dashboard: "https://grafana/d/svc/{{ $labels.service }}"

# Alertmanager: group, throttle, and inhibit
route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s               # collect related alerts before sending
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: [ severity="page" ]
      receiver: pagerduty
    - matchers: [ severity="ticket" ]
      receiver: jira

inhibit_rules:
  - source_matchers: [ severity="critical", alertname="NodeDown" ]
    target_matchers: [ severity="page" ]
    equal: ['node']             # node is down → don't also page for its pods

What makes a good Grafana dashboard?

Grafana visualises data from many sources (Prometheus, Loki, Elasticsearch, CloudWatch, SQL) — it stores no metrics itself.

Principles for dashboards people actually use:

  • One dashboard, one question. "Is the checkout service healthy?" — not 60 panels of everything.
  • Structure top-down — RED signals (rate, errors, duration) at the top; resource/infra detail below. The first screen should answer "is this OK?" in three seconds.
  • Use template variables ($environment, $service, $pod) so one dashboard serves every service instead of 40 copies.
  • Percentiles, not averages — show p50/p95/p99. An average latency of 200ms hides that 1% of users wait 8 seconds.
  • Annotate deployments — overlaying deploy markers makes "what changed?" instantly visible. This is the single highest-value dashboard feature.
  • Consistent units and thresholds; add SLO lines so "good" is visible without interpretation.
  • Dashboards as code — provision JSON via Terraform/ConfigMap so they're version-controlled and reproducible, not hand-built and lost.
# provision dashboards as code (Terraform)
resource "grafana_dashboard" "service" {
  config_json = file("${path.module}/dashboards/service-red.json")
  folder      = grafana_folder.services.id
}

# template variable → one dashboard for every service
# Query variable: label_values(http_requests_total, service)
# then use $service in every panel:
#   sum(rate(http_requests_total{service="$service"}[5m]))

# panel: p50/p95/p99 on one graph
histogram_quantile(0.50, sum by (le) (rate(http_request_duration_seconds_bucket{service="$service"}[5m])))
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket{service="$service"}[5m])))
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{service="$service"}[5m])))

How do you build centralised logging? Compare ELK/EFK with Loki.

In a distributed system you can't SSH to a box and tail — pods are ephemeral and there are hundreds. You need a pipeline: collect → ship → parse → store → query.

ELK / EFK stack

  • Beats/Fluentd/Fluent Bit — agent on each node collecting container stdout.
  • Logstash (optional) — parsing, enrichment, filtering.
  • Elasticsearch — indexes the full text of every log. Powerful search, but storage- and RAM-hungry and operationally heavy.
  • Kibana — query UI.

Loki takes the opposite approach: it indexes only labels (service, namespace, level) and stores the log content compressed in object storage. Dramatically cheaper and simpler to run; queries filter by label first then grep the stream. Trade-off: full-text search across everything is slower than Elasticsearch.

Choosing: Loki if you're already on Prometheus/Grafana, want low cost, and mostly query by service + time window (which is most DevOps use). Elasticsearch if you need rich full-text search, complex aggregations, or already run it for other purposes.

# Fluent Bit → Loki, with Kubernetes metadata as labels
[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Parser            cri
    Mem_Buf_Limit     5MB

[FILTER]
    Name                kubernetes
    Match               kube.*
    Merge_Log           On            # parse JSON app logs into fields
    Keep_Log            Off

[FILTER]
    Name    grep
    Match   kube.*
    Exclude log /health|/ready       # drop probe noise before it costs money

[OUTPUT]
    Name        loki
    Match       kube.*
    host        loki.observability.svc
    labels      job=fluentbit, namespace=$kubernetes['namespace_name']

# LogQL: errors for one service, then extract a field
{namespace="prod", app="checkout"} |= "error" | json | duration_ms > 1000

What are logging best practices — structure, levels, correlation IDs, and what NOT to log?

Log structured JSON, not prose. "User 8821 failed payment after 30s" requires a regex to query; {"user_id":"8821","event":"payment_failed","duration_ms":30012} is filterable and aggregatable immediately.

Levels, used consistently:

  • ERROR — something failed and needs attention. If nobody would act on it, it isn't an error.
  • WARN — unexpected but handled (retry succeeded, fallback used).
  • INFO — significant business events (order placed, user registered). Default production level.
  • DEBUG — detailed diagnostics, off in prod (or sampled / dynamically enableable).

Correlation/trace IDs are non-negotiable in microservices. Generate an ID at the edge, propagate it through every downstream call and into every log line, so one query reconstructs the full journey of a request across ten services.

Never log: passwords, tokens, API keys, full card numbers, personal data (GDPR/PCI), or entire request bodies. Redact at the logging library level, not by hoping developers remember.

Also: write to stdout in containers (the platform handles shipping), never log inside a tight loop, and include enough context that the line is useful without the surrounding lines.

// ✅ structured, correlated, safe
logger.error({
  event: "payment_failed",
  trace_id: ctx.traceId,          // propagated from the edge
  user_id: user.id,               // an ID, not the user's email
  order_id: order.id,
  gateway: "stripe",
  duration_ms: 30012,
  error_code: "gateway_timeout"
  // NEVER: card_number, cvv, password, auth header, full request body
}, "payment gateway timeout");

// propagate the trace context downstream
await fetch(url, {
  headers: { traceparent: ctx.traceparent, "x-request-id": ctx.traceId }
});

// redact centrally so it cannot be forgotten
const redact = ["req.headers.authorization", "password", "*.card_number", "*.ssn"];

What is distributed tracing? Explain spans, context propagation, and OpenTelemetry.

Distributed tracing follows one request as it crosses service boundaries, producing a timeline that shows exactly where time was spent.

  • Trace — the whole request journey, identified by a trace_id.
  • Span — one unit of work (an HTTP handler, a DB query, a cache lookup) with a start time, duration, parent span ID, and attributes. Spans nest into a tree.
  • Context propagation — the mechanism that makes it work: the trace context travels in HTTP headers (the W3C traceparent header) or message metadata, so downstream services attach their spans to the same trace. If propagation breaks anywhere, the trace fragments — this is the most common tracing bug.

OpenTelemetry (OTel) is the vendor-neutral standard: one set of SDKs and one Collector that exports to Jaeger, Tempo, Datadog, Honeycomb — so instrumentation isn't locked to a backend. It covers traces, metrics, and logs.

Sampling is necessary at volume: head-based (decide at the start, e.g. keep 1%) is cheap but may miss the rare errors you care about; tail-based (decide after the trace completes) lets you keep 100% of slow or failed traces and 1% of the boring ones — far more useful, at the cost of buffering in the Collector.

// OpenTelemetry — auto-instrumentation plus a manual span
const tracer = opentelemetry.trace.getTracer("checkout-api");

await tracer.startActiveSpan("process_payment", async (span) => {
  span.setAttributes({
    "payment.gateway": "stripe",
    "order.id": order.id,
    "order.amount": order.total
  });
  try {
    const res = await gateway.charge(order);
    span.setStatus({ code: SpanStatusCode.OK });
    return res;
  } catch (err) {
    span.recordException(err);
    span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
    throw err;
  } finally {
    span.end();
  }
});

// W3C trace context propagated on the wire
// traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
//              ^v ^trace_id                        ^parent_span_id  ^sampled

How do you monitor a Kubernetes cluster? What do you actually alert on?

Where the data comes from:

  • node_exporter — host CPU, memory, disk, network per node.
  • cAdvisor (built into kubelet) — per-container CPU/memory/network usage.
  • kube-state-metrics — the desired vs actual state of Kubernetes objects: replicas wanted vs ready, pod phase, restart counts, job status, PVC state. This is the one people forget, and it's where most real alerts come from.
  • API server / etcd / scheduler metrics — control-plane health.
  • Usually deployed together as kube-prometheus-stack (Prometheus Operator + Grafana + Alertmanager + default rules).

What's worth paging on (symptoms, at the workload level):

  • Pods in CrashLoopBackOff, or restarting repeatedly.
  • Deployment has fewer ready replicas than desired, sustained.
  • Pods stuck Pending — unschedulable, usually resource pressure or a missing PVC.
  • Containers being OOMKilled (exit 137) — the limit is wrong or there's a leak.
  • CPU throttling — the silent killer: the app is slow but shows no errors, because it's hitting its CPU limit.
  • Node NotReady, disk/memory pressure; PVC nearly full; certificates expiring.
# pods crash-looping
increase(kube_pod_container_status_restarts_total[15m]) > 3

# deployment not fully available for 10 minutes
kube_deployment_status_replicas_available
  < kube_deployment_spec_replicas

# pods stuck Pending
sum by (namespace) (kube_pod_status_phase{phase="Pending"}) > 0

# OOMKilled containers
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1

# CPU throttling — the silent latency killer
rate(container_cpu_cfs_throttled_seconds_total[5m])
  / rate(container_cpu_cfs_periods_total[5m]) > 0.25

# memory usage close to the limit
sum by (pod) (container_memory_working_set_bytes)
  / sum by (pod) (kube_pod_container_resource_limits{resource="memory"}) > 0.9

# node problems / PVC filling up
kube_node_status_condition{condition="Ready", status="true"} == 0
kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes < 0.1

What is high cardinality and why does it break your metrics system?

Cardinality = the number of unique time series, which is the product of all label value combinations. Prometheus stores one series per unique label set, largely in memory — so cardinality is the dominant cost and stability factor.

The killer: adding a label with unbounded values.

http_requests_total{method, status, endpoint}
  = 5 × 8 × 20 = 800 series          ✅ fine

add user_id (1,000,000 users):
  = 800 × 1,000,000 = 800,000,000    💥 OOM

Never use as metric labels: user IDs, email addresses, session IDs, request IDs, trace IDs, full URLs with path parameters, timestamps, or raw error messages.

Use instead: templated routes (/orders/{id}, not /orders/8821), bounded status classes, service/namespace/region. If you need per-user detail, that belongs in logs or traces — which are designed for high-cardinality data — not metrics.

Symptoms of a cardinality problem: Prometheus memory climbing steadily, slow queries, OOM restarts, and a huge /metrics payload from one service.

# find your worst offenders
topk(10, count by (__name__)({__name__=~".+"}))     # series per metric
count({__name__=~".+"})                              # total series
topk(10, count by (job)({__name__=~".+"}))           # series per job

# ❌ unbounded label
http_requests_total{path="/orders/8821", user_id="u_8821"}

# ✅ templated + bounded
http_requests_total{route="/orders/{id}", status_class="2xx"}

# drop a runaway label at scrape time
metric_relabel_configs:
  - source_labels: [__name__]
    regex: 'expensive_metric_.*'
    action: drop
  - regex: 'user_id|session_id|request_id'
    action: labeldrop

You get paged at 2am: "checkout latency p99 above SLO". Walk me through your response.

Mitigate first, root-cause second. The goal at 2am is restoring service, not understanding it.

  1. Acknowledge the page so others know it's being handled; open an incident channel.
  2. Assess impact — how many users, which regions, is it degrading or total? This sets severity and whether to escalate.
  3. Ask "what changed?" first. Most incidents follow a change: a deploy, a config/feature-flag toggle, an infrastructure change, or a traffic shift. Check the deploy timeline against the graph — annotations make this instant.
  4. Localise with the pillars — dashboard shows which service degraded; a trace shows which operation is slow; logs from that service explain why.
  5. Check dependencies — database CPU/connections/locks, cache hit rate, downstream third-party status, saturation on queues and connection pools.
  6. Mitigate — roll back the deploy, flip the feature flag off, scale out, shed load, or fail over. Rollback is usually fastest and safest.
  7. Verify recovery against the same SLI that alerted, then communicate.
  8. Blameless postmortem — timeline, contributing factors, and action items that include "what would have detected this sooner".
# 1. what changed?
kubectl rollout history deploy/checkout -n prod
git log --oneline --since="3 hours ago"

# 2. how bad, and where?
histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket{service="checkout"}[5m])))
sum by (region) (rate(http_requests_total{service="checkout", status=~"5.."}[5m]))

# 3. saturation on dependencies
sum(pg_stat_activity_count) / sum(pg_settings_max_connections)
rate(container_cpu_cfs_throttled_seconds_total{pod=~"checkout.*"}[5m])

# 4. mitigate
kubectl rollout undo deploy/checkout -n prod
kubectl scale deploy/checkout --replicas=12 -n prod

Health checks vs monitoring — and what's the difference between liveness, readiness, and synthetic monitoring?

Health checks are how the platform decides what to do with an instance right now. Monitoring is how humans understand trends and get alerted. Different consumers, different design.

  • Liveness probe — "is this process wedged?" Failing it restarts the container. Keep it dumb and dependency-free: if you check the database here, a brief DB blip restarts every pod in your fleet and turns a small problem into an outage.
  • Readiness probe — "can this instance serve traffic right now?" Failing it removes the pod from the Service endpoints without restarting it. This is where dependency checks belong — it lets a pod temporarily step out while a dependency recovers.
  • Startup probe — for slow-booting apps; disables the other probes until the app has started, so a slow JVM boot isn't mistaken for a hang.

Synthetic / blackbox monitoring runs scripted requests against your service from outside — a probe hitting the login flow every minute from three regions. It catches what internal metrics can't: DNS failures, expired TLS certificates, CDN/load-balancer misconfiguration, and regional network issues. It's the closest signal to "can a real user actually use this?"

livenessProbe:
  httpGet: { path: /healthz, port: 8080 }   # process-only check, NO dependencies
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:
  httpGet: { path: /readyz, port: 8080 }    # checks DB/cache — safe to fail here
  periodSeconds: 5
  failureThreshold: 2

startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  failureThreshold: 30
  periodSeconds: 10                          # allows 5 minutes to boot

---
# blackbox_exporter — synthetic check from outside
modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_status_codes: [200]
      fail_if_body_not_matches_regexp: ["\"status\":\"ok\""]
# alert: probe_success == 0  |  probe_ssl_earliest_cert_expiry - time() < 14*86400

Your observability bill has tripled. How do you cut it without losing visibility?

Observability cost is driven by volume × retention × cardinality. Attack each, starting with the biggest line (almost always logs).

  • Drop noise at the collector — health-check and readiness-probe logs, verbose framework startup output, successful access logs for static assets. Filtering at the agent is free; filtering after ingestion is not.
  • Tier retention — 7–14 days hot and searchable, then compressed object storage (S3/Glacier) for the compliance tail. Most log queries look at the last 48 hours.
  • Sample — keep 100% of errors and slow requests, sample successful DEBUG/INFO. Same for traces (tail-based sampling).
  • Fix cardinality — one metric with an unbounded label can dominate a Prometheus bill; audit with topk(count by (__name__)) and labeldrop the offenders.
  • Downsample old metrics — full resolution for 15 days, 5-minute rollups for a year (Thanos/Mimir do this natively).
  • Delete unused dashboards and alerts — and the metrics only they consumed.
  • Right-size log levels in prod — DEBUG left on in one busy service is a classic cause of a sudden bill jump.
# 1. find the biggest metric offenders
topk(20, count by (__name__)({__name__=~".+"}))

# 2. drop them at scrape time
metric_relabel_configs:
  - source_labels: [__name__]
    regex: 'go_gc_.*|promhttp_.*'
    action: drop

# 3. drop log noise at the agent (before it costs anything)
[FILTER]
    Name    grep
    Match   kube.*
    Exclude log (GET /health|GET /ready|GET /metrics)

# 4. tier retention (Loki)
limits_config:
  retention_period: 336h        # 14 days hot
table_manager:
  retention_deletes_enabled: true

# 5. downsample long-term metrics (Thanos compactor)
# raw 15d → 5m downsample 90d → 1h downsample 1y

Push vs pull metrics collection — what are the trade-offs?

Pull (Prometheus): the server scrapes /metrics from a target list it maintains.

  • ✅ The server knows what should exist, so a missing target is detectable (up == 0) rather than silently absent.
  • ✅ You can curl a target's /metrics to debug it — no pipeline involved.
  • ✅ No client can overwhelm ingestion; the server controls the rate.
  • ❌ Needs network reachability to every target and service discovery to track them.
  • ❌ Poor fit for short-lived jobs that finish between scrapes.

Push (StatsD, Datadog agent, OTLP): the application sends metrics to a collector.

  • ✅ Works through NAT/firewalls and across network boundaries; good for serverless, batch jobs, and edge/IoT.
  • ✅ Naturally handles ephemeral workloads.
  • ❌ A silent client is indistinguishable from a healthy client with nothing to report — you lose "is it alive?" for free.
  • ❌ A misbehaving client can flood the collector.

Pushgateway exists only for short-lived batch jobs. Do not use it as a general push endpoint: it becomes a single point of failure, it doesn't expire stale metrics automatically, and it breaks target-health semantics.

# pull: a dead target is an alert
- alert: TargetDown
  expr: up == 0
  for: 5m

# push: only for jobs that exit before a scrape can happen
echo "backup_last_success_timestamp $(date +%s)" \
  | curl --data-binary @- http://pushgateway:9091/metrics/job/nightly-backup

# then alert on staleness, which is what you actually care about
- alert: BackupStale
  expr: time() - backup_last_success_timestamp > 26*3600
  for: 10m

API p99 latency jumped from 200ms to 3s, but error rate and CPU look normal. How do you find the cause?

Normal errors and normal CPU narrows this a lot — the system is waiting, not failing or computing. Work through it systematically:

  1. Is it all traffic or a slice? Break the p99 down by endpoint, region, customer, and pod. If p50 is unchanged but p99 exploded, a subset of requests is slow — the most common shape.
  2. What changed at that timestamp? Deploy, feature flag, config, traffic volume, or a dependency's own deploy.
  3. Where does the time go? This is exactly what tracing is for — compare a slow trace to a fast one and find the span that grew.
  4. Saturation on something invisible to CPU:
    • Connection pool exhaustion — requests queue waiting for a DB connection. Classic cause of "slow but no errors".
    • CPU throttling — the container is at its cgroup limit; host CPU looks fine.
    • Thread pool / event loop blocked, or GC pauses (check GC time metrics).
    • Lock contention or a long-running transaction in the database.
  5. Dependencies — slow downstream service, cache hit rate collapsed (so everything falls through to the DB), a third-party API degraded, DNS resolution slow.
  6. Data-shaped causes — a table grew past an index's usefulness, a missing index on a new query, an N+1 that only hurts at scale, or one large customer's dataset.
# is it everything, or one slice?
histogram_quantile(0.99, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
histogram_quantile(0.50, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))

# connection pool exhaustion — 'slow but no errors'
hikaricp_connections_pending
sum(pg_stat_activity_count) / sum(pg_settings_max_connections)

# CPU throttling despite low usage
rate(container_cpu_cfs_throttled_seconds_total[5m])
  / rate(container_cpu_cfs_periods_total[5m])

# GC pauses
rate(jvm_gc_pause_seconds_sum[5m])

# cache hit rate collapse → DB overload
rate(cache_hits_total[5m]) / (rate(cache_hits_total[5m]) + rate(cache_misses_total[5m]))

# slow queries at the source
# SELECT query, calls, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;

You're adding observability to a service that has none. What do you instrument first?

Start with the signals that answer "is it working?" and expand only when a real question demands it.

  1. RED metrics on every endpoint — request rate, error rate, and a latency histogram. This alone covers most incidents. Get it free via framework middleware or OpenTelemetry auto-instrumentation.
  2. Structured JSON logs with a trace ID on every line, written to stdout.
  3. Health endpoints/healthz (process only) and /readyz (dependencies).
  4. Outbound dependency metrics — latency and error rate for every DB query, cache call, and downstream HTTP request. "Which dependency is slow?" is the most-asked question in an incident.
  5. Saturation gauges — connection pool in use vs max, queue depth, worker pool utilisation. These are your leading indicators.
  6. A few business metrics — orders placed, payments failed, signups. These detect problems that are invisible technically: everything returns 200 but orders dropped 40%.
  7. Then tracing — auto-instrument first, add manual spans only around meaningful custom operations.

Finally, define an SLO from those metrics and alert on that, rather than on arbitrary thresholds.

// RED via OpenTelemetry auto-instrumentation — most of it for free
const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: "http://otel-collector:4318/v1/traces" }),
  metricReader: new PrometheusExporter({ port: 9464 }),   // exposes /metrics
  instrumentations: [getNodeAutoInstrumentations()]        // http, express, pg, redis
});
sdk.start();

// business metric — the one auto-instrumentation can't give you
const ordersPlaced = meter.createCounter("orders_placed_total");
const orderValue  = meter.createHistogram("order_value_rupees");

ordersPlaced.add(1, { payment_method: order.method, region: order.region });
orderValue.record(order.total, { region: order.region });

// saturation gauge — the leading indicator
meter.createObservableGauge("db_pool_in_use", (obs) =>
  obs.observe(pool.totalCount - pool.idleCount)
);