Backend
What One Iteration Costs
Anton Brilliantov Dev.to (EN Zone)
1 views
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.
Read original: https://dev.to/anton_brilliantov/what-one-iteration-costs-2dpo
← Previous
A practical startup budget for React Native apps
Next →
Being a Software Engineer Is Harder in 2026 Than It Was Five or Ten Years Ago
Related
Astra won't kill anything. It will silence everything. (And why that's good news.)
Backend
0
DEV Community
Ho scritto Ratiform per smettere di scrivere i form in Ratatui
Backend
2
Dev.to (EN Zone)
Idempotency: Protecting User Intent Beyond the Buy Button
Backend
0
Dev.to (EN Zone)
I Built a Git Hook That Makes Revert Commits Conventional
Backend
2
DEV Community
Comments0
No comments yet — be the first