# Optimizing Real Systems

> Putting measurement to work end to end: run a disciplined optimization loop against a target, learn where the time actually goes in real systems, and ship speed safely in production without breaking correctness.


---

# Optimizing Real Systems

You've measured something. Maybe you profiled a slow endpoint, or you finally wired up tracing and now you can see where requests spend their time. Good. But there's a gap nobody talks about between *having data* and *making the system faster* - and it's a gap people fall into for weeks. They tweak, they tune, they rewrite a clever loop, and the graph that matters doesn't move. Or it moves, and a week later nobody can say why, or whether it was worth it.

This guide is the capstone. It assumes you can already take a measurement (if you can't yet, start with [Profiling 101](/guides/profiling-101) and [Observability: Logs, Metrics, Traces](/guides/observability-logs-metrics-traces)) and it teaches the harder thing: how to turn measurement into durable speed. The disciplined loop that keeps you on track. Where the time actually goes in real-world systems, ranked, so you look in the right place first. And how to make a system faster in production - under real traffic, watching the right numbers - without trading away the correctness and readability you'll regret losing.

The thread running through all of it: **the fastest code is the work you never do.** Most real wins come not from making the work faster but from doing less of it.

## How to read this

- **Already deep in an optimization that isn't paying off?** Jump to [Phase 1: The Optimization Loop](01-the-optimization-loop.md) - you're probably missing a baseline or a target, and that's the whole problem.
- **Don't know where to look first?** [Phase 2: Where the Time Actually Goes](02-where-the-time-goes.md) ranks the usual suspects so you start at the top, not the bottom.
- **Want it to finally make sense?** Read in order - each phase builds on the last.

## The phases

1. **[The Optimization Loop](01-the-optimization-loop.md)** - measure, find the bottleneck, form one hypothesis, change *one* thing, re-measure, repeat - against a target you set in advance, and stop when you hit it.
2. **[Where the Time Actually Goes](02-where-the-time-goes.md)** - the real-world bottlenecks ranked: the database, the network, I/O and serialization, then CPU and algorithms - and the biggest lever of all, doing less work.
3. **[Optimizing Safely in Production](03-optimizing-safely-in-production.md)** - verify with real traffic and observability, watch percentiles not averages, and steer clear of the classic traps: micro-optimizing a cold path, optimizing the wrong layer, and trading correctness for speed you didn't need.

> Deliberately out of scope: the *mechanics* of taking a measurement. This guide is about what to do with the measurement once you have it. For flame graphs, sampling profilers, and reading a trace, see [Profiling 101](/guides/profiling-101) and [Observability: Logs, Metrics, Traces](/guides/observability-logs-metrics-traces).


---

# The Optimization Loop

Optimization feels like it should be a burst of cleverness - spot the slow thing, rewrite it, done. In
practice the developers who reliably make systems faster are the boring ones. They don't out-clever the
problem; they run a loop, and they refuse to skip steps. The cleverness, when it's needed at all, comes
at the very end.

Here's the secret that makes the whole thing work: **optimization is a measurement problem wearing a
coding problem's clothes.** The hard part isn't writing the faster version - it's knowing *what* to make
faster, *whether* it worked, and *when* to stop. Get those three right and the code mostly writes
itself. Get them wrong and you can spend two weeks shaving microseconds off a function that runs once a
day.

## The loop, top to bottom

Every optimization that actually lands walks the same circle:

```mermaid
flowchart TD
  T["① set a TARGET<br/>'p95 checkout < 300ms' (once, up front)"] --> B["② measure a BASELINE<br/>'p95 is 1200ms'"]
  B --> F["③ find the BOTTLENECK<br/>'70% is in the DB call'"]
  F --> H["④ form ONE hypothesis"]
  H --> C["⑤ change ONE thing"]
  C --> R{"⑥ re-measure:<br/>hit target?"}
  R -->|no| F
  R -->|yes| STOP(["STOP"])
```

Notice what's at the top and what's at the bottom: a target you decide *before* you start, and a hard
stop the moment you reach it. Everything in between is what people think of as "optimizing" - the two
ends are what separate a focused afternoon from a lost fortnight.

## Set a target before you touch anything

**What it actually is.** A target is a specific, measured number that defines "done." Not "make it
faster" - *"p95 checkout latency under 300ms"* or *"the nightly report finishes before 6am."* It names
the metric, the threshold, and ideally the conditions.

**Why people get this wrong.** "Make it faster" feels like a goal, but it has no finish line - there's
always another millisecond, and *faster than what, by how much, is that enough?* stay undefined. A
target converts an open-ended craving into a yes/no question - and your permission to **stop**. When p95
hits 280ms against a 300ms target, you're done; go ship something else. Performance work has steeply
diminishing returns, and the target is what stops you grinding past the point anyone cares.

⚠️ **Gotcha - optimizing without a baseline or a target is how you lose weeks.** This is the single most
common way performance work goes wrong. With no baseline, you can't prove anything got faster - "it
feels snappier" is not a result. With no target, you can't tell when to stop, so you keep going until
you run out of patience or break something. Decide both before you write a line of code.

## Measure the baseline

**What it actually is.** The baseline is the system's performance *right now*, measured the same way
you'll measure after each change - the "before" photo. Every claim of improvement compares against it,
so if it's sloppy, every later number is meaningless.

**What it does in real life.** You run the workload and record the number that matters - and the
*distribution*, not just one figure, because real latency is lumpy (much more on this in
[Phase 3](03-optimizing-safely-in-production.md)).

```console
$ ./bench checkout --requests 5000
checkout latency over 5000 requests:
  p50   180ms
  p95  1200ms
  p99  2100ms
```
*What just happened:* You now have a concrete "before." The typical request (p50) is fine at 180ms, but
the tail (p95, p99) is dragging - and since your target is p95 under 300ms, that tail is exactly the
problem. Without this measurement you'd be guessing at which end to fix.

⚠️ **Gotcha - measure in conditions that resemble production.** A baseline taken on your laptop against
an empty database tells you almost nothing about a system serving real traffic against fifty million
rows. The bottleneck on a tiny dataset is often *completely different* from the one in production.
Measure against realistic data volume and concurrency, or your baseline is a fairy tale.

## Find the bottleneck - then attack only it

**What it actually is.** The bottleneck is the one part of the system responsible for the largest share
of the time you care about. In almost every system, time is wildly unevenly distributed - a small
fraction of the code accounts for most of the wall-clock cost.

**Why this matters so much.** Amdahl's law is the math behind the intuition: the speedup from optimizing
a component is capped by how much of the total time it uses. A function that's 5% of your runtime,
made *infinitely* fast, speeds up the whole system by at most 5% - so optimizing anything but the
bottleneck is, almost by definition, a waste of effort.

```text
   where the time goes in one checkout request:

   DB query   ███████████████████████████████████  70%   ◄── the bottleneck
   serialize  ████████                              16%
   app logic  ████                                   8%
   network    ███                                    6%

   (illustrative breakdown - your real split comes from a profiler or trace)
```

*What just happened:* The breakdown says 70% of the time is one database query. Spend your week
hand-optimizing the app logic (8%) instead, and the best possible outcome is an 8% improvement - still
missing target by a mile. The profiler just told you where the only meaningful win lives. (Producing
this breakdown is the subject of [Profiling 101](/guides/profiling-101) and
[Observability: Logs, Metrics, Traces](/guides/observability-logs-metrics-traces).)

💡 **Key point.** Don't optimize what's *slow in isolation*; optimize what's *expensive in total*. A
function that takes 2ms but runs ten thousand times per request beats a function that takes 200ms but
runs once. The bottleneck is about total cost, not per-call cost.

## Form one hypothesis, change one thing

**What it actually is.** A hypothesis is a falsifiable guess about cause and effect: *"The query is slow
because it's doing a full table scan; adding an index on `user_id` will bring it under 300ms."* It names
what you think is wrong and what you'll do about it, so a re-measurement can confirm or refute it.

**Why change exactly one thing.** Add an index, rewrite the query, *and* add a cache all at once, and
the number improves - which change did it? You don't know; maybe two helped and one hurt and they
happened to net out. One change per loop is what lets the re-measurement actually *mean* something -
slower per step, far faster overall, because you never untangle a knot of interacting changes.

🪖 **War story.** A teammate once "optimized" a slow page with six changes in one afternoon - new index,
query rewrite, added cache, tweaked serializer, bumped connection pool, changed timeout. The page got
faster. Two weeks later it was *slower than the original*, and nobody could undo the regression because
nobody knew which change was load-bearing and which was secretly harmful. They reverted the whole batch
and started over, one at a time. The one-thing rule isn't bureaucracy - it's what makes your work
reversible and your conclusions trustworthy.

## Re-measure, then decide

**What it does in real life.** You make your one change and run the *exact same* measurement as your
baseline - same workload, same conditions - and compare.

```console
$ ./bench checkout --requests 5000
checkout latency over 5000 requests:
  p50   120ms
  p95   260ms     (was 1200ms)
  p99   340ms     (was 2100ms)
```
*What just happened:* The index dropped p95 from 1200ms to 260ms, under target. The hypothesis is
confirmed, the target is met - so you **stop**. You don't chase the next 20ms; it isn't worth a day of
your life and the risk of a new bug. If the number *hadn't* moved, discard that hypothesis and loop back
to "find the bottleneck" with what you just learned.

⚠️ **Gotcha - a change that doesn't help is still information, so keep it only if it's free.** If your
hypothesis was wrong and the change made no difference, revert it. Dead "optimizations" that don't
measurably help are just complexity you'll pay for forever in readability. Carry forward only the
changes that earned their place on the graph.

## Recap

1. **Set a target first.** A specific measured number ("p95 < 300ms") defines done and gives you
   permission to stop. No target means no finish line.
2. **Measure a baseline** in production-like conditions, capturing the distribution - it's the "before"
   every later claim compares against.
3. **Find the bottleneck.** Amdahl's law: optimizing anything but the largest cost is capped at a tiny
   payoff. Attack total cost, not per-call slowness.
4. **One hypothesis, one change, re-measure.** Changing one thing per loop is what makes the result
   interpretable and reversible.
5. **Stop when you hit the target.** Diminishing returns are real; the next millisecond rarely pays for
   its risk.
6. ⚠️ **Optimizing with no baseline and no target is the classic way to lose weeks.** Both, up front,
   every time.

Next: now that you have the loop, where should you point it? The usual real-world bottlenecks, ranked,
so you look in the right place first.


---

# Where the Time Actually Goes

When developers guess where their system spends its time, they guess wrong with remarkable consistency.
The instinct is to suspect the code you can see - the loop you wrote, the algorithm you're a little
embarrassed by. But in a real, networked, data-backed system, your CPU-bound code is usually a rounding
error next to the time spent *waiting*: on the database, on another service, on a disk or a serializer
chewing through a payload.

So here's the mental model: **the bottleneck is almost always at a boundary.** Every time your request
crosses from your code into something else - the database, the network, the disk - it can lose a
surprising amount of time there. The sections below walk those boundaries in roughly the order they
bite, most common culprit first. Point your loop from [Phase 1](01-the-optimization-loop.md) at the top
of this list, because that's where the odds are.

```text
   MOST OFTEN THE REAL BOTTLENECK
   ─────────────────────────────────────────────────────────
   1. The DATABASE        N+1 queries, missing indexes
   2. The NETWORK         too many round trips, payloads too big
   3. I/O & SERIALIZATION reading/writing, JSON encode/decode
   4. CPU & ALGORITHMS    the code you actually wrote
   ─────────────────────────────────────────────────────────
   LEAST OFTEN - but where everyone looks first

   …and cutting across all of them, the biggest lever:
   ★ DOING LESS WORK      caching, batching, not asking twice
```

(Ordering reflects what's commonly the bottleneck in typical data-backed web systems; your system may
differ - which is exactly why you measure first.)

## 1. The database - usually the whole story

In a data-backed application, the database is the bottleneck more often than everything else combined.
Two problems account for most of it, and both are invisible until you look.

**The N+1 query.** This is the classic, and it's everywhere. Fetch a list of N things, then loop over
them firing one more query per item for a related thing - a page load becomes 1 query plus N. With 100
items that's 101 round trips, each cheap alone and ruinous in bulk.

```text
   N+1 (101 round trips):              batched (2 round trips):

   SELECT * FROM posts            ──▶  SELECT * FROM posts
   then, for EACH post:                SELECT * FROM authors
     SELECT * FROM authors                WHERE id IN (1,2,3,…,100)
       WHERE id = post.author_id
   × 100 separate queries
```
*What just happened:* The N+1 version asks the database the same shape of question a hundred times,
paying the round-trip cost each time. The batched version asks once for all posts and once for all their
authors - two round trips total. The fix (often called *eager loading* in ORMs) is usually a single line
telling the ORM to load the relation up front. ORMs make N+1 *easy to write by accident*, which is
exactly why it's so common.

**The missing index.** A query with no usable index makes the database read every row to find the ones
you asked for - a *full table scan*. Unnoticeable on a small table; on a large one it's the difference
between a millisecond and several seconds, and it worsens as the table grows. The fix is an index on the
columns you filter and join by.

Both of these - and how to find them with `EXPLAIN` and fix them properly - are the subject of
[Why Is My Query Slow?](/guides/why-is-my-query-slow). When your trace points at the database, go there
next.

💡 **Key point.** Before you suspect anything else, count your queries. A page that should run 2 or 3
queries and is somehow running 200 has told you exactly what's wrong, and the fix is usually cheap.

## 2. The network - death by a thousand round trips

Once you've ruled out the database, the next boundary is the network, with the same two flavors of
problem: *too many trips* and *too much per trip*.

**Chatty calls.** Every call to another service pays a fixed latency tax - the travel time there and
back - regardless of how much work the other side does. Twelve sequential calls to render one page means
paying that tax twelve times, in series, while the user waits for all of it. This is the network's
version of N+1. The fix is fewer, fatter trips: batch small requests into one, fetch in parallel when
calls don't depend on each other, or move the work closer to the data it needs.

```text
   chatty - 12 sequential calls:        batched / parallel:

   call ─wait─▶ call ─wait─▶ call …     one batched call ─wait─▶
   └── you pay the round trip 12×       OR all 12 fired at once,
       one after another                   you wait for the slowest one
```
*What just happened:* The chatty version adds twelve round-trip taxes back to back. Batching collapses
them into one tax; parallelizing means you wait for the *slowest* call instead of the *sum* of all of
them. Same work, dramatically less waiting.

**Payload size.** The other network cost is moving bytes. Sending a megabyte of JSON when the client
needs three fields wastes time serializing, transferring, and parsing it. Asking only for the fields you
use, paginating large lists, and enabling compression all cut the bytes in motion - which shades into
the next boundary, since turning objects into bytes and back is itself work.

## 3. I/O and serialization - the cost of crossing the wire

**What it actually is.** *I/O* is any time your program reads or writes outside its own memory - a disk,
a socket, a file. *Serialization* is converting in-memory objects into a format that can travel or be
stored (JSON, Protobuf), and *deserialization* converts it back. Both are pure overhead: not your
business logic, just the tax for talking to anything outside itself.

**What it does in real life.** Serialization is sneaky - spread thin across everything, rarely one
obvious slow line - but encoding and decoding large JSON payloads on every request adds up, especially
for big responses or high request rates. If a trace shows time in JSON parsing, the cure is usually to
*move less* (smaller payloads, fewer fields) or *serialize less often* (cache the serialized form).

⚠️ **Gotcha - synchronous I/O blocks more than the one request.** A blocking read or write doesn't just
slow its own request; in many runtimes it ties up a thread or worker that could have served someone else,
so one slow disk read can ripple into latency for unrelated requests. This is why blocking I/O on a hot
path is more dangerous than its raw duration suggests.

## 4. CPU and algorithms - last, not first

**What it actually is.** This is the work people *picture* when they hear "optimization": the loop, the
sort, the data structure, the algorithm with the wrong big-O. CPU-bound work is real and sometimes
dominates - image processing, large in-memory computation, a genuinely quadratic algorithm on a growing
input.

**Why it's last on the list.** In a typical data-backed web service, the CPU is mostly *waiting* - for
the database, the network, the disk. The actual compute is a small slice, which is why hand-optimizing
application logic before ruling out the boundaries above is the classic misallocation: polishing the 8%
and ignoring the 70%.

**When it *is* the bottleneck**, the highest-leverage fix is almost never micro-optimization. It's a
better algorithm - turning an O(n²) into an O(n log n), or an O(n) lookup into an O(1) hash lookup -
because that changes how the cost *scales*, not just its constant factor. Finding which function
actually burns the CPU is what a sampling profiler is for; that's
[Profiling 101](/guides/profiling-101).

💡 **Key point.** Algorithmic wins beat micro-optimizations because they change the slope of the curve.
A faster constant factor helps today; a better complexity class helps forever, and more as the input
grows.

## The biggest lever: do less work

Every boundary above is something you can make *faster*. But there's a move that beats all of them:
don't do the work at all.

**What it actually is.** Caching is storing the result of an expensive operation so the next request
gets the answer without redoing the work. If a query, computation, or API call produces the same answer
many times, do it once and serve the saved result to everyone after.

**Why it's the highest-leverage tool.** Making a database query 2× faster is good. *Not running it at
all* - because the answer is already cached - is effectively zero time. Caching doesn't optimize the
work; it deletes it, at any layer: a query result, a serialized payload, a rendered fragment, a
downstream API response.

⚠️ **Gotcha - caching trades freshness for speed, on purpose.** A cached answer can be stale until it
expires or you invalidate it - that's the deal you're signing. The hard part of caching was never the
cache; it's deciding when to throw entries away so users don't see stale data. The full treatment,
including invalidation strategies, is in [Caching Explained](/guides/caching-explained).

Caching is the headline, but "do less work" is bigger than caching: batching turns N operations into 1,
pagination fetches 20 rows instead of 20,000, lazy loading skips work the user never asks for, computing
a value once beats recomputing it in a loop. The pattern underneath all of them is the closing idea of
this guide: the cheapest, fastest, most reliable work is the work you found a way to avoid.

## Recap

1. **The bottleneck lives at a boundary** - where your code waits on something else - far more often
   than in your own CPU-bound code.
2. **The database is the most common culprit:** N+1 queries (fix by batching/eager loading) and missing
   indexes (fix with an index to avoid full scans). See
   [Why Is My Query Slow?](/guides/why-is-my-query-slow).
3. **The network is next:** too many round trips (batch, parallelize) and payloads too big (fewer
   fields, pagination, compression).
4. **I/O and serialization** are a thin, pervasive tax - cut bytes moved and serialize less often;
   beware blocking I/O on hot paths.
5. **CPU and algorithms come last,** and when they matter, a better complexity class beats
   micro-optimization. See [Profiling 101](/guides/profiling-101).
6. ★ **Doing less work is the biggest lever.** Caching deletes work rather than speeding it up - at the
   cost of freshness. See [Caching Explained](/guides/caching-explained).

Next: you've made the change and the benchmark looks great. Now make sure it's actually faster *in
production*, for real users - watching the right numbers, avoiding the traps that make a "win"
worthless.


---

# Optimizing Safely in Production

Your benchmark says the change is faster. That's a good sign and it is not the finish line, because a
benchmark is a controlled, simplified, optimistic version of reality. Production has cold caches, noisy
neighbors, weird data, traffic spikes, and a long tail of slow requests your benchmark never generated.
The change that's faster on your laptop can be neutral - or worse - under real load.

So the mental model for this final phase is: **a benchmark result is a hypothesis about production, and
production is the only judge.** Verifying there, with real traffic, watching the right numbers, is what
turns "looks faster" into "is faster." We'll close with a few traps that can make even a real, measured
speedup not worth what you paid for it.

## Verify with real traffic and observability

**What it actually is.** Verifying in production means watching your live system's performance metrics
*before and after* the change ships, on real user traffic - using the observability you already have
(or should). The benchmark proved the change *can* help; the production metric proves it *does*.

**Why benchmarks lie by omission.** A benchmark runs one workload, usually warm and uniform. Production
runs every workload at once: the user with ten items and the one with ten thousand, the cache-cold first
request after a deploy, the request that hits the one overloaded shard. Your optimization might help the
common case and hurt a rare-but-important one - only production traffic exercises all of it.

**What it does in real life.** You ship behind a flag or to a fraction of traffic, then watch the same
metric you targeted in [Phase 1](01-the-optimization-loop.md) move on the live dashboard.

```console
$ # checkout p95 latency, before vs after the index change rolled out
14:00  p95  1180ms   ← before
14:05  p95  1205ms
14:10  p95   270ms   ← change reaches 100% of traffic
14:15  p95   265ms
```
*What just happened:* The live metric confirms what the benchmark predicted - p95 dropped to ~270ms
under real traffic, matching the target. Now it's a result, not a hope. If the line *hadn't* dropped,
you'd have learned your benchmark wasn't representative - worth knowing before declaring victory.
Standing up the metrics and traces that make this visible is the subject of
[Observability: Logs, Metrics, Traces](/guides/observability-logs-metrics-traces).

⚠️ **Gotcha - ship it observably, not blindly.** Rolling a performance change to 100% of traffic with no
way to compare before/after and no quick rollback is how a "speedup" becomes an incident. Use a feature
flag, a canary, or a staged rollout so you can see the effect on a slice first and back out fast if it
regresses something.

## Watch percentiles, not averages

This is the single most important measurement idea in production performance, and it's where averages
quietly betray you.

📝 **Terminology.** A *percentile* describes the slow end of your distribution. *p95 latency* is the
value that 95% of requests come in under (so the slowest 5% are above it); *p99* is the value 99% beat.
*Tail latency* is shorthand for these high percentiles - the experience of your unluckiest requests.

**Why the average is a trap.** An average smears all your requests into one number and hides the slow
tail completely. A system can have a beautiful average and still be making a meaningful fraction of
users miserable.

```text
   1000 requests, two ways to describe them:

   AVERAGE:  ~110ms   ← looks great, ship it!

   reality:
     950 requests   →   50ms     (fast, the happy majority)
      50 requests   → 1200ms     (the slow tail - 1 in 20 users)

   p50  =   50ms      ← the typical request
   p95  = 1200ms      ← what 1 user in 20 actually feels
```
*What just happened:* The average (~110ms) makes the system look healthy while 1 in 20 requests takes
over a second. The percentiles tell the truth: the typical user is fine (p50 = 50ms) but the tail is bad
(p95 = 1200ms). Optimizing toward the average might make the *fast* requests slightly faster - improving
the number you're watching - while the suffering 5% stay exactly as miserable. Averages tell you to fix
the wrong thing.

💡 **Key point.** Set your target on a percentile (p95 or p99), not an average. The tail is where real
users hit timeouts, abandon carts, and file complaints - and at scale, "the slowest 1%" is a lot of
people. Optimize the experience of your unluckiest users, because the average user was already fine.

## The traps that make a win worthless

You can run the loop perfectly, measure accurately, verify in production - and still waste your effort or
do harm. Three traps account for most of it.

### Trap 1 - micro-optimizing a cold path

Pouring effort into code that runs rarely or isn't on the critical path - shaving microseconds off a
function that runs once at startup, or once a day in a background job nobody's waiting on. It *feels*
productive, since you made something measurably faster. But per Amdahl's law from
[Phase 1](01-the-optimization-loop.md), speeding up code that's a tiny fraction of the time anyone
experiences buys a tiny fraction of improvement. Only optimize what the measurement says is expensive
*on a path that matters*. Cold path, hot effort, cold result.

### Trap 2 - optimizing the wrong layer

Fixing a symptom at the layer where you noticed it, not the layer where it's caused: application-side
caching papering over an N+1 query instead of fixing it, scaling up web servers when the database is the
bottleneck, compressing payloads when the real cost was the twelve round trips that produced them. The
symptom moves but the cause stays, so the problem returns - often bigger, now wrapped in extra machinery
that hides the real fix. Trace the time to its *source* (that's what
[Phase 2](02-where-the-time-goes.md) is for) and fix it there - a missing index fixed in the database
beats a cache bolted onto a query that should never have been slow.

🪖 **War story.** A team kept adding read replicas to a slow database, baffled it stayed slow - the
bottleneck was writes, not reads, and replicas don't take writes off the leader. Months of effort at the
wrong layer, until one person measured the read/write split, saw it was write-bound, and the strategy
changed. Measure *which* layer before you optimize *a* layer.

### Trap 3 - trading correctness or readability for speed you didn't need

Reaching for the fast-and-dangerous version - the clever bit-twiddle, the cache with a subtle staleness
bug, the hand-rolled concurrency, the unreadable one-liner - to win speed that nobody asked for and no
target required. This is the deepest trap because the cost is deferred and compounding: a correctness
bug traded for speed is a future incident with your name on it, and an unreadable "optimization" is a
tax every teammate pays every time they touch that code - the next person, not understanding it, will
eventually reintroduce the slowness or a new bug. Donald Knuth's warning applies here: *"premature
optimization is the root of all evil"* (Knuth, *Structured Programming with go to Statements*, 1974) -
optimizing before you've measured, for a target you don't have, sacrifices clarity to chase speed you
may never need.

💡 **Key point.** Speed you didn't need, bought with correctness or clarity you did, is a net loss even
though the number went down. The target from [Phase 1](01-the-optimization-loop.md) is your defense: if
you've hit it, stop - don't risk a bug for a millisecond no one will notice.

## The fastest code is the work you avoid

Pull the whole guide together and it collapses into one idea. The optimization loop keeps you disciplined
about *what* to change and *when* to stop. Knowing where the time goes points you at the boundaries -
database, network, I/O, then CPU - instead of your guesses. Production verification and percentiles make
sure the win is real for real users. And the traps remind you that a faster number isn't automatically a
better system.

But the thread under all of it, the thing that produces the biggest wins again and again, is the same:
**the fastest code is the work you avoid.** The query you don't run because it's cached. The round trip
you don't make because you batched. The rows you don't fetch because you paginated. The computation you
don't repeat because you saved the answer. You will get further by deleting work than by speeding it up
- and you'll sleep better, because work that doesn't happen can't be slow, can't be wrong, and can't
wake you at 2am.

## Recap

1. **A benchmark is a hypothesis; production is the judge.** Verify on real traffic with observability,
   behind a flag or canary so you can compare and roll back. See
   [Observability: Logs, Metrics, Traces](/guides/observability-logs-metrics-traces).
2. **Watch percentiles, not averages.** Averages hide the slow tail; set targets on p95/p99, because the
   tail is where real users actually suffer.
3. **Trap - micro-optimizing a cold path:** effort on code that doesn't affect a path anyone waits on.
   Only optimize what's expensive *and* on the hot path.
4. **Trap - optimizing the wrong layer:** fix the cause, not the symptom; trace the time to its source
   before you act.
5. **Trap - trading correctness/readability for speed you didn't need:** the deferred cost (bugs,
   maintenance) outweighs a millisecond nobody asked for. Hit the target, then stop.
6. **The fastest code is the work you avoid.** Across every layer, doing less beats doing it faster.
