DEV Community

Anton Brilliantov
Anton Brilliantov

Posted on

What One Iteration Costs

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

Tag-to-tag metrics table: thirteen rows from v0.1.0 to HEAD with code lines, test lines, tests-to-code ratio, file count, package count and average file size; the v1.0.0 row is highlighted

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
Enter fullscreen mode Exit fullscreen mode

Two rows of monospaced bars: the upper row is the tests-to-code ratio per tag rising from 0.45 to 1.25, the lower row is average file size falling from 50 to 39 lines, with a marker at v1.0.0 labelled generic cores

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

Table of token spend sources with shares - reconnaissance largest, repeat run and rework large, rule block and rule copy-paste medium, re-reading small but constant - under a header line reading about 350,000 tokens per iteration, early set

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.

Top comments (0)