HomeOperationsObservability
Day 42 — knowing what your systems are doing before a customer tells you

Observability

A cluster that runs your apps is not the same as a cluster you can trust — the difference is whether you can answer "is it healthy right now, and why not" without guessing. This chapter rebuilds Day 42 into a workshop: first the idea of observability and why Prometheus became the default for it, then hands-on standing up Prometheus and Grafana on a real cluster, then the interview questions the instructor flags, and finally the senior scenarios where a missing scrape config or a silent alert is yours to diagnose.

① Theory② Lab③ Interview Q&A④ Scenario drills
Day 42 · Prometheus & Grafana — monitoring Kubernetes end to end
⚡ The whole of observability in one mental model

Observability means you can ask new questions about your system's health without shipping new code to answer them. It rests on three pillars — metrics (numbers over time: CPU, request rate, error count), logs (discrete events with detail), and traces (a single request's path across services) — and on Kubernetes the default metrics stack is Prometheus for collection and storage, plus Grafana for visualising it. Prometheus works by pulling: it scrapes a /metrics HTTP endpoint on every target on a schedule and stores the result as a time series, so anything that exposes that endpoint — the API server, your nodes, your own application — becomes visible for free.

Targets API server nodes (kubelet) your app /metrics Prometheuspulls /metrics on schedulestores as a time series (TSDB) AlertmanagerSlack / email / PagerDuty Grafanadashboards, PromQL queries Prometheus pulls; it never receives pushed metrics from targets
Pull, store, alert, visualise. Prometheus scrapes every target's /metrics endpoint on a schedule and stores the results as time series. Alertmanager fires on rules Prometheus evaluates; Grafana queries the same data to draw dashboards.

Three ideas carry this chapter: (1) observability's three pillars are metrics, logs, and traces, and Day 42 is entirely about the metrics pillar; (2) Prometheus is a pull-based system — it scrapes targets, it is not pushed to — and it stores everything as time series queried with its own language, PromQL; and (3) a real Kubernetes monitoring setup layers three sources of metrics — the API server and node-level metrics Kubernetes exposes natively, kube-state-metrics for the state of objects (deployments, pods, nodes), and your own application's custom metrics — all scraped by the same Prometheus and shown in the same Grafana.

01
Phase 1 · Theory

Why observability, and how Prometheus and Grafana fit together

First why "is it running" isn't enough, then Prometheus's pull-based architecture, then how Grafana and the three layers of Kubernetes metrics complete the picture.

T1

Why monitoring — and the three pillars of observability

You can deploy a perfectly healthy-looking application and still be flying blind, if nothing tells you when it stops being healthy.

Every earlier chapter got you to the point where an application is deployed and kubectl get pods shows Running. That is not the same as knowing it is healthy. Is it using more memory than it should? Is one replica silently failing more requests than the others? Is a node about to run out of disk? Without monitoring, the first sign of any of these is usually a customer complaint or an outage — which is the most expensive way to find out. Monitoring is the discipline of continuously collecting signals from your systems so you find out from a dashboard or an alert, not from a support ticket.

The modern word for this is observability, and it is usually broken into three pillars. Metrics are numbers sampled over time — CPU percentage, requests per second, error count — cheap to store and perfect for dashboards and alerting on trends. Logs are discrete, detailed events — an application printing "user 4021 failed login: bad password" — invaluable for the specifics a metric can't carry. Traces follow one request as it hops across many services in a microservices architecture, so you can see exactly which hop added the latency. Day 42 is entirely about the metrics pillar, through Prometheus and Grafana — logs and traces are their own tooling (Loki, Jaeger, and similar) built on the same instinct.

The reason metrics come first is cost and immediacy: they are cheap to collect at high frequency and they are what an on-call alert is almost always built from — "error rate above 5% for five minutes," not "read every log line." Getting metrics right is what makes the other two pillars worth adding later, rather than a substitute for them.

T2

Prometheus architecture — pull, store, alert

Prometheus became the default Kubernetes metrics tool because its model — pull, not push — matches how a dynamic cluster actually behaves.

Prometheus's central idea is the pull model. Rather than every application pushing its metrics somewhere, Prometheus is configured with a list of targets and, on a schedule (commonly every 15–30 seconds), makes an HTTP request to each target's /metrics endpoint and reads back whatever plain-text metrics it exposes. This suits Kubernetes particularly well: pods come and go constantly, and Prometheus can be told to auto-discover targets via labels rather than needing every new pod to know where to push its data.

Everything Prometheus scrapes is stored as a time series — a metric name, a set of key/value labels, and a value at a point in time — inside its own on-disk time-series database (TSDB), no external database required. You query that data with PromQL, Prometheus's own query language, whether you're building a dashboard panel, checking a value ad hoc, or writing an alerting rule. On top of the data, Alertmanager is the separate component that takes rules Prometheus evaluates ("this PromQL expression has been true for five minutes") and routes the resulting alert to Slack, email, PagerDuty, or another destination — Prometheus itself only decides an alert condition is true; Alertmanager decides who hears about it and how noise is grouped and silenced.

Target /metricsplain-text exposition Scrape (pull)every 15–30s, per target TSDBon-disk time series PromQLquery it
The core loop. Prometheus pulls each target's plain-text metrics on an interval, keeps them in its own time-series store, and everything downstream — dashboards, alerts — is a PromQL query over that store.
T3

Grafana, and the three layers of Kubernetes metrics

Prometheus collects and stores; Grafana is the layer that turns the numbers into something a human reads at a glance.

Grafana is a visualisation tool: it connects to a data source — Prometheus is the most common — and lets you build dashboards of graphs, gauges, and tables driven by PromQL queries, without writing PromQL by hand every time you want to look. You rarely start a dashboard from a blank page; the Grafana community publishes ready-made dashboards by ID that you import and immediately get a polished view for a known workload (Kubernetes cluster health being one of the most popular).

A complete Kubernetes monitoring setup layers three sources of metrics into the same Prometheus. The first layer is what Kubernetes exposes natively — the API server's own metrics endpoint, and node-level metrics from the kubelet (CPU, memory, disk per node). The second layer is kube-state-metrics, a separate service that watches the Kubernetes API and turns object state into metrics — how many replicas a Deployment wants versus has, whether a pod is pending, how old a Job is — none of which the raw node/API metrics capture, because they're about the objects themselves, not the machines running them. The third layer is your own application's custom metrics — a business counter, a queue depth, a request latency histogram — exposed on your app's own /metrics endpoint and added to Prometheus's scrape configuration like any other target. All three layers end up as time series in the same Prometheus, queryable and dashboardable the same way.

02
Phase 2 · Lab

Hands-on — Prometheus and Grafana on a real cluster

Install Prometheus with Helm, explore it through its own UI and PromQL, install Grafana and wire it to Prometheus with an imported community dashboard, then extend the setup with kube-state-metrics and a scrape config for your own application.

L1

Install Prometheus with Helm

Prometheus ships as an official Helm chart, so installing the whole stack — server, Alertmanager, and default scrape configuration for the cluster itself — is three commands: add the community chart repository, refresh it, then install:

bashhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus prometheus-community/prometheus   # installs server + alertmanager + node-exporter + default scrape config

A few pods appear under a name prefixed prometheus-: the server itself, an Alertmanager pod, and a node-exporter DaemonSet running on every node — it's the piece that exposes each node's own hardware metrics (CPU, memory, disk, network) on /metrics for the server to scrape. Confirm everything is running with a plain kubectl get pods before moving on.

L2

The Prometheus UI and PromQL

Before adding Grafana on top, look at the raw data Prometheus is already collecting, through its own built-in UI.

The Prometheus server ships with a basic web UI for browsing targets and running ad hoc PromQL — useful for a quick check, even though Grafana is where dashboards really live. Expose it with a NodePort Service (or kubectl port-forward for a quick local look) and open it in a browser:

bashkubectl expose service prometheus-server --type=NodePort --target-port=9090 --name=prometheus-server-np
kubectl get svc prometheus-server-np   # note the NodePort, then browse http://<node-ip>:<nodeport>

Under Status → Targets you can see every endpoint Prometheus is scraping and whether the last scrape succeeded — the first place to look if a metric you expect is missing. Under Graph, try a PromQL expression directly, for example up (1 for every target currently reachable, 0 if a scrape is failing) or node_memory_MemAvailable_bytes for a specific node metric. You don't need to master PromQL syntax up front — the same instinct as Terraform's resource blocks applies: learn to read what a query returns, and build from working examples rather than memorising the grammar.

L3

Install Grafana and wire up the data source

Grafana has its own Helm chart, installed the same way. The chart auto-generates an admin password and stores it in a Secret rather than printing it in plain text, so you retrieve it by decoding the Secret:

bashhelm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install grafana grafana/grafana
kubectl get secret grafana -o jsonpath="{.data.admin-password}" | base64 --decode   # the admin password
kubectl expose service grafana --type=NodePort --target-port=3000 --name=grafana-np

Log in with user admin and that decoded password, then add Prometheus as a data source (Grafana → Connections → Data sources → Prometheus), pointing it at the in-cluster service URL, typically http://prometheus-server. Rather than building a dashboard panel by panel, import a community dashboard by ID — dashboard 3662 ("Prometheus 2.0 Overview") is a well-known one that immediately renders a full cluster-health view once the data source is wired up: Grafana → Dashboards → Import → paste the ID → select the Prometheus data source → Import.

L4

kube-state-metrics and scraping your own app

The default install covers node hardware; two more steps complete the picture — object state, and your own application.

The default Prometheus install scrapes node and API-server metrics but does not include kube-state-metrics, the service that turns Deployment/Pod/Node state into scrapeable metrics. Install it as its own chart, and once it's running and labelled correctly for discovery, Prometheus picks it up as another target automatically — no separate dashboard work needed, since dashboard 3662 and similar community dashboards already expect its metric names.

bashhelm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install kube-state-metrics prometheus-community/kube-state-metrics

For your own application's custom metrics, the app must expose a /metrics endpoint (most languages have a Prometheus client library that does this for you), and then Prometheus needs to be told to scrape it — done by editing the scrape_configs section of the prometheus-server ConfigMap (or, more cleanly, adding a ServiceMonitor object if you're running the Prometheus Operator) to add a new job pointing at your app's Service and metrics port. After a config change, Prometheus needs to reload — either it watches the ConfigMap and reloads automatically depending on how it was installed, or you trigger a pod restart — and the new target then shows up under Status → Targets in the UI from L2, confirming it's actually being scraped.

03
Phase 3 · Interview Q&A

The observability questions you'll be asked

These are the questions the instructor explicitly flags in the Prometheus/Grafana video, with answers built from his explanations. Answer each out loud, then reveal.

Q1What is observability, and what are its three pillars?reveal ▸
Answer

Observability is the ability to understand a system's internal state from the signals it produces, so you can answer new questions about its health without shipping new code. Its three pillars are metrics (numbers over time — CPU, request rate, error count), logs (discrete detailed events), and traces (a single request's path across services). Prometheus and Grafana cover the metrics pillar.

Q2Why Prometheus specifically, for Kubernetes monitoring?reveal ▸
Answer

Its pull model fits a dynamic cluster where pods constantly come and go — Prometheus discovers and scrapes targets rather than needing every workload to know where to push metrics. It's also a CNCF graduated project with a huge ecosystem (exporters, client libraries, Alertmanager, Grafana integration), which is why it became the de facto default rather than one option among many.

Q3What is the difference between a pull-based and a push-based monitoring model?reveal ▸
Answer

In a pull model (Prometheus), the monitoring server initiates the connection, requesting each target's /metrics endpoint on a schedule. In a push model, targets send their metrics to a central collector on their own schedule. Pull makes it easy to see whether a target is even reachable (a failed scrape is itself a signal) and suits a cluster where Prometheus can auto-discover targets via labels; push suits short-lived jobs that finish before a pull would ever reach them, which is why Prometheus has a separate Pushgateway for exactly that case.

Q4What does Prometheus actually store, and in what?reveal ▸
Answer

It stores time series — a metric name plus a set of labels plus a value at a timestamp — in its own on-disk time-series database (TSDB); it doesn't require an external database. You query that data with PromQL, Prometheus's own query language, whether for an ad hoc check, a Grafana panel, or an alerting rule.

Q5What is Alertmanager, and how is it different from Prometheus itself?reveal ▸
Answer

Prometheus evaluates alerting rules (PromQL expressions that become true or false) and fires an alert when one is true; Alertmanager is the separate component that receives those firing alerts and decides what happens next — routing to Slack, email, or PagerDuty, grouping related alerts together, silencing known issues, and de-duplicating repeats. Prometheus decides if something's wrong; Alertmanager decides who hears about it and how.

Q6What is Grafana, and why not just use Prometheus's own UI?reveal ▸
Answer

Grafana is a visualisation layer that connects to a data source — commonly Prometheus — and builds proper dashboards: graphs, gauges, tables, alerts, all driven by saved PromQL queries. Prometheus's built-in UI is useful for a quick ad hoc check and confirming targets are being scraped, but it isn't meant for building the kind of shareable, polished dashboard a team actually watches day to day — that's Grafana's job.

Q7What are the three layers of metrics in a complete Kubernetes monitoring setup?reveal ▸
Answer

First, what Kubernetes exposes natively — the API server's own metrics and node-level metrics from the kubelet. Second, kube-state-metrics, which turns object state (replica counts, pod phase, node conditions) into metrics the raw node/API data doesn't capture. Third, your own application's custom metrics exposed on its own /metrics endpoint. All three are scraped by the same Prometheus and shown in the same Grafana.

Q8What is kube-state-metrics, specifically, and why is it needed?reveal ▸
Answer

It's a service that watches the Kubernetes API and converts the state of objects — how many replicas a Deployment wants versus has, whether a pod is pending, a Job's age — into Prometheus metrics. Node-level exporters and the API server's own metrics tell you about machine health and API traffic, but nothing about whether your Deployment's desired state matches reality; kube-state-metrics is what fills that specific gap.

Q9How do you get your own application's metrics into Prometheus?reveal ▸
Answer

The application exposes a /metrics HTTP endpoint, usually via a Prometheus client library for its language, and you add a scrape job for it in Prometheus's scrape_configs (in the prometheus-server ConfigMap, or a ServiceMonitor if running the Prometheus Operator), pointing at the app's Service and metrics port. After Prometheus reloads its config, the new target appears under Status → Targets once it's being scraped successfully.

Q10Do you build Grafana dashboards from scratch?reveal ▸
Answer

Rarely for well-known workloads. Grafana has a public library of community dashboards identified by ID — for example 3662 for a general Prometheus/Kubernetes overview — that you import and immediately get a polished, working view once your data source is connected. Custom panels are for metrics specific to your own application that no community dashboard could know about.

04
Phase 4 · Scenario drills

The senior layer — observability where a silent gap is the incident

You can now stand up Prometheus and Grafana and wire in all three metric layers. These 18 scenarios are the senior/on-call version: missing targets, cardinality blow-ups, alert fatigue, dashboards that lie, and knowing the difference between "no alert fired" and "everything is fine." Answer out loud, reveal, and mark yourself.

📈

Observability

18 scenarios · 1–18

The complete Observability scenario set, worked through in depth. This is the thinking and the story behind each one, not answers to memorise. The goal is that a missing target or a dashboard showing green during an outage is a puzzle you already know how to open, not a page you have to look up.

⚡ The universal observability reflex

Before trusting any dashboard or the absence of any alert, ask these in order:

  1. Is the data even arriving? Check Status → Targets in Prometheus — a scrape failure looks identical to "everything is fine" on a dashboard that just shows a flat line or a gap.
  2. Is the query actually measuring what you think? A dashboard can be green because the PromQL is wrong, not because the system is healthy.
  3. Would an alert have fired for this? If nobody wrote a rule for it, "no alert" tells you nothing — it's not the same as "no problem."
  4. Is this a monitoring gap or a real incident? Distinguish "the system is broken" from "our visibility into the system is broken" before you start debugging the wrong one.

Almost every scenario below is one of these four questions applied to a specific failure mode. Get the reflex automatic and the rest is detail.

---

📋 The full scenario inventory (distinct — no padding)

A. Collection & scrape health

  1. A target silently stops being scraped
  2. A metric that used to exist has disappeared
  3. Short-lived jobs never show up in Prometheus

B. Storage, scale & cardinality

  1. Prometheus runs out of memory / disk
  2. A cardinality explosion from an unbounded label
  3. Long-term retention and historical queries

C. Alerting

  1. Alert fatigue — too many low-value alerts
  2. An alert that should have fired never did
  3. Flapping alerts firing and resolving repeatedly
  4. Nobody owns an alert when it fires

D. Dashboards & queries

  1. A dashboard shows green during a real outage
  2. A PromQL query returns no data unexpectedly
  3. Two dashboards disagree about the same metric

E. Kubernetes-specific

  1. kube-state-metrics is missing or outdated
  2. Metrics disappear when a pod restarts
  3. Multi-cluster / multi-tenant monitoring

F. Process & maturity

  1. Monitoring was bolted on after an incident, not designed in
  2. Choosing what to monitor for a brand-new service

---

1A target silently stops being scrapedScenario

A service's dashboard panels have gone flat — not showing an outage, just showing nothing new, and nobody noticed for two days.

What is actually happening (the mental model).

Prometheus only has data for targets it can successfully scrape — if a target becomes unreachable (a pod restarted with a new IP that Prometheus's service discovery hasn't picked up, a NetworkPolicy blocking it, a port change), Prometheus doesn't error loudly; the metric just stops updating, and a dashboard panel with no fresh data can look deceptively like "flat and stable" rather than "broken."

How to work through it.

  1. Check Status → Targets in the Prometheus UI — every target shows its last scrape time and error, if any; a target stuck in the past or marked down is the whole answer.
  2. If the target is missing entirely (not even listed as down), the problem is service discovery, not the scrape itself — check the relevant labels/annotations the Prometheus config relies on to discover it.
  3. Confirm the target's /metrics endpoint actually still exists and responds, independent of Prometheus, with a direct curl from inside the cluster.

The root causes and trade-offs.

One cause is the pod's IP changing while service discovery either lagged behind or was misconfigured to begin with. Another is a NetworkPolicy introduced after the scrape configuration was already set up, quietly blocking it. A third is the application changing its metrics port without anyone updating the scrape job to match. All three are silent by nature — nothing crashes, the data just quietly stops arriving.

The trap that less-experienced engineers fall into.

Reading "the graph looks flat" as "the system is stable," instead of checking whether the flat line means healthy or means no new data arrived at all — the two look identical on most dashboards.

🎯 Interviewer follow-up questions you should expect.

  • "How do you know if a target has actually stopped reporting, vs. genuinely being flat?" You check Status → Targets for the last-scrape time and any errors, not just the dashboard graph.
  • "How would you alert on this happening again?" You'd add an alert on the up metric being 0, or on scrape staleness, for critical targets.
Say it like this"A flat graph and a healthy graph can look identical, so I never trust the dashboard alone — I check Status → Targets for the actual scrape status and last-seen time. If the target's missing outright rather than marked down, that's a service discovery problem, not a scrape failure, and I'd also add an alert on the up metric so this doesn't sit silent for two days again."

---

Mark:
2A metric that used to exist has disappearedScenario

A PromQL query that worked last month now returns nothing, and a dashboard panel built on it is empty.

What is actually happening (the mental model).

Metrics in Prometheus are identified by name and labels — a metric "disappearing" almost always means either the exporter/library version changed and renamed it, or a label value changed so your exact query no longer matches anything, even though a very similar series still exists under a slightly different name or label set.

How to work through it.

  1. Search for the metric family by partial name in the Prometheus UI's metric browser — renamed metrics are usually a close variant of the old name.
  2. Check the exporter's or client library's changelog around the time it disappeared — a version bump is a very common trigger for metric renames.
  3. If the name is unchanged, check whether a label value the query filters on (a version tag, an environment label) changed, silently breaking the exact match.

The root causes and trade-offs.

One cause is an exporter upgrade that renamed or restructured its metrics. Another is a label's value changing, for example a deploy that relabels environment or version differently than before. A third is the exporter itself being removed or replaced entirely. Pinning exporter versions trades away newer metrics in exchange for query stability, and that's a real trade-off worth making deliberately rather than discovering by accident.

The trap that less-experienced engineers fall into.

Assuming the underlying system stopped producing the data entirely, and investigating the application, when the data is still there under a slightly different name or label — a broader, unfiltered search would have found it immediately.

🎯 Interviewer follow-up questions you should expect.

  • "A query that used to work now returns nothing — first thing you check?" You search for the metric family more broadly before assuming the data is gone.
  • "How do you prevent this from breaking dashboards silently?" You pin exporter and library versions deliberately and review changelogs before upgrading anything metrics-related.
Say it like this"Before assuming the data's gone, I search the metric browser more broadly, because 'disappeared' is almost always a rename from an exporter upgrade or a label value that quietly changed — the series is usually still there under a slightly different name. Once I've found the pattern, I'll pin exporter versions going forward so a routine upgrade doesn't silently break dashboards again."

---

Mark:
3Short-lived jobs never show up in PrometheusScenario

A nightly batch job runs for 90 seconds and finishes — its metrics never appear in Prometheus at all, even though the job logs say it emitted them.

What is actually happening (the mental model).

Prometheus's pull model assumes a target stays up long enough to be scraped on the next interval — a job that starts, runs, and exits inside 90 seconds may finish and disappear entirely between one scrape cycle and the next, so Prometheus never gets the chance to pull anything from it.

How to work through it.

  1. Recognise this as a mismatch between the pull model and short-lived workloads, not a configuration bug in the usual sense.
  2. Use the Pushgateway — a component built exactly for this — where the short-lived job pushes its final metrics before exiting, and Prometheus then scrapes the Pushgateway itself (which holds the value until explicitly cleared) instead of the job directly.
  3. Confirm the job is instrumented to push (not just expose /metrics, which is useless if nothing scrapes it in time) and that stale values are cleared appropriately so old runs don't linger and mislead.

The root causes and trade-offs.

The fundamental cause is that pull-based scraping simply doesn't fit workloads that run for less time than the scrape interval itself. The Pushgateway fixes the visibility problem, but it adds a component with its own trade-off to manage: because it just keeps serving the last pushed value, it can itself mask a job that has stopped running entirely, so that trade-off needs active management rather than being ignored.

The trap that less-experienced engineers fall into.

Trying to fix this by shortening the scrape interval — even a very short interval can still race a genuinely fast job, and it doesn't address the real mismatch between pull-based scraping and ephemeral workloads.

🎯 Interviewer follow-up questions you should expect.

  • "Why don't short batch jobs show up in Prometheus?" They can finish and disappear between scrape cycles, so pull never gets a chance to reach them.
  • "How do you monitor them properly?" You use the Pushgateway — the job pushes before exiting, and Prometheus scrapes the gateway instead.
Say it like this"Pull-based scraping assumes the target is still up on the next interval, and a 90-second job can finish and vanish before that happens — that's a mismatch with the model, not a config bug. The fix is the Pushgateway: the job pushes its result before it exits, and Prometheus scrapes the gateway instead of the job directly. I also make sure stale values get cleared, so the gateway doesn't quietly keep showing yesterday's success after a run actually starts failing."

---

Mark:
4Prometheus runs out of memory / diskScenario

The Prometheus server pod itself gets OOMKilled or its disk fills up, and monitoring for the whole cluster goes down with it.

What is actually happening (the mental model).

Prometheus keeps recent time-series data in memory before it's compacted to disk, and its resource footprint scales directly with how many distinct time series it's tracking (its cardinality) and how long it retains data — this isn't a fixed cost, it grows with every new label combination and every additional day of retention you configure.

How to work through it.

  1. Check prometheus_tsdb_head_series (or similar internal metrics Prometheus exposes about itself) to see the actual current series count and whether it's been growing.
  2. Check the retention window (--storage.tsdb.retention.time) and disk size against how much data that retention actually requires — a retention bump without a matching disk/memory bump is a common cause.
  3. Look for a recent cardinality spike (see the next scenario) as the trigger, rather than assuming it's simply "grown over time" from ordinary usage.
  4. Fix with more resources for genuine growth, or by reducing retention/cardinality if the growth is a symptom of something that shouldn't be happening.

The root causes and trade-offs.

This can come from real, expected growth as more services get onboarded, from a cardinality explosion like the one in the next scenario, or from a retention setting that was increased without a matching increase in resources. More retention and more granular labels are both genuinely useful, but the trade-off is that both cost real memory and disk, and that cost needs to be planned for in advance rather than discovered through an outage.

The trap that less-experienced engineers fall into.

Treating Prometheus's own resource limits as something to just keep raising, without ever checking why usage is growing — sometimes it's healthy growth, but sometimes it's a single bad label choice multiplying series count unnecessarily.

🎯 Interviewer follow-up questions you should expect.

  • "Prometheus keeps running out of memory — how do you investigate?" You check the current series count and its trend, then correlate it with retention settings and any recent cardinality spike.
  • "How does retention affect resource usage?" More retention means more data held on disk, and touched in memory during compaction, so it must scale with actual capacity, not be bumped in isolation.
Say it like this"Prometheus's footprint scales with how many distinct time series it's tracking and how long it retains them, so before just adding resources I check the actual series count and whether it's a steady healthy trend or a recent spike. If it's a spike, that points to the next scenario — a cardinality problem from one bad label choice — rather than genuine organic growth that just needs more memory."

---

Mark:
5A cardinality explosion from an unbounded labelScenario

Prometheus's memory usage suddenly spikes hard after what looked like a routine application change.

What is actually happening (the mental model).

Every unique combination of a metric name plus its label values is a separate time series — a label with a genuinely unbounded set of values (a user ID, a raw URL path with IDs in it, a request ID) multiplies the number of series by however many distinct values that label ever takes, which for something like a user ID can mean millions of new series from a single bad instrumentation choice.

How to work through it.

  1. Identify the metric responsible via Prometheus's own prometheus_tsdb_head_series breakdown, or a "top series by metric name" query if your setup supports it.
  2. Inspect that metric's label set for anything with unbounded cardinality — user IDs, full URL paths, request/session IDs are the classic offenders.
  3. Fix at the source: remove or bucket the offending label in the application's instrumentation (a URL template like /users/:id instead of the literal path with the real ID in it) — Prometheus-side mitigation (dropping the label via relabeling) is a stopgap, not the real fix.

The root causes and trade-offs.

The root cause is instrumenting a metric with a label that was convenient to add but turns out to have effectively unlimited values. The underlying trade-off is between very granular labels, which are genuinely useful for debugging one specific request, and the very real cost that granularity has at scale. The fix is almost always to bucket or template the label, not to eliminate the metric entirely.

The trap that less-experienced engineers fall into.

Reflexively giving Prometheus more memory/disk to absorb the spike instead of finding and fixing the specific label causing it — the underlying problem keeps growing unbounded, so more resources only delays the same outage.

🎯 Interviewer follow-up questions you should expect.

  • "What causes a cardinality explosion?" A label with effectively unbounded values — a user ID, a raw URL path, a request ID — multiplies the number of distinct time series.
  • "How do you fix it properly, not just patch it?" You remove or bucket the offending label at the instrumentation source, not just add more Prometheus resources.
Say it like this"A cardinality explosion is almost always one label with effectively unbounded values — a user ID or a raw URL path — multiplying the series count. I find the offending metric through Prometheus's own series breakdown, then fix it at the instrumentation source by templating or bucketing that label, rather than just throwing more memory at Prometheus, because the underlying growth is unbounded and resources alone would only buy time."

---

Mark:
6Long-term retention and historical queriesScenario

Someone asks for a year-over-year comparison of error rates, but Prometheus only retains 15 days of data.

What is actually happening (the mental model).

Prometheus's local TSDB is deliberately optimised for recent data at high resolution, not indefinite long-term storage — its retention is a knob you set, and raising it substantially costs real disk and query performance on the single server, which is why long-term/historical needs are usually solved by a separate remote-storage layer, not by cranking local retention arbitrarily high.

How to work through it.

  1. Distinguish the actual need: is this a one-off report, or an ongoing requirement for long-range queries?
  2. For genuine long-term retention, look at Prometheus's remote_write integration to a long-term storage backend (Thanos, Cortex, Mimir, or a managed equivalent) that's designed for exactly this instead of the local TSDB.
  3. Understand these systems typically downsample older data (keep full resolution recently, coarser resolution further back), which is a reasonable trade-off for year-scale queries that don't need per-second granularity anyway.

The root causes and trade-offs.

Sometimes local retention is genuinely set too low for a real, ongoing long-term need, and the fix there is remote long-term storage. Other times it's really just a one-off report that doesn't justify standing up new infrastructure, and the right response is simply to note the gap for future retention planning rather than over-engineering a solution for a single ask. Downsampling trades away exact historical precision in exchange for a query that stays fast at long time ranges.

The trap that less-experienced engineers fall into.

Simply cranking up local retention to a very long window without recognising the disk and query-performance cost that comes with keeping full-resolution data indefinitely on a single Prometheus server — long-term storage is a different problem with different tools, not just a bigger number in a config flag.

🎯 Interviewer follow-up questions you should expect.

  • "How do you handle year-scale historical queries when Prometheus only keeps 15 days?" You use remote_write into a long-term storage system like Thanos, Cortex, or Mimir, rather than just raising local retention.
  • "What's the trade-off with long-term storage systems?" They typically downsample older data, keeping full resolution recently and coarser resolution further back, trading precision for sustainable query performance.
Say it like this"Local Prometheus retention is meant for recent, high-resolution data, not indefinite history — cranking it up on a single server just trades disk and query performance for a problem it isn't designed to solve. For genuine long-term needs I'd look at remote_write into something like Thanos or Mimir, which is built for exactly this and usually downsamples older data, which is a reasonable trade for a year-scale query that doesn't need per-second precision anyway."

---

Mark:
7Alert fatigue — too many low-value alertsScenario

The on-call Slack channel is so full of alerts that people have started muting it — and a genuine incident got missed in the noise.

What is actually happening (the mental model).

Every alert that fires and doesn't require action trains the on-call engineer to ignore alerts — this is a real, well-documented failure mode, not a discipline problem with the humans involved; the fix is treating alert quality as a metric to actively manage, not something that accumulates passively as thresholds get added over time.

How to work through it.

  1. Audit existing alerts for ones that fire often but rarely require action — these are the highest-value candidates to delete, raise the threshold on, or convert into a dashboard-only signal instead of a page.
  2. Distinguish symptom-based alerts (user-facing impact — error rate, latency) from cause-based alerts (an internal metric that might contribute to a symptom) — page on symptoms, and let cause-based signals live on dashboards for diagnosis after a symptom alert has already fired.
  3. Use Alertmanager's grouping and inhibition features so one root cause doesn't fan out into a dozen separate pages for every downstream symptom.
  4. Track a "percentage of alerts that led to action" metric over time, so alert quality is visibly improving or degrading, not just assumed.

The root causes and trade-offs.

This usually comes from alerting on every internal metric just in case, instead of on genuine user-facing symptoms, combined with not using Alertmanager's grouping and inhibition features, so that one incident turns into many separate pages. It's made worse by nobody ever going back to remove an alert once it's been added. Tightening alerts does carry a real, if small, risk of missing something genuinely rare, but that's worth accepting against the much larger cost of training the whole team into alert-blindness.

The trap that less-experienced engineers fall into.

Responding to alert fatigue by adding more alerting rules to "catch things better," which makes the noise worse — the fix is almost always deleting or consolidating alerts, not adding new ones.

🎯 Interviewer follow-up questions you should expect.

  • "How do you fix alert fatigue?" You audit for low-value alerts and remove or consolidate them, favour symptom-based paging over cause-based, and use Alertmanager's grouping and inhibition features.
  • "Symptom-based vs cause-based alerting — what's the difference?" Symptom-based alerting pages on user-facing impact, like error rate or latency; cause-based alerting flags internal metrics that might contribute — you page on symptoms and diagnose with causes.
Say it like this"Alert fatigue is a real failure mode, not a discipline problem — every alert that fires without needing action trains people to tune out the channel. I audit for the noisy, low-value alerts first and either delete them or move them to dashboard-only, favour symptom-based paging over cause-based, and lean on Alertmanager's grouping and inhibition so one root cause doesn't fan out into a dozen pages. The instinct to add more alerts to 'catch things better' is usually exactly backwards."

---

Mark:
8An alert that should have fired never didScenario

After an outage, the postmortem finds there was a metric that clearly showed the problem building — but no alert ever fired.

What is actually happening (the mental model).

"No alert fired" can mean the system was healthy, or it can mean nobody wrote a rule for this failure mode, or the rule existed but had a bug (wrong threshold, wrong PromQL, wrong duration window) — these are three very different findings with three very different fixes, and a postmortem needs to identify which one actually happened.

How to work through it.

  1. Check whether an alerting rule for this condition existed at all — if not, this is a coverage gap, not a bug.
  2. If a rule existed, re-run its exact PromQL against the historical data from the incident window to see whether it actually would have fired — sometimes the logic is subtly wrong (an "or" that should be "and," a threshold set from stale assumptions).
  3. Check the rule's for duration — a rule requiring a condition to hold for 10 minutes won't fire for a spike that resolves in 3, which might be correct behaviour or might be exactly the gap.
  4. Add the missing rule (or fix the buggy one) and, ideally, replay it against the historical data to confirm it would now catch the same incident.

The root causes and trade-offs.

Sometimes no rule was ever written for this specific failure mode at all, which is a genuine blind spot. Other times a rule did exist but had incorrect logic or thresholds, a latent bug that stays invisible until it's actually tested against a real incident. And sometimes the rule's duration window was a legitimate noise-reduction trade-off that simply happened to miss this specific case.

The trap that less-experienced engineers fall into.

Treating "no alert fired" as proof the monitoring is broken in general, and reflexively adding a pile of new alerts, rather than precisely diagnosing whether this was a missing rule, a buggy rule, or a legitimate trade-off that just happened to bite this once.

🎯 Interviewer follow-up questions you should expect.

  • "How do you investigate a missed alert after an incident?" You check whether a rule existed at all, then replay its exact logic against historical data to see if it should have fired.
  • "What's a common reason a real alert rule doesn't fire in time?" A common reason is a for duration window tuned for noise reduction that happens to be longer than the incident took to resolve.
Say it like this"'No alert fired' isn't one finding, it's three possible ones — no rule existed, the rule existed but was buggy, or the rule's duration window legitimately didn't match this incident's timing. I check which one it actually was by replaying the rule's exact PromQL against the historical data before deciding whether the fix is a new rule, a corrected threshold, or accepting a known trade-off."

---

Mark:
9Flapping alerts firing and resolving repeatedlyScenario

The same alert fires, resolves, fires again, resolves again — dozens of times over an hour, each one paging on-call.

What is actually happening (the mental model).

Flapping means the underlying metric is oscillating right around the alert's threshold — every small crossing re-triggers the alert, which is a config/tuning problem (the threshold or duration doesn't match how the metric actually behaves), not necessarily a sign the underlying issue itself is getting better or worse each time.

How to work through it.

  1. Graph the raw metric over the flapping window — is it hovering right at the threshold, genuinely unstable, or is the threshold simply set at a noisy point in the metric's normal range?
  2. Add or lengthen the alert's for duration, so a brief crossing doesn't immediately fire — this smooths out noise without hiding a sustained problem.
  3. Use Alertmanager's grouping so repeated fire/resolve cycles for the same alert don't each generate a separate page.
  4. If the metric is inherently noisy, consider alerting on a smoothed/averaged version of it (a rolling average over PromQL) rather than the raw instantaneous value.

The root causes and trade-offs.

The root cause is usually a threshold set exactly where the metric naturally hovers, combined with no for duration in place to smooth over brief crossings, and often alerting on the raw noisy value instead of a rolling average. Adding a duration window and smoothing trades a little detection latency for a lot less noise, and that's almost always worth it for anything that isn't safety-critical enough to need an instant response.

The trap that less-experienced engineers fall into.

Reflexively silencing or disabling a flapping alert to stop the noise, without fixing the underlying threshold/duration tuning — which either hides a real recurring problem or just defers the same noise to reappear later.

🎯 Interviewer follow-up questions you should expect.

  • "An alert keeps flapping — what do you do?" You check whether the metric is hovering right at the threshold, then fix it with a longer for duration or a smoothed metric, rather than silencing it.
  • "How does Alertmanager help with repeated fire/resolve cycles?" Its grouping feature consolidates related notifications so the same flapping alert doesn't generate a fresh page every cycle.
Say it like this"Flapping usually means the metric is hovering right at the threshold, so I graph the raw value first rather than just silencing the noise. The fix is almost always a longer for duration or alerting on a smoothed rolling value instead of the instantaneous one, plus Alertmanager grouping so the same flapping alert doesn't page separately every cycle. Silencing without fixing the tuning just hides the problem or defers the noise."

---

Mark:
10Nobody owns an alert when it firesScenario

An alert fires at 3am, pages a generic on-call rotation, and the responder has no idea what team or system it's even about.

What is actually happening (the mental model).

An alert that doesn't clearly identify which team owns it, what it means, and what to do transfers the cognitive load of figuring that out onto the least-prepared person — someone freshly woken up at 3am — instead of the person who wrote the alert and had full context at the time.

How to work through it (the standard for a good alert).

  1. Every alerting rule should carry labels routing it to the right team/service (via Alertmanager routing rules matched on labels) so it pages the people who can actually act, not a generic catch-all rotation.
  2. Every alert should include a clear annotation with what the alert means and, ideally, a link to a runbook — the response steps written calmly in advance, not improvised at 3am.
  3. If an alert genuinely has no clear owner, that's itself the finding — either assign one, or seriously reconsider whether the alert should exist as a page at all versus a dashboard signal.

The root causes and trade-offs.

This usually comes from alerts written without ownership labels or a runbook link, often because they were added quickly during or right after an incident and never revisited afterward, combined with a shared or generic on-call rotation covering systems that no single person fully understands. Runbooks take real upfront effort to write and maintain, but the trade-off is against a 3am responder having to improvise blind every single time.

The trap that less-experienced engineers fall into.

Adding new alerting rules during an incident, under pressure, without also adding the ownership/routing labels and a runbook reference — solving today's visibility gap while creating tomorrow's "nobody knows what this alert means" problem.

🎯 Interviewer follow-up questions you should expect.

  • "What makes a good alert, beyond the threshold itself?" Clear ownership and routing labels, plus an annotation with a runbook link, so the responder doesn't have to reconstruct context at 3am.
  • "How do you route alerts to the right team?" You use Alertmanager routing rules matched on the labels the alert carries, not a single generic rotation for everything.
Say it like this"A good alert isn't just a correct threshold — it needs ownership labels so Alertmanager routes it to the team that can actually act, and an annotation with a runbook link so the responder isn't reconstructing context at 3am from scratch. When I see an alert with no clear owner, that's the actual finding — either it gets one, or it doesn't deserve to be a page at all."

---

Mark:
11A dashboard shows green during a real outageScenario

Users are reporting the app is down, but the team's main Grafana dashboard shows every panel green.

What is actually happening (the mental model).

A dashboard is only as honest as the metrics feeding it — it can show green because the system genuinely is healthy, or because the metric it's built on doesn't actually capture the failure mode happening right now (a health check that only tests one shallow path, an average that hides a severely affected minority of requests), which is a dashboard-design gap, not a monitoring-system malfunction.

How to work through it.

  1. Confirm the underlying data is actually fresh (see the "silent scrape failure" scenario) before assuming the metric itself is the problem — check it isn't simply stale data rendering as a static "healthy" state.
  2. If the data is fresh and still shows green, question what the metric actually measures — an average latency can look fine while a real minority of requests time out; a shallow health check can pass while a deeper dependency is failing.
  3. Cross-check against a metric closer to the actual user experience — error rate on real traffic, a synthetic end-to-end check — rather than trusting one dashboard's framing of "healthy."
  4. After resolving, add the metric or percentile (like p99 latency instead of average) that would have actually shown this outage, so the dashboard's blind spot is closed.

The root causes and trade-offs.

One cause is stale data rendering as a false-positive "healthy" state. Another is an average hiding a real problem that's only affecting a minority of traffic. A third is a shallow health check that never actually exercises the failing dependency. Averages and shallow checks are cheap and simple to build, but the trade-off is that they can miss exactly the kind of partial, tail-latency incident that matters most to users.

The trap that less-experienced engineers fall into.

Trusting the dashboard over the user reports, and spending time trying to explain why users must be wrong — instead of immediately suspecting the dashboard's own metrics might not capture this specific failure mode.

🎯 Interviewer follow-up questions you should expect.

  • "Dashboard is green but users report an outage — what do you trust?" You trust the user reports, then go find out what the dashboard's metric actually measures and where its blind spot is.
  • "Why can an average metric hide a real incident?" It can look fine while a real subset of requests, or one specific path, is badly affected, as long as the majority of traffic is unaffected.
Say it like this"If users report an outage and the dashboard's green, I trust the users and go find out what the dashboard's metric actually measures — an average latency or a shallow health check can look perfectly fine while a real subset of requests is failing badly. Once I've found what it actually missed, I add the metric — often a p99 instead of an average, or a deeper synthetic check — that would have caught this specific failure mode."

---

Mark:
12A PromQL query returns no data unexpectedlyScenario

A PromQL query you're confident should return data comes back completely empty in the Prometheus UI.

What is actually happening (the mental model).

An empty PromQL result almost always means the query's label selector doesn't match any existing series exactly — Prometheus label matching is exact-string by default, so a subtly wrong label value, a typo, or a metric name that doesn't exist at all produces silently empty results rather than an error.

How to work through it.

  1. Strip the query down to just the bare metric name with no label filters — if that alone returns nothing, the metric doesn't exist (or isn't being scraped, see earlier scenarios); if it returns data, the problem is in your label filter.
  2. Once the bare metric returns data, inspect the actual label values Prometheus has for it and compare them character-for-character against what your filter expects — case, exact spelling, and value format (a version string like "1.0" vs "1.0.0") all matter.
  3. Check the query's time range — a valid query can return nothing if the time window predates when the metric started being collected, or a rate/increase function's window is shorter than the scrape interval.

The root causes and trade-offs.

Sometimes the metric genuinely doesn't exist or isn't being scraped at all. Other times a label filter has a typo or the wrong exact value. And sometimes the time range or the rate window is simply shorter than the data that's actually available. All three are query-construction issues, not Prometheus itself malfunctioning.

The trap that less-experienced engineers fall into.

Assuming a complex query is "broken" and rewriting it repeatedly, rather than methodically stripping it back to the bare metric name first to isolate whether the issue is the metric's existence or just one label filter.

🎯 Interviewer follow-up questions you should expect.

  • "A query returns no data — how do you debug it?" You strip it down to the bare metric name first, then add filters back one at a time to isolate which one excludes everything.
  • "Why does PromQL label matching fail silently instead of erroring?" Because it's exact-string matching by design — a non-matching filter is treated as a legitimate, if empty, result, not an error.
Say it like this"An empty PromQL result is almost always a label filter that doesn't exactly match what's actually there, so I strip the query down to the bare metric name first — if that alone is empty, the metric isn't being collected; if it has data, I add filters back one at a time to find which one is wrong. It's rarely Prometheus malfunctioning, it's exact-string matching being unforgiving of a typo or a slightly different value format."

---

Mark:
13Two dashboards disagree about the same metricScenario

Two Grafana dashboards, both supposedly showing "error rate" for the same service, show visibly different numbers at the same moment.

What is actually happening (the mental model).

"Error rate" is not one unambiguous query — two dashboards can both be technically correct while measuring subtly different things: different time windows for the rate calculation, different label filters (one includes a specific route the other excludes), or one query using rate() while the other uses increase() divided by a different denominator.

How to work through it.

  1. Open both dashboard panels' underlying PromQL (Grafana lets you inspect and edit the query behind any panel) side by side rather than trusting the panel titles.
  2. Compare the exact time window each uses in its rate/increase function — a 1-minute window and a 5-minute window smooth differently and will legitimately show different instantaneous values.
  3. Compare label filters precisely — one dashboard might be scoped to a specific route, namespace, or version that the other isn't.
  4. Once you've found the actual difference, decide which definition is the "official" one for that name, and align both dashboards to it (or rename one so the difference is explicit rather than implicit).

The root causes and trade-offs.

Sometimes the two panels use different rate or increase time windows. Sometimes they use different label scoping. And sometimes they're built on an entirely different underlying metric — an application-level error counter versus an ingress-level 5xx counter, say — despite both being labelled "error rate." None of these is a bug on its own; the real problem is that two things with genuinely different meanings are sharing a name that implies they're the same.

The trap that less-experienced engineers fall into.

Assuming one dashboard must simply be "wrong" and trying to fix its data source or plumbing, instead of comparing the actual PromQL behind both panels, where the real difference almost always lives.

🎯 Interviewer follow-up questions you should expect.

  • "Two dashboards show different numbers for 'the same metric' — how do you resolve it?" You compare the actual PromQL behind both panels — time windows and label scope are the usual culprits, not a data pipeline bug.
  • "How do you prevent this ambiguity long-term?" You standardise a single definition, ideally through a shared, reusable query or library panel, for commonly named metrics like "error rate" across dashboards.
Say it like this"'Error rate' isn't one unambiguous query, so when two dashboards disagree I go straight to comparing the actual PromQL behind each panel rather than assuming one is broken — it's almost always a different time window or a different label scope, sometimes even a different underlying metric entirely. Once I've found the real difference I standardise on one definition so the same name means the same thing everywhere."

---

Mark:
14kube-state-metrics is missing or outdatedScenario

A community Grafana dashboard is imported but half its panels show "No data," even though Prometheus and Grafana are both clearly working.

What is actually happening (the mental model).

Community Kubernetes dashboards are built assuming kube-state-metrics is deployed and scraped — if it's missing entirely, or an old version whose metric names have since changed, every panel relying on object-state metrics (replica counts, pod phase, node conditions) comes back empty, while panels built on raw node/API metrics work fine, which is a useful clue about exactly what's missing.

How to work through it.

  1. Check whether kube-state-metrics is even deployed and being scraped (Status → Targets again) — the single most common cause of "half the panels are empty" on an imported Kubernetes dashboard.
  2. If it's deployed but panels are still empty, check its version against what the dashboard expects — a version mismatch can mean the exact metric names the dashboard queries no longer exist, similar to the "disappeared metric" scenario but scoped specifically to this component.
  3. Reconcile by either installing/upgrading kube-state-metrics or, if pinned for a reason, importing a dashboard version compatible with the installed version instead.

The root causes and trade-offs.

Sometimes kube-state-metrics was simply never installed as part of the monitoring setup. Other times it was installed, but at a version whose metrics don't match what an imported community dashboard is actually expecting. This is essentially the earlier "metric disappeared" and "silent scrape failure" scenarios showing up again, in the specific and very common context of a fresh Kubernetes monitoring setup.

The trap that less-experienced engineers fall into.

Assuming the whole monitoring stack is broken because "half the dashboard is empty," instead of noticing that the working panels and the empty panels cleanly split along exactly which metric source (node/API vs. kube-state-metrics) they depend on.

🎯 Interviewer follow-up questions you should expect.

  • "Half a Kubernetes dashboard shows No data — what's the first thing you check?" You check whether kube-state-metrics is deployed and being scraped at all.
  • "The dashboard was working before an upgrade and now some panels break — why?" kube-state-metrics's own version may have changed the metric names the dashboard still queries under the old names.
Say it like this"When exactly half an imported Kubernetes dashboard shows no data, that split is the clue — the working panels and the empty ones usually divide cleanly along node/API metrics versus kube-state-metrics. So I check first whether it's even deployed and being scraped, and if it is, whether its version still matches the metric names the dashboard expects, rather than assuming the whole stack is broken."

---

Mark:
15Metrics disappear when a pod restartsScenario

A counter metric that had been steadily climbing suddenly drops back to zero after a routine pod restart, making a rate-of-change graph spike wildly.

What is actually happening (the mental model).

A Prometheus counter only ever increases while a process is alive — it lives in that process's memory and resets to zero whenever the process restarts, which is expected, documented behaviour, not data loss; the reason it doesn't ruin dashboards in practice is that PromQL's rate()/increase() functions are specifically designed to detect a counter reset and handle it correctly.

How to work through it.

  1. Confirm the metric type is genuinely a counter (as opposed to a gauge, which isn't supposed to only-increase) — the reset-on-restart behaviour is correct for counters and wrong-looking for gauges.
  2. Make sure any query or panel graphing this metric uses rate() or increase() rather than the raw counter value directly — graphing the raw value will show the reset as a visible cliff, while rate() correctly treats it as "the counter restarted, don't count this interval as a huge negative rate."
  3. If a panel is still showing an artificial spike even through rate(), check the query's time window relative to how frequently pods restart — a very short window right around a restart can still show a brief artefact.

The root causes and trade-offs.

The actual bug is almost always a dashboard graphing the raw counter value instead of wrapping it in rate() or increase(). That leads people to confuse a counter that's genuinely just reset on restart with a real anomaly. This is a dashboard-construction issue, not something wrong with Prometheus or the application itself.

The trap that less-experienced engineers fall into.

Treating a counter reset after a restart as a sign of data loss or a monitoring bug worth investigating deeply, when it's expected behaviour that a correctly-written rate() query already accounts for — the actual bug, if any, is a panel that graphs the raw counter instead.

🎯 Interviewer follow-up questions you should expect.

  • "A counter resets to zero after a pod restart — is that a bug?" No, that's expected counter behaviour — if a graph looks wrong, the fix is using rate() or increase() instead of the raw value.
  • "What's the difference between a counter and a gauge in this context?" A counter only ever increases, until a restart resets it, and is meant to be queried with rate or increase; a gauge can go up or down and is graphed directly.
Say it like this"A counter resetting to zero on a pod restart is expected — it lives in that process's memory, and PromQL's rate and increase functions are specifically built to detect that reset and not treat it as a huge negative rate. If a graph is spiking on a restart, the actual bug is almost always that the panel is plotting the raw counter value instead of wrapping it in rate(), not that Prometheus or the app did something wrong."

---

Mark:
16Multi-cluster / multi-tenant monitoringScenario

The company now runs five Kubernetes clusters, and leadership wants one place to see the health of all of them, not five separate Grafanas.

What is actually happening (the mental model).

A single Prometheus server is normally scoped to one cluster it can reach directly — a true multi-cluster view requires either a federated/central layer that aggregates from each cluster's own Prometheus, or a long-term storage backend that already supports multi-source ingestion (Thanos/Cortex/Mimir again), rather than trying to point one Prometheus at five clusters' worth of targets directly.

How to reason about it.

  1. Keep a Prometheus per cluster for local scraping (so a network blip to one cluster doesn't blind you to the others), and use a central layer on top for the cross-cluster view — this mirrors the earlier long-term-storage answer, since both problems are solved by the same class of tool.
  2. Add a cluster label to every metric at the source (Prometheus's external_labels) so once data is aggregated centrally, queries can still filter or group by cluster.
  3. For genuine multi-tenancy (different teams who shouldn't see each other's data), look specifically at a system with tenant isolation (Mimir, Cortex) rather than a single shared Grafana with no access boundaries.

The root causes and trade-offs.

Trying to make one single Prometheus reach directly across every cluster is fragile, and it defeats the whole benefit of local, resilient scraping that per-cluster Prometheus gives you. The alternative, a Prometheus per cluster plus a central aggregation or long-term-storage layer on top, has more moving parts, but it's genuinely resilient and scalable. Multi-tenancy on top of that adds real access-control complexity that a single shared dashboard never had to solve.

The trap that less-experienced engineers fall into.

Trying to solve a multi-cluster view by pointing a single central Prometheus directly at every cluster's targets, which reintroduces a single point of failure and network dependency that per-cluster Prometheus was specifically avoiding.

🎯 Interviewer follow-up questions you should expect.

  • "How do you monitor five Kubernetes clusters from one place?" You run a Prometheus per cluster for resilient local scraping, plus a central federation or long-term-storage layer, like Thanos, Cortex, or Mimir, for the aggregated view.
  • "How do you keep cluster data distinguishable once it's aggregated?" You use external_labels to tag every metric with its cluster at the source.
Say it like this"I'd keep a Prometheus per cluster so a network issue to one cluster doesn't blind me to the others, and put a central layer like Thanos or Mimir on top for the aggregated cross-cluster view — it's the same class of tool as the long-term-storage answer, because both problems are really 'combine data from many sources without losing local resilience.' I'd also tag every metric with its cluster via external_labels so the aggregated view can still filter and group by cluster."

---

Mark:
17Monitoring was bolted on after an incident, not designed inScenario

Every alert and dashboard panel that exists today can be traced back to a specific past incident — nothing was set up proactively.

What is actually happening (the mental model).

Reactive-only monitoring means your coverage is a map of past failures, not a map of the system's actual risk surface — it's real evidence that monitoring has been treated as an incident-response afterthought rather than a design input alongside the system architecture itself, and it guarantees blind spots for any failure mode that hasn't happened yet.

How to work through it (shifting toward proactive).

  1. For any new service, define its key signals before launch — request rate, error rate, latency (the classic "RED" trio for request-driven services, or "USE" — utilisation, saturation, errors — for resources) — rather than waiting to see what breaks first.
  2. Use SLOs (service-level objectives) as a forcing function: deciding "99.9% of requests should succeed" up front makes you instrument and alert on the thing that actually matters to users, before an incident forces the question.
  3. Periodically review coverage against a checklist of common failure modes for the service's type (a database dependency, an external API call, a queue) instead of only after something breaks.
  4. Treat "we only just added this alert after an incident" as a signal worth tracking over time — a ratio of proactive-to-reactive additions is itself a useful health metric for the monitoring practice.

The root causes and trade-offs.

This usually comes from organisational pressure to focus only on what's visibly urgent, like fixing the last incident, rather than investing time in what's merely likely to happen next. It's made worse by genuinely not knowing what "good" monitoring looks like for a new service type, without a framework like RED, USE, or SLOs to structure the thinking. Proactive monitoring takes real upfront time that competes directly with feature work, and that's a trade-off worth making deliberately, not by default neglect.

The trap that less-experienced engineers fall into.

Treating "we have lots of alerts" as evidence of mature monitoring, without noticing that all of them are reactive patches for specific past incidents rather than proactive coverage of the system's actual risk surface.

🎯 Interviewer follow-up questions you should expect.

  • "How do you tell if monitoring is proactive or just reactive patchwork?" You check whether each alert or panel traces back to a past incident, versus being derived from a framework like RED, USE, or an SLO defined up front.
  • "What do you set up for a brand-new service, before anything has broken?" You set up RED metrics — rate, errors, duration — for request-driven services, or USE for resources, tied to an SLO that reflects what users actually need.
Say it like this"If every alert and panel traces back to a specific past incident, that's coverage of what's already happened, not of the system's actual risk surface. For a new service I'd rather start from RED or USE metrics and an SLO defined before launch, so instrumentation is driven by what matters to users, not by waiting to see what breaks first. I also treat the ratio of proactive-to-reactive additions as its own signal of whether the monitoring practice is actually maturing."

---

Mark:
18Choosing what to monitor for a brand-new serviceConcept

"Set up monitoring for this" — a new service is about to ship, and it currently has zero metrics, zero dashboards, and zero alerts.

What is actually happening (the mental model).

Starting from a blank slate is exactly where a framework earns its keep — rather than guessing which metrics matter, a small number of proven patterns (RED, USE, the four golden signals) tell you almost the entire starting checklist, and everything past that baseline is specific to what this particular service actually does.

The checklist (each item tied to a concrete signal).

  1. RED for the service itself — Rate (requests per second), Errors (error rate/count), Duration (latency, ideally as a histogram so you can pull p50/p95/p99, not just an average that hides tail latency).
  2. USE for anything it depends on as a resource — Utilisation, Saturation, Errors — for its host/pod (CPU, memory) and for any queue or connection pool it uses.
  3. Dependency health — if it calls a database or another service, track that call's latency and error rate separately from the service's own, so you can tell "we're slow" from "someone we depend on is slow."
  4. An SLO — a stated target (like "99.9% of requests succeed in under 300ms") that turns the RED metrics into a clear pass/fail an alert can be built on, instead of an arbitrary threshold picked with no reference point.
  5. Alerts on symptoms, dashboards for causes — page on the SLO being at risk (a symptom), and keep the RED/USE/dependency breakdowns as dashboards for diagnosing why, once a symptom alert has already fired.
  6. Business/custom metrics — whatever this specific service uniquely needs to prove it's doing its job (a queue depth, a job success count, a conversion counter) that no generic framework would know to include.

The trap that less-experienced engineers fall into.

Either shipping with zero monitoring "to add later" (which almost never happens until the first incident forces it), or over-instrumenting everything imaginable from day one without an SLO to anchor which of those metrics actually deserves an alert versus just existing quietly on a dashboard.

🎯 Interviewer follow-up questions you should expect.

  • "A new service ships tomorrow with no monitoring — what's the minimum you'd add?" You'd add RED for the service, USE for its resources, dependency-call tracking, and an SLO to anchor the first alert.
  • "Why prefer a latency histogram (p95/p99) over an average?" Because an average can look fine while a real minority of requests have much worse tail latency, which is exactly what users actually notice.
Say it like this"From a blank slate I start with RED for the service itself — rate, errors, and duration as a histogram so I get p95/p99, not just an average that hides tail latency — plus USE for whatever resources it depends on, and separate tracking for any external dependency call so I can tell 'we're slow' from 'something we depend on is slow.' Then I define an SLO so the first alert has an actual target behind it instead of an arbitrary number, and I keep alerting scoped to that symptom while the RED/USE breakdowns live on dashboards for diagnosing why, once something's already paged."

---

Mark:
🧠 How to turn this into muscle memory
  • Drill the reflex first — whether the data is arriving at all, whether the query actually measures the right thing, whether an alert would have fired, and whether this is a real incident or just a visibility gap — until it's automatic. Nearly every scenario above is this reflex applied to a specific corner of the stack.
  • Break and observe, in a real cluster. Kill a target's network path and watch it go stale on the dashboard; add a label with unbounded values to a test metric and watch series count climb; write a deliberately noisy alert and then fix it with a for duration; delete kube-state-metrics and watch which panels go blank. Reproducing each teaches more than reading it.
  • One scenario, three passes. Reason it out, then say the spoken version without looking, then explain it to a teammate — especially the silent-scrape, cardinality, and alert-fatigue stories, which come up constantly.
  • Keep an "incident → monitoring gap it revealed" log — the missing alert, the dashboard that lied, the metric that quietly renamed itself. These become the concrete stories you tell in the behavioural round.

Next in the course order: Project Management & JIRA, at this same depth.

No scenarios match that search. Clear search
Observability — DevOps Zero → Hero. Theory, Lab & Interview rebuilt from Day 42 (why observability, Prometheus architecture, Grafana, the three metric layers, kube-state-metrics, custom app metrics); the 18 scenario drills are the Scenario Playbook's Observability domain, rewritten in plain English. ← All chapters