Minutes and tokens per iteration - and why an expensive one is a defect of the text, not of the executor. 👋 I'm Anton - a software engineer working mostly in PHP/Symfony and Go, currently carving a live PHP monolith into Go services. Earlier parts of this series were about how small a unit of work has to get and what has to be written into it; this part is the invoice for that unit - what one iteration actually costs in minutes and in tokens, and how I read those numbers. Notes: github.com/brilliant-almazov. Possibly you already measure this better than I do, possibly you'd measure something else entirely. This is one codebase, one set of habits, and the numbers behind them. The unit being measured Before any number means anything, the unit has to be closed. Mine is narrow on purpose: An estimate is the working time of one background executor on one iteration: editing the named files, moving and extending the tests, running them until green. What is explicitly not in it: Not counted Why my own time writing the prompt is separate work, with its own price (see below) review happens after the iteration is closed, on my clock waiting for CI queue time, not work One exception to the flat range: integration iterations cost more, because a container comes up inside them. Everything below is stated in that unit. If a number here looks small, check the unit before comparing it to yours - most disagreements about "how fast agents are" are really disagreements about what got counted. Tag to tag This is the whole measurement table for the service I take samples from - one row per tag, plus HEAD. It goes first, before any conclusion drawn from it. Tag Date Code lines Test lines Tests/code Code files Packages Avg file v0.1.0 2026-08-10 1751 781 0.45 35 13 50 v0.2.0 2026-08-11 5054 2817 0.56 101 20 50 v0.3.0 2026-08-11 22765 26924 1.18 520 74 43 v0.4.0 2026-08-11 28095 31952 1.14 640 85 43 v1.0.0 2026-08-12 25097 35368 1.41 683 94 36 v1.1.0 2026-08-12 29443 36272 1.23 804 109 36 v1.2.0 2026-08-13 34523 39861 1.15 933 119 37 v1.3.0 2026-08-13 42461 49564 1.17 1116 135 38 v1.4.0 2026-08-13 50755 62986 1.24 1300 175 39 v1.5.0 2026-08-15 55979 68437 1.22 1403 187 39 v1.8.0 2026-08-16 58372 72758 1.25 1476 205 39 v1.10.0 2026-08-16 60019 74701 1.24 1514 232 39 HEAD 2026-08-16 61411 76468 1.25 1540 252 39 What the table says The tests-to-code ratio is the row I actually watch. It went from 0.45 to 1.25, and from v1.0.0 on it never falls below 1.14. That single column is the cheapest early warning I have: a drop against the previous tag gets written down as an explicit line, because it means code arrived somewhere that tests did not. Average file size sits at 39 lines from v1.4.0 onwards. That is the number that tells me the service is growing the way it was designed to grow - by file count and package count, not by files getting fatter. Over the whole span the average file went 50 → 39 lines while packages went 13 → 252. Both movements are the same fact seen twice. The dip in code lines at v1.0.0 is the most useful row in the table (28095 → 25097). Nothing was deleted as waste: repeated per-domain files were pulled into generic cores. Tests went up in the same step, and the ratio jumped to 1.41 - the highest figure in the table. A release that removes three thousand lines of code and adds tests is exactly the shape I want a consolidation release to have, and the table is where I can prove it happened rather than assert it. The last release, read as a delta: code +1392 lines across 26 files, tests +1767 lines across 20 files, packages +20. That was a generic read core and a SQL catalogue engine arriving with their tests. More test lines than code lines in the increment, and twenty new packages for twenty-six new files - the growth is going sideways into packages, which is what keeps the average file at 39. tag tests / code avg file, lines ─────── ────────────────────── ────────────────────── v0.1.0 ████ 0.45 █████████████ 50 v0.2.0 ██████ 0.56 █████████████ 50 v0.3.0 ████████████ 1.18 ███████████ 43 v0.4.0 ███████████ 1.14 ███████████ 43 v1.0.0 ██████████████ 1.41 █████████ 36 generic cores v1.1.0 ████████████ 1.23 █████████ 36 v1.2.0 ████████████ 1.15 █████████ 37 v1.3.0 ████████████ 1.17 ██████████ 38 v1.4.0 ████████████ 1.24 ██████████ 39 v1.5.0 ████████████ 1.22 ██████████ 39 v1.8.0 █████████████ 1.25 ██████████ 39 v1.10.0 ████████████ 1.24 ██████████ 39 HEAD █████████████ 1.25 ██████████ 39 How the numbers are produced The table is worth exactly as much as the discipline behind it, so the rules are boring on purpose: The document is regenerated on every new tag and on the close of any large prompt set. The numbers come from running the written-down commands. Nothing is typed in by hand. A number I could have typed by hand is a number I could have wished into existence. Generated code does not count - neither in code lines, nor in files, nor in the ratio. If it did, the ratio would improve every time I regenerated a contract, which would be flattering and useless. The tag history is walked without checking out a working tree, so producing the table costs no local state and can be re-run at any point. Coverage travels alongside it: 86.7% overall, with the five worst packages named individually, three of them at 0.0%. Naming them matters more than the headline percentage - an aggregate figure hides which parts of the service nobody tests, and a list of five names does not. The coverage floor is enforced by a ratchet that can only go up, which is a check rather than a rule: the difference between the two is the subject of the next section but one. Estimates per set Sets are estimated before they are handed out. Three of them, in the same unit as above: Set Iterations Executor work Calendar, two executors "the service takes its runtime from the platform" 13 5 h 20 min ≈3 h 15 min "extraction into the platform", phase A 7 2 h 50 min ≈1 h 30 min "extraction into the platform", phase B 7 1 h 25 min — Two things are worth reading off this table. First, the calendar column is not the work column halved. 5 h 20 min of executor work lands at about 3 h 15 min of wall clock, not 2 h 40 min. Two executors is the ceiling I run on main work, and iterations inside a dependency chain wait on each other - so the parallel speedup is a fraction, not a factor. Second, the per-iteration arithmetic is a sanity check on the estimate itself. 5 h 20 min over 13 iterations averages about 25 minutes; phase A's 2 h 50 min over 7 units averages about 24. Both sit inside the range I actually observe. Phase B's 1 h 25 min over 7 units averages about 12 minutes - below that range, which tells me either its iterations are unusually mechanical or the estimate is optimistic. I know which one I'd bet on, and I'd rather have the mismatch visible in a table than discover it during execution. The honest caveat Both "extraction into the platform" sets are written but not executed. They are sitting in the queue. Their phases are a plan, and the numbers in the rows above them are estimates, not measurements. I am stating that in the body of the text rather than in a footnote, because an estimate quoted as a measurement is the single easiest way to make a table like this dishonest. The 13-iteration set is the one with execution behind it. Everything below about the observed 15-35 minute range comes from that side of the line. A typical iteration: 15-35 minutes The range, and its two endpoints as they actually happened: Iteration Time "the schedule is read from config" 15 min "typed subscription" 35 min That spread - a bit over two to one between the cheapest and the dearest iteration in the same set is what the estimate gets calibrated against. It is narrow enough to be useful: if a written iteration looks like it will take three hours, the iteration is wrong, not ambitious. Somewhere in it there is either a second concern or a fact I failed to write down. The two endpoints also show what drives the spread, and it is not lines of code. The cheap one substitutes a value that already exists for a hard-coded one. The dear one introduces a shape - a typed subscription that later iterations attach to. New shapes cost more than new instances of an existing shape, consistently, in every set I've measured. Tokens Minutes are the estimate. Tokens are the bill, and they are where the interesting failure lives. Iterations in one early set burned roughly 350,000 tokens each. Not because those iterations were harder than the ones that came later - because the executor was assembling its own context. It read neighbouring packages to find types, re-derived paths and signatures, and worked out conventions that were already decided and written down somewhere I hadn't put in front of it. That number is the direct reason I introduced a closed file list and a "facts of the set" section into every prompt. Where the spend actually goes, ranked: Source What happens Share reconnaissance types, paths and signatures are searched for again; neighbouring packages get read largest repeat run every iteration runs the whole test suite and the linter over the whole repository large rework the iteration failed acceptance and is written again large rule block a copy of an existing generic gets written, a check blocks it, and the hunt for the reason starts medium rule copy-paste the shared style block is duplicated into every file of the set medium re-reading a file that was just written gets read back small, but constant Read that column top to bottom and there is one pattern: not a single line of it is the task. Reconnaissance, repeat runs, rework, blocked copies, duplicated boilerplate, re-reads - all of it is overhead created by how the work was handed over. The task itself is the small remainder at the bottom of the invoice. Two of those rows have mechanical fixes that took an afternoon each. Acceptance per iteration runs only its own package, not the whole suite - the full pass happens once, at the end of a set, on my clock. And the shared style block is declared once in the controlling prompt, with every iteration carrying a one-line reference to it instead of a copy. Both changes are unglamorous and both show up in the bill. An expensive iteration is a defect of the text This is the part I actually changed my mind about. When an iteration comes back expensive, the question is not "why was the executor inefficient". It is "what did I fail to write". Five signals, each of which points at a specific hole in the prompt rather than at a person: Signal What it means a clarifying question was asked a fact was not written down a rule-check blocked the write the primitive that had to be reused was not named two iterations edit the same file the split is wrong the iteration gets rewritten after acceptance the acceptance criterion was not checkable the iteration ate more context than the named files need a defect in the prompt The last row is the general form of the other four, and it is the one that makes the cost measurable rather than anecdotal. I can bound, in advance, how much context the named files need. If an iteration consumed considerably more than that, something in it sent the executor looking - and I can go find what, in the text, without any access to the executor's reasoning. To be exact about the purpose of that list: it is a calibration list for my own writing, not a scorecard for the executor. Every row of it is actionable by editing a file I wrote. That is convenient, because a text can be fixed and re-run cheaply, and there is nothing to fix in a complaint about somebody else's diligence. There is a matching lesson about how the fixes stick. A reminder in a conversation lives exactly one session. A rule in an instruction file works while it is being read. What holds is a check: a linter, a forbidding test, a structure test, a hook that blocks the write. The coverage ratchet above is one of those; a test asserting that a certain low-level loop appears in exactly one package is another; so is a block that refuses to save a prompt containing a phrase from my banned-formulations list. Each of those started life as a repeated expensive iteration. What the measuring itself costs None of the above is free, and the price is paid in the one place the estimate excludes: my time. The estimate is set before the set is handed out, and reconciled after it runs. That reconciliation is the whole point - an estimate nobody checks afterwards is a number with no feedback loop - and it is human work that appears in no row of any table above. Facts have to be gathered before the prompt is written. Paths, full type signatures, sentinel names, the acceptance command. That is the one place where reading around the codebase is legitimate, and it is not cheap. Some numbers stay estimates. Two of the three sets above have not run. Until they do, their rows are forecasts, and I keep them labelled as forecasts. The trade is straightforward: a fact written into the prompt is paid for once, while the same fact discovered by the executor is paid for on every executor that needs it. That asymmetry is the entire economic argument for writing the boring version of the task. Why any of this is worth tabulating The reason to keep a unit of work measurable is not the tables. It is that a measurable unit lets you argue with a number instead of an impression. "This is faster now" is unfalsifiable; "the ratio has not dropped below 1.14 in nine tags, and a typical iteration is 15-35 minutes" can be checked, disputed, or shown to have degraded next month. Without a unit, any acceleration stays a feeling - and feelings about speed are exactly the kind of claim that survives long after it stops being true. Where I'd like to be wrong That's my unit and my bill. Three things I'd genuinely like to hear about: You do this better. Especially if you've found a unit that survives contact with exploratory work, where mine falls apart. You've been through this already. The 350,000-token iteration is the sort of thing people discover independently and rarely write down. You look at it differently. Maybe minutes and tokens are the wrong two axes and I'm optimising a proxy. How is it solved on your side, and what broke when you tried? Working with agents - Part 8. Next: where this approach stops working - the task shapes I can't write a specification for, and what the splitting costs when it doesn't pay off.