Chapter 15 of 18

Performance and cost

How to measure speed honestly, where time goes, and what tokens cost.

Ask an agent to make something faster and it will gladly try. It rewrites a loop, adds a cache and tells you the page should feel snappier now. The word to notice is should. A claim about speed is either a measurement or a guess, and on the page they read the same. Nothing in the agent's sentence tells you which one you got, so you have to ask for the number.

There is some evidence this happens more than it should. A preprint from December 2025 read performance pull requests on GitHub: 128 of 280 written by agents (45.7%) showed any validation, against 49 of 77 by people (63.6%), and the agents leaned on reasoning about the code where people more often ran a benchmark. The authors call the link significant but weak, used a model to help with labelling, and can't see checks that happened outside the pull request. They also didn't test whether the changes worked. It is one small study, so treat it as a reason to ask for the number and not as a verdict.

So the first half of this chapter is about measuring honestly: how to time something, why an average hides the people having the worst time, where the time really goes, and why a server gets slow long before it is full. The second half is about the bill. Chapter 5 gave you the price of a token. Here it becomes the cost of a finished task, and the question of how to spend tokens on purpose.

Many benches here time real code on your device, so your numbers will differ from anyone else's, and they will change when you run them again. Learning to read that variation is part of the chapter. The ones that simulate something say so.

Measure first

Before changing anything to make it faster, time it, find the slow part and write the number down. Then time it the same way after the change. Without both numbers, nobody can say the change helped.

An agent reads code and spots what looks slow, and what looks slow often isn't what is slow. It reports the improvement in the same confident voice either way. So make the number part of the work you ask for, because it is the one thing it can't produce by reasoning.

Say it to your agent

“Don't change anything yet. Run hyperfine --warmup 3 --runs 20 "bun run scripts/report.ts" and show me the mean and the spread. Make your change, run the same command, and put both results in the pull request. If the two ranges overlap, tell me it made no measurable difference.”

To measure anything you need to say what you are measuring, and "fast" hides two different things.

Latency and throughput

Latency is how long one thing takes. Throughput is how many things get done per second. They are separate numbers, and improving one can hurt the other: waiting to send items in a batch gets more done per second, and makes each item wait longer.

Tell an agent to make something faster and it has to guess which you meant. Batching can be a real win for a nightly import and a real slowdown for the person waiting on one request, and both get described as a speed-up. Name the number you want to move.

Say it to your agent

“The problem here is p95 latency on GET /orders, not throughput, so don't add batching. Measure p95 at 50 requests per second with k6 before and after, and report both.”

Once you have timed a thing a thousand times, you have to summarise it, and the obvious way is the wrong one.

Percentiles

Also called p50, p95, p99

Sort every timing from fastest to slowest. The 95th percentile, or p95, is the value that 95% of them are at or under. p50 is the middle one. A percentile is a time someone really waited, which an average need not be.

An agent asked if a change helped will often quote an average, because an average is what a quick script prints. A few very slow requests drag it up to a value nobody experienced, and a real regression for 5% of people can barely move it. Ask for the percentiles.

Say it to your agent

“Don't give me the average. Report p50, p95 and p99 for this endpoint, the sample size and the time window, before and after your change.”

Tail latency

Also called p99 latency, Hedged request

The slowest few requests, far along the end of the distribution. One in a hundred sounds rare until a single page makes dozens of requests and waits for them all, at which point a lot of people meet that one.

Chances stack up: with ten requests per page, about one page view in ten hits a p99. An agent run does the same thing, because a session making 40 calls has a one in three chance of meeting a 1-in-100 slow one. So a slow call that looks rare per request is routine per session, and a retry or a timeout has to expect it.

Say it to your agent

“This page makes 12 API calls and waits for all of them. Log the duration of each, tell me which one sets the page's p99, and give every call a timeout. Don't retry a call that writes data unless it is idempotent.”

So timing one thing takes more care than it looks, and it is easy to get a number that flatters you.

Benchmark

A repeatable test that times a piece of code many times under the same conditions, so two versions can be compared. One timing isn't a benchmark, because the same code gives a different number on each run.

An agent asked to prove its change helped can write a benchmark that shows what it hopes for: one run, a tiny input, the old code cold and the new code warm. It need not mean any harm by that. So read the benchmark as well as the result, and ask how many runs it took and whether the ranges overlap.

Say it to your agent

“Benchmark both versions with hyperfine --warmup 3 --runs 30, on the real 50 MB fixture and not a sample. Show me both means with their spreads. If the ranges overlap, say you can't tell them apart.”

A benchmark tells you how long the whole thing took. It doesn't say why. For that you need to see where the time goes.

Profiler

A tool that watches a program run and reports how much time each function took. Self time is what the function spent on its own. Total time also includes everything it called. The answer often surprises people.

This is the most useful thing you can hand an agent for speed work. Without a profile it optimises what looks suspicious. With one it starts from a list of where the time went, and you can check whether its change moved the top line.

Say it to your agent

“Run node --cpu-prof scripts/report.js, read the .cpuprofile, and list the top five functions by self time with their percentages. Don't edit anything until I have seen that list.”

Flame graph

Also called Flame chart

A picture of a profile. Each bar is a function, sitting on top of the one that called it, and the wider the bar, the more time it took. You look for the wide bars at the top: they are doing the work themselves, with nothing above them to blame.

A flame graph lets you check an agent's story at a glance. If it says the slowness is in the formatter and the widest bar is somewhere else, you can see that for yourself without reading a line of code, and without taking its word for it.

Say it to your agent

“Run py-spy record -o profile.svg -- python report.py and tell me the three widest frames with nothing above them, with their percentages. Propose a change only for those, and rerun it afterwards to show that frame got narrower.”

A profile tells you what is slow today. Big O tells you what will get slow next year, when there is more data.

Big O

Also called Time complexity, O(n)

A way of saying how a program's work grows as its input grows. With O(n), twice the data takes about twice as long. With O(n²), twice the data takes about four times as long. It says nothing about how long any one run takes.

An agent's tests run on ten rows, and at ten rows everything is instant. A loop inside a loop can pass every check and only hurt a year later, when the table has grown. It is also a claim agents love to make, so ask to see it measured at two or three sizes.

Say it to your agent

“dedupe in src/report.ts has a loop inside a loop. Time it at 1,000, 10,000 and 100,000 rows and show me the three numbers. If ten times the rows takes about a hundred times as long, replace the inner search with a Set and rerun the same three sizes.”

Bottleneck

Also called Theory of constraints

The one stage that limits how much the whole line can do. Speeding up any other stage changes nothing at the end, and just makes a bigger pile in front of the narrow part.

Agents make writing code far faster, so for many teams the bottleneck has moved to reading it. Generating twice as many pull requests against the same review capacity gets you a longer queue, and pressure to approve without reading. Find the narrow stage before you buy speed anywhere else.

Say it to your agent

“Before you speed up the build, take the last 20 merged pull requests from gh pr list --state merged --json createdAt,mergedAt and tell me where each one waited longest: CI, review or merge. Work only on the longest wait.”

There is a formula for how much a speed-up can buy you, and it is usually less than you hoped.

Amdahl's law

If part of a job has to be done one step at a time, adding more workers can only speed up the rest. If 5% must run alone, you can never go more than 20 times faster, however many workers you add.

This is the sum behind running many agents at once. Ten agents writing in parallel still queue for one person to read, one merge to main and one deploy. That single-file part sets the limit, and past a point another agent mostly adds cost.

Say it to your agent

“dedupe takes 8% of this run according to the profile, so a perfect rewrite saves 8% at most. Is there a part bigger than that? Show me the sum before you start on this one.”

All of which brings us to the most quoted line about speed, which usually gets cut off halfway.

Premature optimisation

Making code faster, and usually harder to read, before you know that it matters. The cost is real and paid at once in complexity. The gain is a guess until someone has measured that this code is where the time goes.

An agent can do this at no effort to itself, so the old brake has gone: a person got tired of writing a clever cache, and an agent doesn't. You then own the extra code. But the same line is often used to excuse not measuring at all, and it doesn't say that.

Say it to your agent

“Take out the cache you added to getUser. Nothing shows this code is slow, and it adds a stale-data risk. If the profile later puts it in the top five, we can bring it back with the numbers in the pull request.”

Now the tools for fixing it. The first one anyone reaches for, and the first one an agent reaches for, is a cache.

Caching

Also called LRU, Hit rate

Keeping a copy of an answer somewhere quick, so you can skip the slow work next time. A hit is when the copy is there. A miss means doing the work anyway. A cache is small, so something has to decide what to throw out, often the least recently used.

A cache is an easy diff that makes a benchmark look great, so it is a favourite agent fix, and it swaps a speed problem for a correctness problem. Check the hit rate on real traffic first: a cache that rarely hits costs memory and complexity, and gives nothing back.

Say it to your agent

“Before adding a cache to getProduct, log 10,000 real lookups and work out the hit rate for a cache of 100, 1,000 and 10,000 entries. Show me those numbers and how stale the data could get. Only then write it.”

Cache invalidation

Also called Stale read, TTL

Getting rid of a stored copy when the real answer changes. Do it too late and people read old data, which is called a stale read. A time limit on each copy, or TTL, is the blunt way: it is simple, and wrong until it runs out.

Tests rarely catch this, because they read a value once and never check again after a write. So an agent's cache can pass everything and still show someone the old price for an hour. Whoever adds a cache has to say what happens on every write path, including the ones they didn't read.

Say it to your agent

“Add a test: read the price, update it, read it again, and assert the second read returns the new price. List every code path that writes to prices, and show me each one clearing the cache. Keep a TTL of 5 minutes as a backstop.”

The next surprise is that a server doesn't get slow when it is full. It gets slow well before that.

Queueing and utilisation

Also called M/M/1, Load shedding

When work arrives at random, it sometimes bunches up and has to wait in line. Utilisation is how busy the server is. As it nears 100%, the line grows faster and faster: at 50% a request takes about twice its work time, at 90% ten times, at 99% a hundred times.

Running everything at 95% looks efficient, and the people waiting pay for it. The same sum applies to you: an agent that hands a reviewer more than they can read doesn't get faster reviews, only a longer line. And a word to keep straight: this is where chapter 9's word has its software meaning.

Say it to your agent

“This worker runs at 90% busy and p99 is climbing. Don't just raise the queue size, since that only lengthens the wait. Put a limit of 1,000 on the queue, return a 429 with Retry-After when it is full, and graph queue depth next to p99.”

Load test

Also called Stress test, Coordinated omission

Sending a system a realistic amount of made-up traffic, on purpose, to see how it behaves before real people do. A stress test goes past the expected load, to find where it breaks.

An agent's checks run one request at a time, so everything it builds looks fast. Load is what finds the queue and the exhausted pool. The test can also lie: a tool that waits for each reply before sending the next quietly stops testing at exactly the moment things go wrong.

Say it to your agent

“Write a k6 script with a constant-arrival-rate of 100 requests per second for 5 minutes, and thresholds of p(95)<200 and rate<0.01 on errors, against staging. Don't use closed virtual users: the rate has to stay fixed when the server slows down.”

Now for what a machine can do at once, which depends on what the work really is.

Concurrency and parallelism

Concurrency means juggling many tasks, swapping between them while each waits for something. Parallelism means truly doing several at the same instant, on separate cores, the independent workers inside a CPU. Waiting can be juggled. Crunching numbers needs more cores.

An agent will happily wrap heavy work in Promise.all and call it parallel. That speeds up work that waits, such as network calls, and does nothing for work that computes, because in JavaScript it all still takes turns on one thread. Ask which kind of work it is, then time it.

Say it to your agent

“Promise.all won't speed up hashing these files, because that work is CPU and not waiting. Move the hashing to a pool of worker_threads with one worker per core, time it against the current version on the same 200 files, and show me both numbers.”

Event loop

Also called Long task, Blocking the main thread

In a browser or Node, a single line of work, called a thread, takes jobs off a list and finishes each before starting the next. While a job runs, nothing else can: no clicks, no animation, no other requests. A job over 50 ms in a browser is called a long task.

An agent's code works on its small test data and passes everything, and then it sorts 200,000 rows inside a click handler and the page freezes. No test notices a freeze. In Node it is worse: one slow job stalls every other person's request on that server.

Say it to your agent

“This click handler sorts all the rows on the main thread. Move the sort to a Web Worker. Then record PerformanceObserver longtask entries before and after with 200,000 rows, and show me that none are over 50 ms.”

Some limits don't show up in any profile, because the code is simply waiting its turn for something.

Connection pool

A fixed set of open database connections that requests borrow and hand back. Opening a new one each time is slow, so they are reused. If all are in use, a request waits. If code forgets to hand one back, the pool slowly runs out.

This one bites agents because everything works in development, where there is one user. A missing release only shows up under load. A pool per serverless instance can multiply past what the database allows. And the instinct to fix it with a bigger pool can make it worse.

Say it to your agent

“Use one shared pool from src/db/pool.ts with max: 10, and release in a finally block on every path. Then run k6 at 100 requests per second for 2 minutes against staging and show me pool wait time and any connection errors. Don't raise max to fix an error until we know why it happened.”

Memory leak

Memory the program no longer needs, and can't release because something still points at it: a list that only grows, a listener never removed. Each bit is small, so it passes every test and takes days to bring a server down.

An agent's session ends long before a leak shows. It adds a cache with no limit or a listener with no cleanup, the tests pass, and nothing fails until your server has been up for a week. Restarting hides it, so it can run for months. You have to go looking.

Say it to your agent

“Run the server under k6 at a steady 20 requests per second for 30 minutes, and log process.memoryUsage().heapUsed every 10 seconds. If it keeps climbing, take two heap snapshots ten minutes apart with node --inspect and tell me which object count grew. Every cache here needs a size limit.”

Garbage collection

Also called GC, GC pause

The part of a runtime that finds memory nothing can reach any more and frees it, so you don't have to. It costs work, and some of that work can pause your program. Those pauses are one reason a fast function sometimes isn't.

Most of the time you can ignore it, and an agent should too. It matters when you see a tail with no obvious cause and code that creates a lot of short-lived objects in a hot loop. Check with a profile first: rewriting code to avoid allocation on a hunch makes it worse to read and may not help.

Say it to your agent

“Before you rewrite this loop to avoid allocating, run it with node --cpu-prof and tell me what share of the profile is (garbage collector). If it is under 5%, leave the code alone.”

So far this has mostly been about servers. In a browser, Google has put numbers on what a person feels.

Core Web Vitals

Also called LCP, INP, CLS

Three scores for how a page feels. LCP: how soon the main content appears. INP: how soon the page responds when you tap. CLS: how much things jump around. Good means LCP within 2.5 seconds, INP within 200 ms and CLS at 0.1 or less.

An agent can't feel a page. It sees that the tests pass, and it may never load the page on a slow phone. These numbers put what a person feels into a form it can read and act on, and be held to. A fast laptop on good wifi will flatter all three, so look at what real visitors got.

Say it to your agent

“Add the web-vitals library and send LCP, INP and CLS to our analytics on every page view. In the CI Lighthouse run, fail if CLS is over 0.1 on / or /pricing. Give every image width and height, and don't insert anything above existing content after load.”

Bundle size

How much code a page makes the browser download. A browser then has to unpack it, read it and run it, so on a cheap phone a big bundle costs time long after the download has finished.

Adding a package is one line for an agent, and it won't notice it just added 200 KB for one date format. Nothing fails, because each addition looks harmless, and no one reviewing the diff sees the size at all. You need something to fail when it grows.

Say it to your agent

“Before adding a dependency, tell me how much it adds to the production bundle after minifying and compressing. Run size-limit and show me the figure. If it is over 10 KB, look for a smaller package or a few lines of our own first.”

Lazy loading

Not loading something until it is needed: an image as it nears the screen, a chart's code when someone opens the chart. The first view gets lighter, and anyone who never scrolls that far never pays for it.

It's an easy thing to ask an agent for, and a common way to do harm. Told to lazy-load the images, it may well do every one of them, including the big one at the top, and that makes the main content appear later, which is the number you were trying to improve.

Say it to your agent

“Add loading="lazy" to images below the fold only. Keep the first image in the page eager with fetchpriority="high", and give every image width and height. Show me LCP from a Lighthouse run before and after.”

Image optimisation

Sending an image no bigger than it will be shown, in a format that compresses well, with its size stated in the page. Images are often the largest thing a page downloads, so this can be the cheapest big win going.

An agent asked to add a picture adds the file you gave it: a 4000-pixel photo, in a box 400 wide. Nothing complains, and on a fast connection nobody notices. So tell it the display size and the format as part of the job, because neither shows up in a test.

Say it to your agent

“hero.png is 4000 pixels wide and shown at 800. Generate 800 and 1600 wide versions in AVIF and WebP, serve them with srcset and sizes in a picture element with a JPEG fallback, and set width and height. Show me the bytes before and after.”

Every one of those is easy to fix once and easy to lose again. To keep it, turn it into a check.

Performance budget

A limit you set in advance on something like bundle size or p95 latency, with a check that fails when it is broken. Speed then stops being something you remember to care about, and becomes something that says no.

This is backpressure for speed. Speed tends to get worse a little at a time, each change too small to notice, and an agent adds them faster than anyone can watch. With a budget in CI the agent gets refused in its own loop, with a number, and has to deal with it.

Say it to your agent

“Add a CI job that fails on any of these: main bundle over 200 KB compressed by size-limit, k6 p(95) over 300 ms on GET /orders, Lighthouse CLS over 0.1. The failure message must name the budget and the number. Show me each one failing once.”

That is the speed half. Agents add a second thing to measure, in dollars, and everything above about measuring applies to it too.

Cost per task

What it really costs to get one piece of work finished: every try, every check and the failures you paid for and threw away. The price per token is only a price list. This is the number you can compare.

Comparing models on price per token is like comparing cars by the cost of a litre of fuel. A cheap model that takes three tries can lose to a dear one that takes one, and a wrong result that nobody catches can cost more than all of them. Measure it on your own tasks.

Say it to your agent

“Run these 10 tasks from git log on each candidate with claude -p --output-format json. Record total_cost_usd, the number of tries, and whether our test suite passed. Give me a table of cost per passing task, not cost per run.”

Spending tokens well

Deciding what is worth putting in front of the model. Every turn sends the whole conversation again, so a big log read early isn't paid for once. You pay for it again on each turn until the session ends.

The agent decides what to read, and it has no reason to care what that costs you. Long test output, whole files and old conversation all ride along on every later turn. Trimming these saves money, and leaves room for the code. Chapter 7 is about what that does to quality. This is the bill.

Say it to your agent

“Don't read the whole test log. Run bun test 2>&1 | tail -30, and if a test failed, grep for its name. Use a subagent for the codebase search and bring back only the file paths. Then /usage, and tell me what used the most.”

Then there is the choice of who does each job.

Model routing

Sending each job to a model that fits it, instead of one model for everything: a cheap one for searching and summaries, an expensive one for planning and hard bugs. The router can be a rule, a setting or a person.

The prices differ by up to a hundred times, so on tokens alone routing nearly always wins. The danger is a cheap model that fails without saying so. A failure you see costs one retry. A failure that passes for success costs whatever it breaks later. Route freely where a check can catch it.

Say it to your agent

“Set model: haiku on the search subagent and leave planning on opus. Run our 10 sample tasks both ways, score them with bun test and the holdout scenarios, and show me cost per passing task for each. If the routed version misses anything the tests catch, keep that step on the bigger model.”

The biggest single saving in most agent sessions comes from one number, and you can read it straight off the response.

Cache hit rate

The share of a request's input tokens that came from the prompt cache. Because an agent resends everything each turn, a high rate can cut the input bill several times over, and a low one can quietly cost more than having no cache at all.

One changing byte near the top of the prompt can send this to zero, and nothing fails. The agent works just as well, and you pay more for it. So this is a number to watch: read it from the usage fields, and treat a fall as you would treat a failing check.

Say it to your agent

“Log usage on every request and work out cache_read_input_tokens divided by the sum of input_tokens, cache_creation_input_tokens and cache_read_input_tokens. Fail the eval if the second turn onwards averages under 80%. Move the date and anything else that changes out of the system prompt.”

Cost of an agent bill

Also called Unit economics

Working out the whole bill from its parts: people, tasks per day, cost per task. Once you have those three, you can tell what would double it, what a cut would save, and whether the work was worth what it cost.

Agent spending grows quietly and spreads across people and machines. A loop running overnight costs the same whether anyone looks or not. If you know your cost per task, a surprising bill becomes a question you can answer. It also gives you a real answer when someone asks if this is worth it.

Say it to your agent

“Pull last month's usage and split it by person and by repository. Divide by merged pull requests to get a cost per pull request, and list the five most expensive sessions with what each one was doing. Set --max-budget-usd 5 on every headless job in CI.”

Do it for real

Reading gets you the words. This gets you the experience behind them, and something to show.

Find the real slow part, before and after

This is the whole first half of the chapter in one sitting, and it gives you a story with numbers in it, which is rare and easy to defend in an interview.

  1. Pick something in a real project that feels slow: a script, a build, a page or an endpoint.
  2. Time it with hyperfine --warmup 3 --runs 20, or with k6 for an endpoint, and write down the numbers with the spread.
  3. Before you profile it, write down what you and your agent each think the slow part is.
  4. Profile it with node --cpu-prof, py-spy record or the browser's Performance panel, and save the profile. Compare it with your guesses.
  5. Fix only the top item. Rerun the same timing, and write down whether the ranges overlap.

Done when: A pull request with the two timings, the profile before and after, your written guess next to what the profile found, and one sentence on what changed.

Put a budget in CI and watch it refuse

A fix you make once decays. A budget doesn't, and it pushes back on your agent without you there. This is the module this chapter installs.

  1. Choose one number that matters on your project: bundle size with size-limit, or p95 with a k6 threshold.
  2. Measure today's value, and set the budget a little above it.
  3. Add it to CI so that going over fails the job, with a message naming the budget and the number.
  4. Make a change on a branch that breaks it, such as adding a large dependency, and capture the failure.
  5. Ask your agent to get back under budget without removing the check, and read what it does.

Done when: A red CI run that names the budget, a green one after the fix, and the budget in the repo.

Price three real tasks, and find the waste

Most people can't say what a finished task costs them. Once you can, you can make an argument about model choice or harness cost that has numbers in it.

  1. Pick three small real tasks and run each headless: claude -p "<task>" --output-format json > run-1.json. Set --max-budget-usd so that a runaway can't surprise you.
  2. From each file, record total_cost_usd and the token counts by kind. Work out the cache hit rate for each.
  3. Find the largest thing each run read, and whether it needed to. Trim it with a tighter instruction, and run the task again.
  4. Run one task on a cheaper model. Check the result with your tests, and work out cost per passing task for each.

Done when: A table of three tasks with cost, tries, hit rate and whether it passed, before and after, and one decision you made because of it.

The build, stage 15 of 18

Measure, then fix

A written guess proved wrong or right by a profile, the query-per-row fix with a before and after, an index you read the plan for, and a budget gate.

About 90 to 150 minutes, on the same app as every other stage. We ran it first and show what went wrong.

Open stage 15 of the build

Gate 15: Performance and cost

0 of 6 right

  1. 1An agent reports: "I replaced the array lookup with a Map, so this is O(1) now and the page should load much faster." It gives no numbers, and the tests pass. What do you ask for?

  2. 2Your dashboard shows average response time steady at 180 ms after a release. Support says some customers now wait several seconds. What is the most likely reason for the mismatch?

  3. 3A worker is at 70% utilisation with acceptable latency. To save money, someone proposes removing machines so that it runs at 95%. Requests arrive at random. What do you expect, on the simple queueing model?

  4. 4A profile says dedupe takes 70% of the run, and formatMoney 2%. An agent proposes a clever rewrite of formatMoney, because the regex "looks slow", and it says the regex is easy to fix. What is the most you could gain from that change?

  5. 5To save money, you add a line with the current time to the top of your system prompt, so the agent always knows the date. A week later the token count looks about the same, and the bill is up. Which number do you check first?

  6. 6In our experiment, the harness nearly doubled writing cost per run ($0.0024 to $0.0043 with the right words), and pass rate went from 98% to 100%. A colleague says that proves harnesses aren't worth paying for. What is the best reply?

Get every answer right to install it, and to put 2 files in your repo. Wrong answers cost nothing.

Next chapterShipping and running

The workshop

Name a word, or describe what you mean in everyday language.