Learn Observability & Performance

Start from the symptom a user reports, find the signal that confirms it, and follow the evidence to the layer that is actually responsible — then prove the change worked. Every lesson names what its numbers depend on, because a performance claim without conditions is folklore.

Observability Fundamentals →

Understanding internal behavior from external signals. The measure-before-optimizing loop, what each signal type is actually good at, the golden signals, RED and USE, and how instrumentation reaches a backend at all.

Observability Is Not a Dashboard

Monitoring answers the questions you thought to ask when you built the dashboard. Observability is whether you can answer a question nobody anticipated — without shipping new code first. The test is not how many tools you run; it is what you can ask at 03:00.

Symptom · Something is clearly wrong — customers are complaining — and every dashboard is green. The only way forward anyone can suggest is to add logging and redeploy.
Measure Before You Optimize
▶ lab

The single most expensive habit in performance work is proposing a fix before taking a reading. This is the loop that replaces it: Problem → Measure → Locate → Understand → Change → Measure Again — and the six questions that turn "it is slow" into a specific reading at a specific layer.

Symptom · Within five minutes of "the API is slow", someone has proposed adding Redis, bumping the instance size, or increasing the thread pool — and nobody has yet looked at a number.
From Symptom to Root Cause

The diagnostic chain — Symptom → Signal → Measurement → Hypothesis → Evidence → Root Cause → Change → Validation → Regression Check — and the discipline that makes it work: write the hypothesis down before you look, so you can be wrong out loud instead of quietly.

Symptom · An investigation that has been running for an hour, has looked at fourteen dashboards, and cannot state what it currently believes or what would disprove it.
Metrics, Logs, Traces, Profiles

Four signals, four different questions. Metrics tell you something changed; traces tell you where the time went; logs tell you what exactly happened; profiles tell you what the CPU was doing. No single one explains an incident, and knowing which to reach for first is most of the speed.

Symptom · An investigation that reaches for logs first, greps 40GB, and finds nothing — because the question was "which service consumed the time", and logs are the wrong instrument for that question.
The Four Golden Signals

Latency, traffic, errors and saturation. Four numbers that describe almost any request-serving system well enough to know whether it is healthy and, when it is not, which direction to look. The value is not the list — it is that the four are read together.

Symptom · A service with either no dashboard or forty panels, where nobody can answer "is it healthy?" in ten seconds.
RED: Rate, Errors, Duration

Three numbers per request-handling service: how many, how many failed, how long they took. RED is the fastest way to make every service in a fleet legible in the same shape — and it goes blind the moment work stops being request-shaped.

Symptom · A fleet of thirty services where each team invented its own dashboard, so comparing two services during an incident means learning two vocabularies under pressure.
USE: Utilization, Saturation, Errors

For every resource, ask three questions: how busy is it, is work waiting for it, and is it failing? The middle question is the one that matters and the one most dashboards omit — which is why "CPU is only 40%" keeps getting offered as evidence that CPU is fine.

Symptom · Latency is high, every utilization chart looks moderate, and the team concludes there is no resource problem — while requests queue for a pool nobody charted.
Instrumentation: From Code to Signal

Telemetry does not appear; it is emitted by code, batched by a client, shipped to a collector and stored by a backend — and every hop can drop data, add latency or cost money. Knowing the path is what lets you trust the dashboard, and notice when it lies.

Symptom · A dashboard that went flat during the incident — and nobody can say whether traffic stopped, or the telemetry pipeline did.
OpenTelemetry Concepts

A vendor-neutral way to describe traces, metrics and logs, propagate context across process boundaries, and ship the result anywhere. Worth understanding as a set of concepts — signals, context, semantic conventions, collector — rather than as a product to install.

Symptom · Six services in four languages, each with its own tracing library, its own attribute names and no shared trace id — so a cross-service request cannot be reconstructed at all.
Metrics →

Counters, gauges and histograms as different questions. Why the average hides the outage, how to read p50 against p99, and why a user id in a metric label can take down the monitoring system.

Four Metric Types, Four Questions

A counter, a gauge and a histogram are not three ways to record a number — they are three different questions, decided at instrumentation time. Choosing wrong does not make the dashboard ugly; it makes the question permanently unanswerable, because the data you needed was never recorded.

Symptom · The dashboard is full of numbers, but nobody can answer "how many checkouts failed between 14:00 and 14:05" or "what did the slowest one percent of users experience".
Counters: The Slope Is the Signal

A counter only goes up, which makes the raw value almost useless and its slope almost everything. The two things that go wrong: graphing the total instead of the rate, and mishandling the reset that happens every time the process restarts.

Symptom · A dashboard shows a line climbing steadily to the right. It climbed yesterday too. Nobody can tell from it whether anything is currently wrong.
Gauges: Blind Between Scrapes

A gauge reports whatever the value was at the instant of the scrape. That is exactly right for queue depth and resident memory, and exactly wrong for anything that spikes — because a spike shorter than the scrape interval can leave no evidence that it ever happened.

Symptom · Users report a burst of timeouts at 14:32. Every gauge on the dashboard — memory, connections, queue depth — looks completely normal across that minute.
Histograms: A Distribution You Can Afford to Keep Forever
▶ lab

A histogram stores counts per bucket instead of individual observations, which is what makes fleet-wide percentiles possible at all. The two decisions that determine whether it is useful: where you put the bucket boundaries, and whether you understand that every percentile it reports is an interpolation.

Symptom · You need p99 for the whole service. Each instance reports its own quantiles, they disagree wildly, and there is no defensible way to combine them into one number.
The Average Was Fine and Users Were Not
▶ lab

Five requests at 50, 55, 52, 48 and 3000ms have a mean of 641ms — a number no single request experienced. The mean is the wrong summary for latency because one extreme value drags it away from everything, and it cannot distinguish "everyone is slightly slow" from "one user in a hundred is unusable".

Symptom · Support tickets say the app is unusable. The average latency graph is flat and green across exactly the same window.
Percentiles: Which One, and How Many Users Is That?

p50 describes the typical user, p99 describes the worst-served one percent, and the distance between them describes the system. The two errors that matter: reading a percentile without knowing the traffic volume behind it, and assuming percentiles compose across a call chain. They do not.

Symptom · p99 on a low-traffic endpoint swings between 200ms and 4s hour to hour with no deploys, while a service whose dependencies all report a healthy p99 is itself slow at p99.
Cardinality: The Label That Took Down Monitoring

Cardinality is the product of every label's distinct value count, and it multiplies. One `user_id` label turns a three-series metric into three million, and the first thing that breaks is the monitoring system you were relying on to tell you what broke.

Symptom · Dashboards time out. Metric ingestion lags behind real time, then drops data. The monitoring system's own memory usage climbs until it restarts — during the incident you deployed it to diagnose.
Label Sets That Survive a Year

A label set is a schema: bounded value sets, names that mean the same thing in every service, and a migration path for the day you need to change one. Get it wrong and you either cannot join across services or cannot afford the series you created.

Symptom · Two services both report request latency, but one labels it `path` and the other `endpoint`, one uses `svc` and the other `service` — so no dashboard can compare them, and the fleet-wide SLO query has to special-case each one.
Logs →

Discrete events with enough context to reconstruct a failure: structured fields over prose, levels that mean something, correlation ids that survive every hop, and the secrets that must never reach log storage.

Structured Logging: Fields a Program Can Read

A log line is either a sentence a human greps or a record a program queries. The difference decides whether "how many payment timeouts hit provider X in the last hour" takes ten seconds or an afternoon of regex archaeology.

Symptom · During an incident, answering "how many checkouts failed and why" means a chain of greps, cuts and sorts against free-form text — and the answer is only as good as the regex someone wrote under pressure.
Log Levels Are a Convention, Not a Standard

Nothing in any specification says what `warn` means. What it means is whatever your team decided, written down or not — and when it was never written down, everything becomes `info`, the error rate becomes unmeasurable, and the level field stops carrying information.

Symptom · Filtering to `level=error` produces both real outages and routine validation failures, so nobody filters on it. Meanwhile a genuine failure is sitting at `info` because the author was not sure.
Correlation IDs: Turning Lines Into a Story

Without a shared identifier, logs from five services are five unrelated piles sorted by time. With one id propagated through every hop — and stored in a dedicated field — they become one request's story, and the log line becomes a doorway into the trace.

Symptom · A customer reports a failed checkout at 14:03. There were 68,000 checkouts that minute, and no way to tell which log lines across six services belong to theirs.
What You Just Wrote Into a Log Half the Company Can Read

Log storage has a wider read audience, a longer retention and weaker access controls than the database the data came from. A token logged once is a token in a search index, in backups, and in whatever third-party service ships your logs — and no rotation policy knows it is there.

Symptom · A routine audit finds live bearer tokens in the log search index. Nobody logged them deliberately — a debug line dumped a request object, and the header came along.
The Log Bill and What It Is Buying

Log cost scales with traffic while its debugging value does not — the ten-thousandth identical success line teaches nothing. Sampling is how you keep the value and drop the volume, and the rule that makes it safe is simple: never sample what you would need during an incident.

Symptom · Log ingest costs more than the compute running the service. Traffic doubles and the bill doubles with it, while the questions the logs can answer stay exactly the same.
Distributed Tracing →

Where a request spends its time across services. Spans and their relationships, context propagation through queues, the waterfall view, critical-path reasoning, and N+1 as a visible shape.

Where the Request Actually Went
▶ lab

Metrics tell you the endpoint got slower. A trace tells you which of the eleven things it touched got slower. One request, one timeline, every hop measured — and usually one span holding 80% of the budget that nobody suspected.

Symptom · Checkout "feels slow". The endpoint dashboard confirms p99 rose from 140 ms to 500 ms, and every individual service dashboard looks normal, because no single service owns the request.
Trace, Span, Attribute, Status

A span is a timed operation with a parent, a status and a bag of attributes. Which facts belong in attributes, which belong in span events, and which belong in a metric instead is the difference between a trace you can query and a very expensive log line.

Symptom · The traces exist but nobody queries them: spans are named `handler`, carry either nothing useful or an entire request body, and the one attribute you need to filter by — tenant, route, cache hit — was never recorded.
Parents, Children and Links

Nesting is a claim about causality and containment: a child span asserts its parent was waiting for it. Get that wrong — most often by making a queued job a child of the request that enqueued it — and the waterfall stops describing anything real.

Symptom · A trace whose root span is four minutes long for an endpoint that returns in 80 ms, or child spans that extend past the end of their parent, or an entire background job missing from the trace it obviously belongs to.
Carrying the Trace Across the Gap

Trace context travels in-band with the work: a header on the HTTP call, a field on the queue message, an argument to the job. Every hop that forgets to carry it cuts the trace in half — and the caller looks like it was idle for 400 ms.

Symptom · Traces that stop at a service boundary: the caller has a 400 ms span with no children and no explanation, while somewhere else a second trace starts from nowhere with no idea who caused it.
Reading the Waterfall

Six shapes cover most of what a waterfall can tell you: the staircase, the comb, the fat leaf, the gap, the overhang and the cliff. Learning to recognize them turns trace reading from scrolling into diagnosis.

Symptom · You have the trace open and 40 spans in front of you, and no idea which one is the finding. Everything looks like it takes some time.
The Critical Path Is the Only Path That Pays

In a fan-out, only the slowest branch controls when the request finishes. Optimizing any other branch produces a beautiful graph in your dependency dashboard and zero improvement for users — until the critical path moves, and then a different branch matters.

Symptom · A dependency was optimized from 64 ms to 10 ms, its own dashboard shows a 6× improvement, and end-to-end p99 did not move by a millisecond.
The Comb: N+1 as a Visible Shape

One query to fetch the users, then one query per user to fetch their orders. Every individual query is fast, every dashboard is green, and the endpoint takes 268 ms because it made 101 round trips instead of 2.

Symptom · An endpoint that got slower as data grew, with a database that reports low CPU, fast queries and no slow-query log entries. Latency scales with the number of items on the page.
Sampling Without Throwing Away the Evidence

At 10,000 requests a second, tracing everything is a second production system. Sampling is inevitable; the question is whether you keep a random 1% — which discards almost every slow and failed request — or keep the ones that matter.

Symptom · Either a tracing bill that rivals compute, or — after someone reduced it — an incident where every slow request the customer reported has no trace, because uniform sampling kept the boring ones.
Profiling →

Where cost goes inside one process. CPU and allocation profiles, reading a flame graph without fooling yourself, and the CPU-bound versus I/O-bound distinction that decides which fix can possibly work.

When the Trace Runs Out of Answers

The trace says `pricing-service` spent 240 ms and has no children. That is where tracing stops and profiling starts: one tells you which process is expensive, the other tells you which function inside it is.

Symptom · A fat leaf span: one wide bar in the waterfall with nothing underneath it, in a service that should not need 240 ms to do its job.
Self Time, Total Time, and Where the CPU Went

A CPU profile ranks functions two ways, and confusing them wastes afternoons. Total time says "this subtree is expensive"; self time says "this function is expensive". Only one of them tells you where to put the fix.

Symptom · CPU utilization near saturation, latency climbing with load, and a profile whose top entry is `main` at 100% — technically true and completely useless.
Reading a Flame Graph
▶ lab

Width is time, height is stack depth, and the horizontal axis is not time at all. Getting that last part wrong is the single most common flame-graph misreading, and it makes people look for patterns that cannot exist.

Symptom · A flame graph is open, it looks like a mountain range, and every frame seems equally plausible as the culprit.
Allocation Rate Is a Cost Even Without a Leak

Memory that is allocated and immediately freed never shows up as growth, so leak hunting finds nothing. It still costs: every megabyte allocated is a megabyte the collector must eventually walk, and at 500 MB/s that is where your latency went.

Symptom · Steady resident memory, no leak, but GC CPU share in double digits and a latency histogram with a second bump caused by collection pauses.
Computing or Waiting?

The first fork in every performance investigation. A CPU-bound service wants better algorithms or more cores; an I/O-bound service wants concurrency, batching or a faster dependency. Applying either fix to the other problem reliably makes things worse.

Symptom · Latency is high. That is all you know so far, and the next decision — profile the code or chase the dependency — depends entirely on this distinction.
Always-On Profiling, and the Diff That Finds Regressions

Profiling during an incident means capturing a baseline you do not have, on an instance that may be healthy, after the pathology has passed. Continuous profiling makes the baseline a query — and turns "did this release get slower" into a diff.

Symptom · CPU per request crept up 30% over six releases, nobody can say which one caused it, and the profile you would need to compare against was never taken.
Latency, Throughput & Queueing →

The physics of a loaded system: tail latency, latency budgets, Little's Law as working intuition, why queueing makes systems slow long before they fail, and what saturation actually means.

Latency Is a Distribution, Not a Number

The dashboard says 120 ms and users say it is slow. Both are right: "the latency" was never one number. Response time decomposes into service time and wait time, and almost every production surprise lives in the waiting.

Symptom · Support tickets say the app is slow. The service dashboard shows average latency flat at 120 ms and nobody can reproduce the complaint.
Tail Latency: Why p50 Being Fine Does Not Help

A one-in-a-hundred slow response sounds harmless until a page makes 40 calls, a user makes 30 page views, and every dependency has its own one-in-a-hundred. Rare events compose, and at scale the tail becomes the typical experience.

Symptom · p50 is 40 ms and stable. Users describe the product as "randomly slow", and the complaints do not correlate with any single endpoint or customer.
Latency Budgets: Spending 200 Milliseconds on Purpose
▶ lab

A latency target is only useful once it is divided. Give every hop an allowance, add them up, and the conversation changes from "make it faster" to "the payment provider is spending 45% of our budget and we have 15 ms left".

Symptom · The team agreed on a 200 ms target months ago. Nobody can say whether any individual service is within budget, so every latency conversation ends in "we should all be faster".
Throughput: The Number That Means Nothing Without a Latency Bound

"We handle 10,000 requests per second" is not a capability claim until you say at what latency and with what error rate. Push a system to its maximum throughput and you will find the maximum is a place nobody wants to operate.

Symptom · A capacity test reports the service peaks at 10,000 rps. In production the same service becomes unusable at 6,000 rps, and nobody can reconcile the two numbers.
Little's Law as Working Intuition
▶ lab

Concurrency equals throughput times latency. Three lines of arithmetic size a connection pool, expose an impossible capacity claim, and turn a queue depth into a wait time — which is most of what the law is for.

Symptom · Someone proposes a pool size, a thread count or a capacity number and nobody can say whether it is roughly right. The numbers are chosen by copying another service or by doubling until the errors stop.
Queueing: Why Systems Get Slow Before They Get Broken
▶ lab

Load rises 20% and latency rises 400%. Nothing errored, no code changed, no dependency degraded. A queue formed — and queues turn a linear increase in arrivals into a non-linear increase in waiting.

Symptom · Traffic grew from 4,000 to 5,000 requests per second over a week. p99 went from 180 ms to 2.4 seconds. Error rate is still near zero and every dashboard shows resources "not fully utilized".
Saturation: The Reading Utilization Cannot Give You

Utilization says how busy a resource was. Saturation says how much work could not be served immediately. A CPU at 55% with twelve threads waiting for eight cores is not half idle — it is oversubscribed, and only one of those two numbers says so.

Symptom · Latency is bad. CPU is at 55%, memory is at 60%, disk is at 40%, network is at 20%. Every resource chart looks fine and the team concludes it must be the application code.
Concurrency Limits: An Unbounded Server Is a Slower Server

Accepting every request that arrives feels generous and produces the worst possible outcome: everything is slow, everything times out, and the capacity is spent on work nobody is still waiting for. A limit is a latency control, not just a safety valve.

Symptom · Under a traffic spike the service does not reject anything, but every request takes 30 seconds and nearly all of them time out. Throughput measured in *useful* responses drops close to zero.
Timeouts: The Latency Contract Nobody Writes Down

A timeout is a statement about how long a caller will wait before deciding the answer is worthless. Set it too short and you manufacture load; too long and you tie up capacity waiting for work that stopped mattering minutes ago.

Symptom · A dependency slows from 200 ms to 3 s. The service does not merely slow down: its error rate goes to 40%, its own upstream starts timing out, and load on the already-struggling dependency triples.
CPU, Memory, Disk & Network →

The four resources a process competes for, the signal that identifies each as the constraint, and the difference between a memory leak and a cache nobody bounded.

What "CPU Is At 60%" Actually Means

A CPU number without a denominator is not a measurement. Sixty percent of how many cores, against which cgroup quota, counting which of user, system, iowait and steal — and is anything actually waiting for a core?

Symptom · A CPU chart that everyone points at during incidents and nobody can act on: it is high during good periods and low during bad ones, and two engineers read the same line differently.
CPU Saturation: When Cores Become the Queue

Throughput stops rising, latency bends upward, and the run queue grows. Confirming CPU as the constraint takes three readings; the causes range from an O(n²) loop to logging in the hot path to a lock everything spins on.

Symptom · Latency climbs steeply once traffic passes some level, and adding load stops adding throughput. Below the knee everything looks healthy; above it, p99 degrades far faster than p50.
Algorithmic Cost in a Request Handler

An O(n²) loop over a collection that grew is a CPU bottleneck that scales with data, not traffic — which is why it passes load tests and fails in production. And the counterpoint: Big-O does not price cache locality, branch prediction or constants.

Symptom · Latency for one endpoint is fine for most users and terrible for a few, and the slow ones are consistently the accounts with the most data. Traffic-based load tests never reproduce it.
Reading Memory: RSS, Heap, Working Set and the Number on Your Dashboard

The memory number everyone watches is usually the wrong one. Resident, virtual, heap, cache and cgroup working set answer different questions, and allocation rate — the one nobody charts — often matters more than any of them.

Symptom · A memory chart sitting at 92% that has been there for months without incident, next to a service that gets OOM-killed while "memory usage" reads 60%.
Memory Leaks: Growth That Does Not Come Back
▶ lab

Stable workload, rising memory, and a sawtooth of OOM restarts. Confirming a leak takes a trend under steady load; finding it takes two heap snapshots and a diff of what is still reachable.

Symptom · Memory climbs steadily over hours or days under unchanged traffic, latency degrades as the runtime collects more often, and eventually the process is killed and restarts with a clean slate — repeatedly, on a schedule.
Leak or Unbounded Cache? The Question That Picks the Fix

Both grow, both end in an OOM kill, and they need opposite fixes. Three questions separate them: does the growth correspond to data you would use again, is there an eviction policy, and does usage stabilize?

Symptom · Memory climbs under load and does not come back down. The team splits into "we have a leak" and "that is just the cache warming up", and both camps can point at the same chart.
Disk and Storage: Latency, Throughput, IOPS and the fsync Tax

Three numbers that people use interchangeably and should not: latency per operation, bytes per second, and operations per second. Plus the one that dominates write-heavy systems and appears on no dashboard by default — fsync.

Symptom · Requests are slow, CPU is largely idle with elevated iowait, and the database or the service that writes files is the common factor across every slow trace.
Network Signals: Is It the Network, or the Service on the Other End?

Connection setup can cost more than the request it carries. RTT, bandwidth, retransmits, handshake counts and pool waits each answer a different question — and the first one to answer is whether the network is involved at all.

Symptom · Calls to a downstream service are slower than that service says it is. Its own latency chart shows 12ms; your client-side chart shows 180ms, and both teams believe their own numbers.
Database & Cache Performance →

Diagnosing the storage layer from the outside: slow-query workflow, scan versus index, lock waits with idle CPU, pool saturation, replication lag, hit rates that lie, stampedes and hot keys.

Which Signal Actually Means "The Database Is Slow"

Nine numbers all get reported as "the database is slow" and they mean completely different things. Query duration measured at the application, split by statement, is the one that confirms it — and database CPU is the one that misleads most often.

Symptom · Application p99 climbs, the widest spans in every trace are database calls, and the incident channel fills with "it is the database".
The Slow Query Workflow
▶ lab

Capture the statement, read the plan against reality, find where the estimate diverged, then decide which layer the fix belongs to — index, query, schema or application. Adding an index before reading the plan is guessing with extra steps.

Symptom · One endpoint's p99 has degraded, traces point at a single statement family, and the engine agrees the statement itself is slow.
An Index Scan Is Not Automatically Faster

The planner chooses a sequential scan over an index for good reasons: selectivity, table size, cache residency and the cost of random page access. Forcing the index because "indexes are fast" is the most confidently made wrong optimization in database work.

Symptom · A query is slow and the plan says `Seq Scan`, which looks like an obvious smoking gun to anyone who has read that indexes make queries fast.
When the Join Strategy Is the Bottleneck

Nested loop, hash join and merge join are each optimal somewhere and catastrophic elsewhere. The planner picks one from a row estimate, so a wrong estimate does not make the query slightly slower — it makes the engine choose an algorithm built for a different problem size.

Symptom · A query with a join takes seconds instead of milliseconds, and the time is concentrated in one join node rather than spread across scans.
Low CPU, High Latency: Lock Contention

The database is 20% busy and every request takes four seconds. Nothing is overloaded — transactions are standing in line for the same rows. This is the shape that defeats capacity-based reasoning, because adding hardware makes the queue longer, not shorter.

Symptom · p99 climbs into seconds, throughput plateaus or falls, and every utilization dashboard — application CPU, database CPU, disk, network — looks comfortable.
Connection Pool Saturation: Waiting in Front of an Idle Database

A hundred concurrent requests, twenty connections, eighty in line. The database is 35% busy and every trace blames it, because the pool wait happens inside the span labelled "database" and outside anything the database can measure.

Symptom · Request latency rises sharply once traffic passes a threshold, database CPU stays moderate, and latency is roughly proportional to concurrency rather than to query complexity.
Replication Lag: Reads That Are Correct and Stale

Replicas turn read capacity into a purchase, and the price is time. Lag is not a failure until the application assumes it is zero — and every read-after-write bug in a replicated system is that assumption meeting reality.

Symptom · A user updates something, the next screen shows the old value, and it is intermittent, unreproducible locally, and worse under load.
A 95% Hit Rate Tells You Almost Nothing

Hit rate is a ratio, and the thing that hurts you is a volume weighted by cost. The right question is never "how high is the hit rate" — it is which objects miss, how expensive each miss is, and how much load the misses put on whatever is behind the cache.

Symptom · The cache dashboard is green, the hit rate is high, and the database behind it is nonetheless under more load than anyone expected.
Cache Stampede: Everyone Misses at Once
▶ lab

One popular key expires and ten thousand concurrent requests discover the miss simultaneously. Each one dutifully queries the database to repopulate it. The database receives ten thousand copies of the same query, and the cache that was protecting it becomes the mechanism that overloads it.

Symptom · A sharp, near-instantaneous spike in identical database queries, correlated with nothing in the deploy log, often repeating at a regular interval matching a TTL.
Hot Keys: When Aggregate Metrics Hide a Saturated Node

Sharding distributes keys, not traffic. One product goes viral, forty percent of requests land on one key, and the node holding it saturates while the cluster reports comfortable average utilization across every other node.

Symptom · p99 latency is bad while p50 is normal; cluster-wide CPU and memory look comfortable; one node in the fleet is pinned and the others are idle.
Queue & Worker Performance →

Arrival rate against service rate, why depth alone is the wrong alarm, oldest-message age as the honest signal, retry storms that feed themselves, and worker pools that saturate quietly.

Six Queue Signals, Two That Wake You Up
▶ lab

Arrival rate, processing rate, depth, oldest-message age, retry volume and dead-letter volume. Depth is the number everyone graphs and the number that explains the least; the rate pair tells you whether you are falling behind, and age tells you whether a human is already suffering.

Symptom · Nothing is erroring. Nothing is timing out. Users say exports "take a while now", and support has three tickets asking where a confirmation email went.
The Backlog Arithmetic: Four Levers and a Drain Time

10,000 jobs/s arriving, 8,000/s processed, backlog growing at 2,000/s. The gap is arithmetic, not opinion — and there are exactly four things you can do about it. Time-to-drain is the number to put in the incident channel.

Symptom · Backlog climbing steadily on a graph that only goes up. Jobs still complete, still succeed, just later and later. Someone asks "when will it catch up?" and nobody has a number.
Depth Is Not an Emergency; Age Is

A million tiny jobs and ten thousand hour-long jobs produce wildly different dashboards from the same word, "backlog". Depth measures accumulation; oldest-message age measures how long a human has been waiting. Only one of them belongs on a pager.

Symptom · A depth graph with an alarming shape, and no way to tell whether it means "busy afternoon" or "the payments reconciliation queue has been stalled since lunch".
Retry Storms: The Load You Generated Yourself

A dependency gets slower, clients retry, the retries become load, the dependency gets slower still. The feedback loop is what turns a 5% error rate into an outage — and it is the one source of traffic you can switch off yourself.

Symptom · A dependency's error rate rises modestly, then its latency climbs, then request volume against it *doubles or triples* while nothing upstream changed, and it stops responding entirely.
Twenty Workers, All Busy, Five Hundred Waiting

Every worker is occupied and the queue is 500 deep, so the obvious move is more workers. Whether that helps depends entirely on what the workers are busy *doing* — and if they are waiting on a shared dependency, adding workers makes things worse.

Symptom · Worker utilization pinned at 100%, queue depth climbing, job latency rising. The autoscaler wants to add capacity and the dashboard seems to agree.
Runtime Performance →

Garbage collection, event-loop lag, interpreter overhead, allocation cost and warm-up — labelled per runtime, because none of this generalizes across JS, Python, Go, the JVM and C++.

Garbage Collection: Pause, Throughput, Footprint — Pick Two
▶ lab

A collector trades pause time against throughput against memory footprint, and no tuning flag escapes the triangle. The lever you actually control is not the collector — it is how much garbage your code produces per request.

Symptom · p50 latency is flat and healthy. p99 shows regular spikes of tens or hundreds of milliseconds, on a rhythm rather than at random, and no trace span accounts for the gap.
Event-Loop Lag: One Callback, Everybody Waits

A single-threaded event loop runs one callback at a time. A 200ms JSON parse does not just make that request slow — it delays every other pending task by 200ms, including the health check that is about to fail.

Symptom · All endpoints degrade together, including trivial ones like `/health`. The slowdown does not correlate with any single route's traffic, and per-request CPU looks unremarkable.
JavaScript Runtime Performance: V8 Where It Costs

Serialization, allocation and shape changes dominate real server-side JavaScript cost far more often than algorithmic choices. The engine optimizes aggressively for predictable code and deoptimizes quietly when you surprise it.

Symptom · CPU high, latency rising with load, and a profile whose top frames are engine internals and serialization rather than any function the team wrote.
CPython Performance: The Interpreter Tax and the GIL

CPython pays a per-operation interpreter cost that no algorithm change removes, and its global lock means CPU-bound threads do not run in parallel. Neither fact makes Python slow at the thing most services actually do, which is wait.

Symptom · CPU pinned at roughly one core's worth regardless of how many worker threads are configured. Throughput flat. Adding threads increases memory and context switching and nothing else.
C++ Memory Performance: Allocation, Copies and Locality

No collector means no pauses and no free lunch: cost moves to allocator behaviour, fragmentation, and copies the language will make for you silently. And on modern hardware, where your data sits usually matters more than how many instructions you execute.

Symptom · Tail latency spikes with no collector to blame, resident memory that grows and never returns under stable load, and a profile where `malloc`, `free` and copy constructors appear above business logic.
JIT and Warm-Up: The First Thousand Requests Are a Different Program

A JIT-compiled runtime starts interpreted and speeds up as it observes what the code actually does. That makes early requests slower, benchmarks without warm-up meaningless, and freshly-scaled instances a source of tail latency nobody attributes correctly.

Symptom · Latency spikes right after a deploy or a scale-out event, decaying over seconds to minutes. Benchmarks report numbers production never reproduces, in either direction.
Frontend Performance →

The user's half of the latency budget: the browser waterfall, Core Web Vitals as user-experience signals, JavaScript cost beyond bytes, images, and layout work that blocks the first paint.

The Half of the Budget You Cannot See From the Server

Your p99 is 80ms and users still call the app slow. Server time is one line item in a budget that also contains DNS, TLS, render-blocking CSS, JavaScript parse and execute, and an image decode on a phone three years older than your laptop.

Symptom · Support tickets and survey answers say "slow" while every backend dashboard is green. The complaint clusters on mobile, on first visits, and on the marketing-heavy routes.
Core Web Vitals as Signals, Not Scores

LCP asks "did anything useful appear?", INP asks "did the page answer when I touched it?", CLS asks "did it move while I was reading?". They are proxies for three different user frustrations — and they are web-platform definitions that have already changed once.

Symptom · A dashboard shows a vitals score that is either reassuring or alarming, and nobody can say which specific user experience it corresponds to or what would move it.
Reading the Browser Waterfall
▶ lab

HTML discovers CSS, CSS blocks the paint, script blocks the parser, and the image nobody prioritised is fetched last. The waterfall shows a dependency chain — and the difference between a resource being downloaded and the page being usable.

Symptom · The page shows blank or unstyled content for seconds. Individual resources all download quickly when inspected on their own, so nothing looks obviously wrong.
JavaScript Costs Four Times, Not Once

A 400KB bundle is not one cost. It is downloaded, parsed, compiled and executed — and gzip only helps with the first of those. The last three are CPU on a device you did not choose and cannot upgrade.

Symptom · Transferred bytes drop after a compression or CDN change, and time-to-interactive on real devices barely moves. The gap between "loaded" and "responds to taps" stays stubbornly wide.
Images: The Largest Bytes, Rarely the Largest Block

Images are usually most of a page's weight and rarely the thing holding up interaction. They are also where the cheapest wins live — serving a 3000px photo into a 400px slot is a mistake that costs nothing to fix and shows up immediately in LCP.

Symptom · Page weight is dominated by images. LCP is poor on mobile. Compressing everything produces a smaller page and a barely-changed LCP.
Layout, Paint and the Main Thread

One thread runs your JavaScript, computes layout, paints, and handles the user's tap. A 300ms task anywhere in that list means a 300ms wait everywhere else in it — which is why "the page freezes when I scroll" and "my handler is slow" are the same bug.

Symptom · Taps and scrolls feel sticky or delayed. Animations stutter. The page is fully loaded, so no loading metric shows anything wrong.
Distributed Systems Performance →

What changes when the work crosses machines: fan-out and tail amplification, sequential versus parallel dependency calls, cross-region propagation delay, jitter, and the cost of coordination.

What Changes When Work Crosses a Machine

An in-process function call costs nanoseconds and either returns or throws. The same call across a network costs milliseconds, serialises both ways, waits in three queues you cannot see, and has a third outcome: no answer at all.

Symptom · The same business operation that took 40ms in one process takes 400ms across services, and the sum of the individual service timings does not account for the difference.
Fan-Out: Waiting for the Slowest of Seven
▶ lab

Call seven services in parallel and wait for all of them, and your latency is not the average — it is the maximum. A dependency that is slow one time in a hundred becomes a request that is slow seven times in a hundred, which is how p99 problems become p93 problems.

Symptom · Aggregating service latency dashboards suggests the page should render in 120ms. Real request p99 is over a second, and no single dependency looks responsible.
Sequential or Parallel: Same Work, Different Latency

Four dependency calls take 740ms in a chain and 300ms fanned out. The parallel version is not simply better: it triples the instantaneous load on everything downstream and turns one failure into four things to reason about at once.

Symptom · A handler makes four downstream calls one after another. Latency is roughly the sum of all four, and each call spends its time waiting rather than computing.
Cross-Region Latency Is Physics, Not Configuration

Light in fibre travels about 200,000 km/s. Frankfurt to Virginia and back is roughly 13,000 km of that, so no amount of tuning gets a round trip under about 65ms — and a request that crosses the Atlantic four times has spent a quarter of a second before doing any work.

Symptom · Latency is strongly bimodal by user geography. The slow group is fine on cached pages and terrible on anything that writes, and no service in the path reports elevated latency.
Packet Loss Buys You a Timeout, Not a Retransmit

A link that drops one packet in a thousand looks almost perfect on an average-latency graph. What it actually does is give one request in a few hundred an extra couple of hundred milliseconds, which is invisible at p50 and dominates p99.

Symptom · p50 is flat and p99 is spiky and irregular. The slow requests are not correlated with any particular endpoint, tenant, or piece of code, and they do not reproduce in testing.
Agreement Costs Round Trips

Every guarantee that several machines agree on something is paid for in round trips. A quorum write is at least one; consensus is more; a distributed lock is two plus however long the holder keeps it. The guarantee is often worth it — the cost is never zero.

Symptom · A logically trivial operation takes tens or hundreds of milliseconds. CPU is idle everywhere, no query is slow, and latency scales with the number of participants rather than with the amount of data.
Capacity & Cost →

How much load the system can take, how much headroom is left, whether autoscaling arrives in time, and what a request actually costs — capacity and efficiency as separate questions.

Capacity Planning: Traffic to Machines
▶ lab

Average traffic, a peak multiplier, per-request cost and a latency target become a instance count through five multiplications — each one an assumption you can name, challenge and re-measure. The output is an estimate, and saying so is what makes it useful.

Symptom · Nobody in the planning meeting can say what happens at three times current traffic. The last scaling decision was "double it and see", and the one before that was an incident.
Headroom: The Capacity You Deliberately Do Not Use

Running at 100% utilization means every burst, every failed instance and every deploy becomes an incident. How much headroom is a real decision with a real cost, and the number comes from burst shape, scale-up time and blast radius — not from a convention.

Symptom · The service is comfortable at steady state and falls over during deploys, instance replacements and modest traffic bumps — events that are all entirely predictable.
Autoscaling: Scaling on the Right Signal

An autoscaling policy is a claim about what your bottleneck is. Scale on CPU and you have claimed the service is CPU-bound; when it is actually waiting on a database, the policy never fires while users time out.

Symptom · Latency climbs and requests time out, but the fleet size never changes. The scaling dashboard shows the policy at rest, well below its threshold, throughout the entire incident.
Autoscaling Lag: The Gap Where the Outage Lives

Between a traffic spike and a new instance serving real traffic sit five delays: the metric window, the evaluation interval, provisioning, boot, and warm-up. Add them up honestly and you often find the spike ends before the capacity arrives.

Symptom · Traffic spikes, latency degrades for two to four minutes, then recovers. The scaling dashboard shows instances arriving right around the time things got better, and everyone credits autoscaling for a recovery it did not cause.
Cost per Request: The Other Performance Metric

Compute, database, cache, bandwidth, third-party calls and inference add up to a number per request. Track it next to latency and a whole category of "optimizations" reveals itself as buying a small latency win with a large permanent bill.

Symptom · The infrastructure bill grows faster than traffic. Nobody can say which endpoint is expensive, and every performance improvement so far has involved adding a replica, a cache tier or a bigger instance.
Capacity or Efficiency: Which Problem Are You Solving?

Capacity asks how much load the system can take. Efficiency asks how much resource each unit of work consumes. A system can scale beautifully while wasting most of what it buys — and the two problems have different fixes, different costs and different urgency.

Symptom · The fleet keeps growing to keep up with traffic. Each scale-out works, so nothing looks broken, but the ratio of machines to users has been drifting upward for a year and nobody planned for that.
Load Testing & Benchmarking →

Producing numbers that mean something: load-test shapes, coordinated omission, benchmark hygiene, microbenchmark versus end-to-end, and telling a real regression from noise.

Load Testing: What Question Is This Test Answering?

Baseline, load, stress, spike and soak are five different tests answering five different questions. Most load tests fail before they start — against a warm cache, an empty database and one hot key, they measure a system that does not exist.

Symptom · The service passed its load test at 5,000 requests per second and fell over in production at 1,800. Everyone believed the number, and the number was measuring something else.
Load Test Shapes: The Shape Is the Hypothesis
▶ lab

Ramp, spike, step and soak are not stylistic choices. Each shape exposes a different failure: ramps find the knee, spikes find scaling lag and cold starts, soaks find leaks and drift. Choosing a shape is choosing what you are willing to find out.

Symptom · Every load test is the same steady rate for ten minutes, every load test passes, and production keeps failing in ways the tests never reproduced.
Coordinated Omission: When the Load Generator Lies

A load generator that waits for each response before sending the next one stops sending requests exactly when the system stalls. The requests that would have been slowest are never issued, never measured, and the reported p99 can be an order of magnitude better than what users experience.

Symptom · Load test percentiles look excellent and production percentiles for the same traffic level are far worse. The test is not obviously misconfigured, the request rate matches, and the numbers still disagree by a factor of ten.
Benchmarking: Does This Number Answer My Question?

A benchmark is an experiment, and most benchmarks fail as experiments before they fail as measurements. Warm-up, environment, workload realism, repetition, variance and a baseline are the difference between a number you can act on and a number you can quote.

Symptom · Two engineers benchmark the same change and get contradictory results. Both are confident, both ran the benchmark correctly, and nobody can say which number to believe.
Benchmark Fallacies: Confident Numbers That Are Wrong

Different environments, no warm-up, unrealistic payloads, averages without variance, several variables at once, and measuring something the system never actually does. Each produces a decisive number, and each is a reason to refuse to act on it.

Symptom · A benchmark result is circulating that justifies a large change. The number is specific, the chart is clean, and something about the setup does not survive a second look.
Microbenchmark or End-to-End: Why p99 Did Not Move

The function got 40% faster and the request did not. A microbenchmark measures one operation in isolation; an end-to-end benchmark measures the system with its contention, I/O and queueing. Each is misleading when asked the other's question.

Symptom · A well-measured optimization ships. The microbenchmark improvement is real and reproducible. The service's latency percentiles are indistinguishable from the week before.
Regression or Tuesday? Telling a Real Change from Noise

p95 moved from 180 ms to 260 ms. Before filing the bug, establish that both numbers answer the same question: same traffic mix, same data, same environment, enough samples. Then compare the difference against the noise you already know your measurement has.

Symptom · A dashboard or CI benchmark shows latency up 40% after a release. Half the team is convinced it is the release, the other half has seen this number move on its own, and nobody has the evidence to settle it.
SLOs & Alerting →

Turning user experience into a measurable objective: SLIs, SLOs and SLAs kept distinct, error budgets as a decision tool, alerts worth waking up for, burn rates, and dashboards built around questions.

SLIs: Measuring What the User Actually Feels

An SLI is a ratio: good events over valid events. The hard parts are not the arithmetic — they are deciding what counts as good, what counts as valid, and where in the request path you measure, because each choice moves the number by more than most outages do.

Symptom · Every dashboard is green, CPU is comfortable, error rate reads 0.02% — and support is fielding complaints that the app "does not work". Nobody can say who is right, because nothing on the wall measures the user's experience.
SLOs: A Target, a Window, and a Reason
▶ lab

An SLO is an SLI plus a target plus a window: "99% of checkouts complete under 300ms over 28 days". The target is not an aspiration — it is a commitment about how much unreliability you are willing to pay to avoid, and 100% is always the wrong answer.

Symptom · Reliability discussions have no shared vocabulary. One person says the service is "pretty stable", another says it is "constantly breaking", and both are looking at the same graph. Every reliability-versus-features argument restarts from zero.
SLAs: The Promise With Money Attached

An SLA is a contract: a reliability promise to a customer with a defined consequence when you break it. It is deliberately looser than your internal SLO, it is written by people who are not on call, and alerting on it means you find out you owe refunds at the same moment as your legal team.

Symptom · The SLA says 99.9%, so the team sets the internal target to 99.9%, and now every breach of the internal objective is simultaneously a contractual breach with financial consequences. There is no room left to be merely degraded.
Error Budgets: Unreliability You Are Allowed to Spend

If the objective is 99.9%, then 0.1% of failure is not a defect — it is a budget. Treating it as something to spend rather than something to avoid turns every reliability-versus-velocity argument into an arithmetic question, which is the only version of that argument anyone ever wins.

Symptom · The reliability conversation is a standoff. Engineering wants to stop feature work and harden the system; product wants to ship. Both positions are asserted with conviction and neither is supported by a number, so the argument is settled by whoever is more senior.
Alerts Worth Waking Someone For

An alert is a claim that a human should stop what they are doing right now. `CPU > 80%` is not that claim — it is a fact about a machine that may or may not correspond to a user having a bad time. Alert on symptoms users feel; keep causes on dashboards where they belong.

Symptom · The on-call phone fires four times a night. Three of the four resolve themselves before anyone opens a laptop. The fourth is a real outage that arrives among the noise and gets acknowledged twenty minutes late.
Alert Fatigue: The Page Nobody Reads

Alert fatigue is not a morale problem, it is a detection failure. Every page that turns out to be nothing raises the probability that the next real one is acknowledged late, investigated slowly, or dismissed entirely — and the arithmetic that produces it is measurable.

Symptom · Nobody looks at the alerts channel. Pages are acknowledged and closed without investigation. When a genuine outage arrives, the first response is "is that another flapping one?" — and the answer takes ten minutes to establish.
Burn-Rate Alerts: How Fast Is the Budget Going?

Alerting on "error rate above 1%" picks a threshold with no relationship to what you promised. Alerting on burn rate asks a better question: at the current failure rate, how long until the error budget is gone? Fast burn pages, slow burn tickets, and two windows each stop the alert from lying.

Symptom · Either the alert fires constantly on brief error spikes that consume a negligible slice of budget, or it stays silent through a week of low-grade degradation that quietly consumes all of it. Both come from the same cause: a fixed threshold that knows nothing about the objective.
Dashboards Built Around Questions

A dashboard is not a place to put metrics — it is a tool for answering a specific question under time pressure. Two hundred charts is not thoroughness, it is an unindexed archive, and at 03:00 the difference between eight charts in the right order and two hundred in no order is the difference between five minutes and fifty.

Symptom · During an incident the responder scrolls through a wall of charts looking for the one that matters. Everyone has a personal favourite panel. The dashboard is comprehensive and nobody can use it, so debugging happens in ad-hoc queries typed from memory.
Incident Debugging →

Evidence-based diagnosis under pressure: reading a timeline, separating correlation from causation, watching the bottleneck move after every fix, and the trade-offs that make a system faster but worse.

Debugging an Incident in Progress

Mitigation and diagnosis are different jobs, and doing them in the wrong order costs users minutes they never get back. Stabilize first, then form a hypothesis you can disprove in two minutes instead of browsing dashboards hoping something looks odd.

Symptom · A page fires, or support escalates: requests are slow or failing, and a channel full of people is proposing causes faster than anyone can check them.
Correlation Is Not the Root Cause

Two charts moved together at 14:03. So did four others. Establishing that one caused another needs a mechanism you can state, or an intervention you can run — and during an incident you usually have time for exactly one of them.

Symptom · Everything on the dashboard turned red within the same two minutes, and each person in the channel is confident about a different one of them being the cause.
Reading a Timeline: Observation Order Is Not Causal Order

The timeline shows database latency rising at 12:08 and API p99 rising at 12:10. That ordering is a property of your alert thresholds and scrape intervals as much as of the system — and the first thing you observed is routinely not the first thing that happened.

Symptom · A reconstructed incident timeline where every entry looks like it could be the trigger, and the team argues about which one to fix.
"What Changed?" — Deploy Markers and the Invisible Deploys

The highest-yield first question in any performance incident is what changed, and it is only answerable in seconds if changes appear on the same time axis as the metrics. Code deploys are the easy part; config pushes, feature flags and someone else's release are the ones that leave no mark.

Symptom · A step change in a performance metric with no obvious trigger, and a team that spends twenty minutes asking around in chat to establish whether anything shipped.
The Bottleneck Moves After Every Fix
▶ lab

You removed the CPU bottleneck and the system is still slow — because the constraint moved to the database, where it had been hiding behind the CPU limit all along. This is what success looks like, and predicting the next constraint is what separates a plan from a sequence of surprises.

Symptom · A carefully validated optimization lands, the targeted resource is now comfortably below its limit, and end-to-end latency improved far less than the arithmetic promised.
Every Optimization Buys Something and Sells Something

Caching buys database load and sells freshness. Compression buys bandwidth and sells CPU. Batching buys throughput and sells latency. There is no move that is purely faster — and the ones that appear to be are usually selling reliability quietly.

Symptom · A performance win that produces a new class of problem weeks later: stale data, higher bills, occasional data loss, worse tail latency, or an outage that the "optimization" made possible.
Performance and Observability Anti-Patterns

Every one of these is a plausible move that a competent engineer makes under pressure, and every one shares a single property: no measurement before, or no measurement after. That is the tell, and it is the only thing they have in common.

Symptom · Optimization work that ships regularly and produces no durable improvement in user-facing latency, alongside monitoring that grows continuously and answers fewer questions each quarter.
AI & Agent Performance →

The newest latency budget: time to first token, tool-call chains, step counts, retries and context size — where agent latency, cost and quality trade against each other.

Where an Agent Run Actually Spends Its Time

An agent run is a chain of network round trips nobody wrote explicitly: model call, tool call, model call, tool call. The wall clock is dominated by that chain, so step count is the variable that matters most and the one least likely to appear on a dashboard.

Symptom · Users watching a spinner for a task that "should be quick", with wide run-to-run variance: the same question takes 8 seconds once and 45 the next time.
Inside One Model Call: Queue, First Token, Generation

A model call is three different waits with three different causes. Provider scheduling you cannot control, time to first token that scales with your prompt, and generation that scales with your output — and only two of those are yours to shorten.

Symptom · Model calls that take seconds with wide variance, and no way to tell whether the provider is busy, the prompt is too large, or the response is too long.
Reading an Agent Run as a Trace
▶ lab

Model 2.2s, search 0.8s, model 1.6s, database tool 0.2s, model 1.4s — 6.2 seconds in a straight line. The question a waterfall answers is which of those steps are sequential because the data requires it, and which are sequential because that is the order the model happened to emit them.

Symptom · An agent run whose duration is the sum of its parts, with every step waiting for the previous one even when several of them are independent lookups.
What One Agent Run Costs, and Which Term Dominates

Cost per run is tokens times price times steps — and the tokens term grows every step, because each tool result is appended to a context that every subsequent call must pay for again. That quadratic-ish growth is why long runs cost far more than their step count suggests.

Symptom · A model spend line that grows faster than usage, with no single expensive feature to point at — and per-run costs that vary by an order of magnitude for superficially similar requests.
The Agent Returned 200 OK and the Answer Was Wrong

Rate, errors and duration all look healthy while the agent confidently tells a customer something untrue. Traditional service metrics measure whether the machinery ran; agent systems need metrics for whether the task was actually accomplished.

Symptom · Healthy dashboards, no alerts, and a steady trickle of support tickets about answers that were wrong, incomplete, or confidently invented.