terralith — adoption at scale
What it costs to take over an estate you already have. An estate is stood up with stock Terraform, and then adopted: choudoufu claims it from the live objects, chant from its own stacks. The number is how many API calls the plan makes against the cloud account afterwards. No agent, no model, no questions — one run per arm per estate size. See what this bench does and does not measure.
The finding is that this costs a little more than an ordinary plan, and the proportion does not grow. Planning an adopted estate without a state file costs about a quarter more than planning the same estate with one: +24% at 79 resources, +23% at 9,477 and +23% at 10,069. A hundred and twenty fold growth in estate size moves it by one point.
Stock Terraform is the reference for that delta, not a track. It has nothing to adopt — it holds a state file and always did — so it is not competing here and there is no ratio presented as a score. What the extra calls buy is the absence of that file: a plan that finds its resources in the account rather than reading them out of something it was handed at creation.
Every row cites the commit, substrate (emulator pin or real AWS region) and oracle tool versions that produced it, and the exact command that reproduces it.
What this page found
The largest estate each arm has adopted, what one plan costs it afterwards, and how much more that is than planning the same estate from a state file. Counts of API calls against the cloud account, not wall time. The two tracks are separate proofs that an estate this size can be adopted, not entries in a race — they do not share a substrate, and chant has no stock run of the same thing to take a difference against.
| Track | Largest estate run | Stages | Account reads (API calls) | More than a stock plan |
|---|---|---|---|---|
| choudoufu | 10069 resources | 4/4 | 22760 | +4250 (+23%) |
| chant | 10036 resources | 4/4 | 52 cold, 52 snapshot, 0 warm diff | — |
chant deploys CloudFormation stacks rather than a stock-Terraform estate, so there is no stock run of the same thing to take a difference against, and its three reads are reported in its own terms rather than collapsed into one.
choudoufu
| Size | Substrate | Stages | Account reads (API calls) | More than a stock plan |
|---|---|---|---|---|
| 79 | real AWS (us-east-2) | 4/4 | not measured | — |
| 79 | emulator | 4/4 | 186 | +36 (+24%) |
| 301 | real AWS (us-east-2) | 4/4 | not measured | — |
| 745 | real AWS (us-east-2) | 4/4 | not measured | — |
| 3705 | real AWS (us-east-2) | 2/3 | not measured | — |
| 9477 | emulator | 4/4 | 21423 | +4001 (+23%) |
| 10069 | emulator | 4/4 | 22760 | +4250 (+23%) |
3705 resources: test_plan failed.
chant
| Size | Stacks | Stages | Cold plan | Snapshot | Warm diff |
|---|---|---|---|---|---|
| 264 | 4 | 4/4 | 260* | 8 | 0 |
| 528 | 8 | 4/4 | 520* | 16 | 0 |
| 1158 | 3 | 4/4 | 6 | 6 | 0 |
| 3088 | 8 | 4/4 | 16 | 16 | 0 |
| 10036 | 26 | 4/4 | 52 | 52 | 0 |
* measured before chant#2407 moved the held-properties pass behind an explicit --deep this harness does not pass — not comparable to an unstarred Cold plan figure in the same column. See the row's own measurement.reads.cold_plan.note and chant measures three reads, not one.
Where the time goes
Per stage, and never added up. The first column on each table is the estate being stood up, which is not the same work as the columns beside it and on choudoufu's track is not choudoufu's work at all — it is stock Terraform's own apply, the identical stage the oracle's table reports. Summing them produces a figure that looks like a tool's cost and is mostly the fixture's.
Read the substrate column before reading a number beside it. An emulator second is not a cost claim about either tool — see an emulator cannot answer this — while a real-AWS row is real time in a real account, against an API that throttles.
choudoufu
| Size | Substrate | Stand-up (stock Terraform's apply) | migrate |
test_plan |
test_apply |
|---|---|---|---|---|---|
| 79 | real AWS (us-east-2) | 68s | 25s | 17s | — |
| 79 | emulator | 134s | 42s | 3s | 5s |
| 301 | real AWS (us-east-2) | 174s | 77s | 267s | — |
| 745 | real AWS (us-east-2) | 413s | 222s | 129s | — |
| 3705 | real AWS (us-east-2) | 2023s | 1214s | — | — |
| 9477 | emulator | 8126s | 1287s | 48s | 86s |
| 10069 | emulator | 8772s | 1393s | 57s | 119s |
chant
| Size | Substrate | Deploy (chant's own stacks) | read_cold_plan |
read_snapshot |
read_warm_diff |
|---|---|---|---|---|---|
| 1158 | emulator | 30s | 3s | 3s | 3s |
| 3088 | emulator | 79s | 3s | 4s | 3s |
| 10036 | emulator | 258s | 4s | 6s | 4s |
Provenance
Every row above, and what produced it.
| Track | Size | Substrate | Commit | Emulator pin | Oracle versions | Reproduce |
|---|---|---|---|---|---|---|
| choudoufu | 79 | real AWS (us-east-2) | da61fc0 |
— | — | live/live-cert/terralith-scale.sh |
| choudoufu | 79 | emulator | fce6b53 |
sha256:0bbeb43075c9 |
terraform 1.15.8 / tofu 1.12.5 | live/e2e/terralith-scale/run.sh |
| choudoufu | 301 | real AWS (us-east-2) | 420d460 |
— | — | live/live-cert/terralith-scale.sh |
| choudoufu | 745 | real AWS (us-east-2) | 1d06e1d |
— | — | live/live-cert/terralith-scale.sh |
| choudoufu | 3705 | real AWS (us-east-2) | 8bbef27 |
— | — | live/live-cert/terralith-scale.sh |
| choudoufu | 9477 | emulator | 9bd278a |
sha256:0bbeb43075c9 |
terraform 1.15.8 / tofu 1.12.5 | live/e2e/terralith-scale/run.sh |
| choudoufu | 10069 | emulator | fb13ead |
sha256:0bbeb43075c9 |
terraform 1.15.8 / tofu 1.12.5 | live/e2e/terralith-scale/run.sh |
| chant | 264 | emulator | 1c52fd5 |
— | — | test/scale-estate.sh |
| chant | 528 | emulator | 1c52fd5 |
— | — | test/scale-estate.sh |
| chant | 1158 | emulator | 3d82057 |
sha256:0bbeb43075c9 |
— | test/scale-estate.sh |
| chant | 3088 | emulator | 3d82057 |
sha256:0bbeb43075c9 |
— | test/scale-estate.sh |
| chant | 10036 | emulator | 3d82057 |
sha256:0bbeb43075c9 |
— | test/scale-estate.sh |
Reading these numbers
Grouped by track, not ranked
Each results section above is one track — one arm, at every size it has been run at. They are not rows in a leaderboard: choudoufu and chant are separate proofs that an estate this size can be handled, not two entries in a race, so nothing on this page sorts by a measured number. That is why the seconds live in where the time goes, per stage and never summed: a total reads as a tool's cost when most of it is the fixture's, and chant's durations are not comparable to choudoufu's at all, because the substrates differ. See what this bench does and does not measure for why.
Which plan each number is: cold, warm, and why stock has neither
choudoufu's Account reads is its cold plan — the first plan
after adoption. Stock is planned once. That is not an unequal
comparison, because stock has no warm case to compare against: a
stock plan refreshes every resource from the state file on every
run, and choudoufu's own cost model records three consecutive stock
plans of the 79-resource estate at 150, 150, 150. There is nothing
for a second stock plan to save, so the bench takes it once and
leaves plan_calls.warm.stock absent rather than copying the cold
figure into it.
choudoufu's warm plan IS measured, and it is not cheaper. At 79
resources it is identical at 186; at 9,477 it is 21,620 against a
cold 21,423, and at 10,069 it is 23,428 against a cold 22,760. So
the published figure is the lower of choudoufu's two, and a ratio
computed from its warm plan would be worse, not better. Each row's
own measurement.plan_calls_warm carries it.
chant is the exception that keeps its three reads apart rather than picking one, for the same reason: a cold plan and a warm diff are both true of the same estate and cost visibly different amounts.
Adoption is not day-to-day, and the stages are not extra work
Once an estate is adopted, choudoufu is plan and apply, the same two commands as Terraform. The stage column is the adoption journey and the day-two checks that follow it — stand the estate up, claim it, prove the next plan is empty, prove the next apply changes nothing. It is not a list of phases a plan pays every time. Reading it as one is a fair mistake to make from an earlier version of this page, which also summed those stages into a single wall time.
Every number on this page is a default plan, and a default plan switches choudoufu's state cache off. That is by design in the tool: both gates require refresh to be off, because a default plan re-reads every instance so drift is always visible. The consequence for this page is that its figures are choudoufu holding no prior state, compared against stock holding the state file its own apply wrote — which is not the same experiment on both sides.
The day-to-day comparison, with each side holding its own prior
state, is a different measurement and this bench has not made it at
size. choudoufu's own internal/live/statefulcost makes it, three
runs per column, every plan empty, each side applying its own
estate:
| estate | stock | choudoufu, cache serving | cache off | default plan |
|---|---|---|---|---|
| 79 objects, identity-heavy | 150 | 169 | 220 | 186 |
| 101 objects, tagging-served | 247 | 256 | 381 | 290 |
With its cache serving, choudoufu is within 19 calls of stock on the identity-heavy estate and within 9 on the tagging-served one. The cache-off column is what proves the cache did it rather than the refresh flag. Those numbers are not this bench's and are quoted, not published here; what they establish is which question this page's own figures answer, which is the adoption one.
chant measures three reads, not one
choudoufu's plan makes one kind of read; chant's harness makes
three, independently. cold_plan is unconditionally live,
snapshot is what writes the cache, and warm_diff reads only
what snapshot just wrote — 260, 8 and 0 calls are all true of
the same 264-resource estate below. Snapshot holds at two calls
per stack and Warm diff at zero all the way from 264 to 10,036
resources, a fortyfold growth — that is the finding. The chant
section has its own three call columns instead of Account reads
/ Stock oracle so a number is never shown without saying which
read it describes. See the three-reads
finding.
Cold plan below is not one continuous series
The starred Cold plan figures at 264 and 528 resources were
measured before chant#2407
moved the held-properties pass behind an explicit --deep this
harness does not pass; every unstarred figure from 1,158 resources
on was measured after it. Read together, the column drops from
520 to 6 calls while the estate roughly doubles — that is a
one-time change in what the same command measures, at a named
commit, not chant getting eighty times cheaper by growing. See
the three-reads finding
for the commit and the per-stack numbers either side of it.
A failed stage is a published result, not a hidden one
A row whose stage column names a failure is a real, low, published
number, not a run that was quietly dropped. test_plan on the
3,705-resource row ran its assertions and found a non-empty plan
— the post-migrate plan was expected to be empty and was not.
That is different from a run whose tooling never worked at all,
which this site does not publish. See what this bench measures
for the distinction.
The 10,069-resource row carried such a failure until
choudoufu#1076
was fixed: choudoufu refused the plan outright under its own
count-index rule, which enumerated at most 256 indices while this
estate declares count = 2 × scale. The refusal was published
here with its provenance, and the row now carries a plan instead.
Account reads: measured for the emulator, not yet for real AWS
independence.account_reads — the axis this whole site turns
on — is a real number for the emulator (floci) row and not
measured for every real-AWS row below. choudoufu#1053 gave the
emulator row an ordinary plan's own cold/warm call count — 186
both times, because choudoufu's record store is seeded by
live-import itself, so there is no cold-plan penalty to pay here;
the real-AWS certification runs have not carried that
instrumentation yet, so those rows still read not measured
rather than a number that looks like one but isn't. Each cell
says its own status — this note describes today, the table is
the source of truth going forward. See what this bench
deliberately does not measure
yet.
The adoption audit's calls are not the plan's
This row also carries adoption_sweep_calls (588) and
adoption_read_pass_calls (118) in its own JSON — a forced
account-inventory sweep of the provider's whole admission table,
not a plan. That 706-call total was published as Account
reads for a few hours on 2026-09-11 and withdrawn once the
mistake was caught: an ordinary plan and a forced full-account
sweep are different operations on the same estate, not two
measurements of the same thing. The audit's numbers are real and
are kept, under their own adoption_* names, but never populate
Account reads again — that column and Stock oracle (read
pass) below it are both about the plan, never the audit.
The adoption audit's calls are not the plan's
This row also carries adoption_sweep_calls (588) and
adoption_read_pass_calls (118) in its own JSON — a forced
account-inventory sweep of the provider's whole admission table,
not a plan. That 706-call total was published as Account
reads for a few hours on 2026-09-11 and withdrawn once the
mistake was caught: an ordinary plan and a forced full-account
sweep are different operations on the same estate, not two
measurements of the same thing. The audit's numbers are real and
are kept, under their own adoption_* names, but never populate
Account reads again — that column and Stock oracle (read
pass) below it are both about the plan, never the audit.
Stock oracle (read pass): not a second product's score
Stock oracle (read pass) is stock Terraform's own call count for
its plan of the identical, unmigrated estate, not a competing
arm. It is what keeps the Account reads figure next to it
from being self-reported — the run measured both sides planning
the same estate, and stock's count is the check. Stock has no
sweep phase to instrument (it never runs choudoufu's tagging
sweep), so this column only ever reports its one plan, never a
sweep or an audit total.