Systeric / Docs
Open App →

How to Debug Performance

The scariest thing about a slow production system is the feeling that you’d need to already know the answer — that debugging is a talent some people have. It isn’t. Debugging is a loop, and the loop is what finds the answer. You do not need to know the landmine’s name before you step near it; the loop hands you the name.

This page is the loop. Every other article in this group — cardinality, indexes and read amplification, finding slow queries in ClickHouse, debugging with SigNoz — is just a smell this loop will eventually walk you into.

The mindset shift#

You are not trying to recall the cause. You are trying to corner it.

A senior engineer isn’t someone who memorised every failure mode. They’re someone who runs this loop fast and has seen enough smells to recognise a few on sight. Both of those are learnable. The loop you can learn today; the smells you pick up one incident at a time — and each article here is one incident, pre-packaged so you don’t have to earn it the hard way.

The loop#

1. Measure get a number 2. Locate where's the time? 3. Ask "why so much?" work vs. result 4. Change ONE thing test a hypothesis 5. Re-measure did the number move? still slow? loop again

1. Measure — replace the feeling with a number#

“It feels slow” is not debuggable. “The p99 latency panel takes 19 seconds to load over a 7-day range” is. The first move is always to turn the vibe into a number you can watch go up and down. If you can’t measure it, you can’t know whether you fixed it — you’ll just move on when it feels better, which is how bugs come back.

2. Locate — find where the time actually goes#

Do not guess which part is slow. Ask the system. Almost everything has a flight recorder that will tell you:

  • A database → its query log. In ClickHouse that’s system.query_log — see finding slow queries. It lists every query with its duration.
  • The app → traces. Open the slow request in SigNoz and look at the waterfall: which span ate the time?
  • A page → the browser’s Network and Performance tabs.

Locating is not solving. It’s narrowing “the system is slow” down to “this one query / span / call is slow.” You cannot fix what you cannot point at.

3. Ask “why is it doing that much work?”#

This is the heart of it, and it has a single golden signal:

Work done vs. result produced. A query that reads 15.5 million rows to return 20 is screaming that it’s doing 750,000× too much work — before you know why.

That ratio is the bug, even when you don’t yet have a name for the cause. High ratio → you ask the follow-up questions, and each one is just another query:

  • Reading the wrong (too-fine) data? → check which table it actually hit (rollups).
  • Not using the index? → EXPLAIN indexes = 1 (indexes).
  • Too many unique things to chew through? → count them (cardinality).

Notice: the cause (cardinality) was discovered by the loop. Nobody started the incident knowing the word. The loop walked us from “reads 15.5M rows” → “why?” → “there are 12,000 unique series” → “what makes them unique?” → “an unnecessary label.” Each arrow was one query.

4. Change exactly one thing#

Form one hypothesis and test only it. If you change three things and it gets faster, you’ve learned nothing — you can’t tell which one worked, and two of them might be quietly making it worse. One variable at a time is slower per step and far faster overall, because you actually learn.

5. Re-measure — did the number move?#

Go back to the number from step 1 and check. Not “does it feel better” — did 19s become 3s? If yes, you’ve confirmed both the cause and the fix. If not, your hypothesis was wrong (great — that’s information), revert it, and loop again. “CI is green” or “it loaded once” is not acceptance; the original number moving is.

A worked example (a real incident)#

This is the loop, verbatim, from a real “the dashboards are slow” report:

  1. Measure: the metrics dashboard took ~11s over 24h; traces and logs were fine. → metrics-specific.
  2. Locate: system.query_log showed the slow queries read ~158M rows from the raw samples table.
  3. Why so much? 158M rows for a chart with ~300 points → huge amplification. Follow-ups: which service emits all this? One service was exporting metrics every 1 second instead of every 60s — 60× too much data.
  4. Change one thing: fix that one service’s export interval.
  5. Re-measure: metric volume dropped ~98%; the 24h query fell from ~11s to ~2.5s.

No prior knowledge required at step 1. The loop produced the concepts (export cadence, then later cardinality and rollups) as it went.

The one-line version#

Measure → Locate → “why so much work?” → change one thing → re-measure. Everything else in this group is a smell you’ll recognise faster next time. You don’t need the map of every landmine. You need the metal detector — and this is it.