← Blog

Recoverable, Not Removable

Zero Percent, Exactly reported a kill. A conservative analyzer, asked to prove third-party Node code unreachable so it could be deleted, returned 0.0% net size reduction on every application it measured, and kept returning 0.0% under every counterfactual built to rescue it.

This post covers the five experiments that replaced it. They test the same analysis under a different failure model: files are relocated rather than deleted, so a wrong prediction costs a fetch instead of a broken application. Two results, up front. Recoverability holds. A closed static artifact omitting 73 to 87% of the universe failed to start on four of six applications, while the identical placement decision expressed as lazy residency started all six. The prediction layer does not. Once both prediction sources are charged in measured retrieval milliseconds they converge on the same physical content, and prefetching exactly the files a workload will touch turns out to cost three times more than buying twenty-one times too much of whatever happens to be contiguous. Performance experimentation stopped there. Every design was frozen before its numbers existed.

1. The prior result, and the inversion it forced

The retired branch ran on one invariant, written down before any code existed: proven reachable keeps, possibly reachable keeps, unknown keeps, and only proven unreachable may be removed. A failure to find a load path is never evidence that no path exists.

Sixteen of nineteen frozen applications completed. Every one returned 0.0% strict net universe-relative size reduction, to the byte, against a preregistered kill threshold of 10 to 15% median on target applications. Four named sound refinements returned 0.0%. Every single-gate and every top pairwise gate suppression, computed as diagnostic upper bounds, returned 0.0%. The parallel security measurement returned 0 of 24 CVE rows removable.

Two properties of production dependency trees produce that. Across the corpus the analyzer saw roughly 300,000 module-load sites, and about 294,000 of them resolved to an exact known target, so the specifier side is tractable. But every application also held a handful of capabilities that can reconstruct an arbitrary loader, and 88,599 of 140,165 export surfaces were open rather than statically closed. Nearly 88,000 of those come from one shape, the exports.default and __esModule interop that every Babel and TypeScript compiled package emits. One reach-any capability plus one open export surface makes the whole production universe possibly-reachable, and real trees supply both by construction. The proof obligation cannot be discharged, and a better analyzer does not change that.

That conclusion holds only while removal is permanent. If every omitted file stays byte-for-byte recoverable, a wrong prediction costs a fetch rather than a broken application, and correctness moves out of the analysis and into the storage model. Three terms, used throughout:

  • Placement is whether a file is locally materialised (HOT) or recoverable from immutable storage (COLD). Placement is what prediction decides. Availability is what it must never decide.
  • A hydration miss is a runtime request for a COLD file. The original bytes are materialised and stock Node semantics proceed. The cost is latency.
  • Candidate-COLD is not a reachability claim. The conservative branch retired those at whole-application scope.

Hazards change role under this model. A dynamic require can keep a package hot, widen a cluster or lower a confidence score, instead of saturating the universe.

2. Apparatus common to every experiment

Corpus. The frozen corpus-v1 the retired branch used: 19 applications pinned to exact commit SHAs, with a package population of 6,603 unique name@version pairs frozen from lockfile walks. Twelve targets and seven controls, split declared before any measurement.

Production universe. The unit of measurement is the production-universe artifact: the complete set of files a production install materialises, computed independently of any tracer. Omission by @vercel/nft never authorises deletion here, which is what keeps a placement claim separable from a reachability claim.

Two plans. Every placement experiment reports both:

Plan A   NFT-positive + entrypoint + resolver metadata are HOT.
         Everything else is candidate COLD. No hazard widening at all:
         dynamic require, createRequire, escaped loaders, vm and generated
         source do NOT expand the hot set.

Plan B   Plan A, plus every package the planner classifies as an opaque
         boundary (native artifact, WASM artifact, install script,
         binding.gyp) forced hot as a whole unit.

Plan A is the intrinsic opportunity. Plan B is what an implementation can ship today. The distance between them measures the current constraints rather than anything about Node.

Preregistration. Each experiment has a DESIGN.md committed before its harness ran, fixing the corpus, the arms, the information each arm may use, the metric formulas and the decision thresholds. Amendments are dated and appended rather than edited in. The fourth experiment carries four, including one that records the pre-amendment numbers so the amendments cannot hide a result. Raw first-run output is committed before anything that might improve it.

3. Experiment 1: how much of a production universe can be cold

Question. With no recovery runtime built and nothing removed, what fraction of a production universe carries no positive evidence of being needed at startup?

Method. For each of the 19 applications, compute production-universe bytes, NFT-positive bytes, forced-hot bytes and candidate-cold bytes under both plans. This is arithmetic over a static artifact. No application executes, so the result is an opportunity ceiling rather than a behavioural claim.

Figure 1
How much of a production universe is candidate-COLD
share of production-universe bytes not selected for initial local placement  ·  19 of 19 frozen applications  ·  ordered by universe size
Plan A: no positive evidence Plan B: also forced hot at opaque boundaries
n8n 53.7%
outline 67.0%
strapi 52.8%
nodebb 72.6%
ghost 69.1%
hackathon-starter 65.5%
juice-shop 66.1%
uptime-kuma 73.4%
parse-server 74.8%
nestjs-realworld 87.2%
excalidraw-room 97.4%
express-realworld 10.7%
node-express-boilerplate 83.7%
express-rest-boilerplate 70.0%
verdaccio 83.6%
bulletproof-nodejs 74.6%
fastify-demo 95.4%
nestjs-typescript-starter 72.7%
json-server 81.3%
81.3%Plan A, median over 19 apps
72.7%Plan B, median over 19 apps
4.80 → 3.85 GBcorpus cold bytes, Plan A to Plan B
The pale bar is Plan A: bytes carrying no positive evidence of being needed at startup. The solid bar is Plan B, which additionally forces hot every package shipping a native artifact, a WASM artifact or an install script. The gap is what that one rule costs. express-realworld is the extreme case, 82.4% to 10.7%, and its placement record says why: four opaque packages, all Prisma, force 69.2 MB of a 96.5 MB universe hot. @prisma/engines (37.6 MB) and prisma (25.3 MB) trip the native-artifact rule; @prisma/client and prisma/prisma-client trip the install-script rule. COLD is a placement claim, meaning the file need not be resident at startup. It is not a claim that the file is dead, unused or removable.

The opaque-boundary rule is the expensive one, and its cost lands unevenly.

Figure 2
What the opaque-boundary rule costs, by the evidence that triggered it
950.8 MB of Plan A cold bytes forced hot  ·  each package charged to its sole or strongest signal
native 44.2% 420.6 MB
install-script 33.4% 317.3 MB
wasm 21.8% 207.4 MB
binding.gyp 0.6% 5.5 MB
A package is opaque when the planner cannot reason about what it loads: a compiled addon, a .wasm blob, a lifecycle script. The rule then forces every file in that package hot. 253.3 MB of the 950.8 MB, 26.6%, is first-party application code, forced hot because the application's own root package tripped a heuristic aimed at its dependencies. One .wasm file anywhere in a monorepo outside node_modules, or a build script in the root manifest, takes the whole first-party tree with it. strapi loses 138.7 MB that way, n8n 70.2 MB.

Granularity matters as much as volume. A package-level virtualiser, the obvious first implementation, reaches 42.2% of the Plan B cold bytes. The other 57.8% sits inside packages that are partially hot and exists only because placement is per-file. That number constrained both later prefetch strategies: whole-package widening spends exactly the sparsity this direction exists to exploit, so neither strategy was allowed to do it.

The largest fully-cold packages are ordinary. typescript in two applications, wordnet-db, bootswatch, emoji-datasource-google, @fortawesome/fontawesome-free, @linear/sdk. The opportunity is the routine sediment of an install.

4. Experiment 2: does execution ever ask for it

Question. Starting from the practical Plan B placement, how much of the candidate-COLD set does a representative workload read?

Method. Every application ran against its complete, untouched production universe with an observe-only tracer preloaded. Nothing was placed cold and nothing was recovered, so a hydration miss here is a counterfactual: the first time execution reads the content of a file that Plan B would have placed cold. Workloads are per-application recipes with declared phases (startup, common requests, full workload, and an extended phase where one exists). Each phase records the share of its requests returning under 400, which is how outline’s sweep was caught silently returning 401 for everything. Sixteen of nineteen applications produced a startup trace; the three that did not are materialisation defects in the frozen corpus, enumerated in §8.

Figure 3
How much of the cold set a real workload actually reads
share of Plan B candidate-COLD bytes whose content execution touched  ·  16 of 19 applications  ·  startup plus the strongest available workload  ·  bar scale 0 to 10%, not 0 to 100%
fastify-demo 9.6%
parse-server 2.6%
uptime-kuma 1.2%
outline 1.1%
juice-shop 1.1%
hackathon-starter 1.0%
nodebb 0.9%
excalidraw-room 0.3%
strapi 0.2%
n8n 0.1%
express-realworld 0.0%
express-rest-boilerplate 0.0%
json-server 0.0%
nestjs-typescript-starter 0.0%
node-express-boilerplate 0.0%
verdaccio 0.0%
99.3%of cold bytes never read, byte-weighted over the corpus
23.4 MBhydrated, out of a 3.37 GB cold set
81.3%of the bytes Plan B placed HOT were never read either
Each bar is the miss rate of the Plan B prediction on that application: the share of bytes it placed cold that execution then asked for. Ten of sixteen are under 1.2%. The worst case, fastify-demo at 9.6%, needed 1,333 misses to get there, almost all of them files under 4 KB. Still cold means not read by this workload. It does not mean never needed: these runs last minutes and exercise no cron path, no backup, no second locale and no alternative database driver, so part of the surviving cold set is untested functionality rather than dead code. That is why the model is recovery and not removal.

Hydration is a startup phenomenon. 94.1% of all hydrated bytes were hydrated before the application reported ready. The workload adds little.

Misses arrive in package-local bursts. 99.1% of consecutive misses arrive within 50 ms of the previous one and 89.5% land in the same package as the previous miss. This is why the later strategies widen by package rather than by proximity in time, and it is the only reason semantic prefetch looked worth testing.

Static uncertainty does not predict runtime hydration on this corpus. Pearson r = 0.035 between an application’s NFT warning density and its hydrated bytes. The designated stress case, n8n, carries 29,491 NFT warnings at 0.32 per traced file, mostly unresolved computed specifiers, and hydrated 128 files totalling 1.2 MB, leaving 99.9% of its 1.11 GB cold set untouched. Its workload created and executed a real workflow, so the node and credential loading machinery behind those warnings did run. The property that saturated the retired branch’s analysis barely registers in placement.

Over-provisioning runs both ways. 81.3% of the bytes Plan B placed HOT were never read. The resident set is not only recoverable-sparse; it is larger than what execution needs.

Two blind spots are bounded and published rather than assumed away. The tracer travels in NODE_OPTIONS, so a child process spawned with a replaced environment never loads it. Every spawn is recorded and the untraced count reported per application: nodebb 1, n8n 3, outline 0 of 6. Separately, 403 files under the clone roots were touched that the frozen universe does not contain, all of which classify as application-generated build output against each repository’s own .gitignore, so this run found no production dependency the universe model missed. Folding every one of those bytes in as both cold and hydrated moves the headline from 99.3% to 99.1%.

5. Experiment 3: the model on a commodity lazy substrate

The first two experiments are arithmetic and observation, and neither moves a byte over a network. The third asks whether the model survives a real lazy-loading substrate, and whether the Node-specific analysis is worth anything on top of one.

Substrate. Nydus v2.4.5, RAFS v6 over nydusd in fusedev mode, with an OCI registry backend and an overlayfs upper. It was chosen over stargz-snapshotter for measurability. Its prefetch input is an arbitrary ordered file list, which is the shape of a placement plan; eStargz can only express a prioritised set as a prefix of a total layer order plus a landmark, which forces “which files” and “how they were packed” to vary together. Nydus also emits per-file telemetry and runs standalone without containerd.

Environment. macFUSE cannot be installed non-interactively, so the applications run inside a Linux VM and five of seven were re-installed for linux/arm64 at the same pinned SHA from the same lockfile. One network profile throughout: 25 ms delay, 100 Mbit, applied to the registry’s egress and verified at 11.5 MB/s. Only the application universe is lazy. Node and the base OS are eager in every arm, which removes about 120 MB from the eager arm’s download and understates the lazy advantage uniformly.

5.1 Recoverability on its own

Isolate the variable that has nothing to do with prediction quality. Take the placement decision a static tracer already makes, and express it two ways: as a closed artifact with the omitted files physically absent, which is what output: "standalone" ships, and as initial residency inside a complete lazy image.

Figure 4
The same placement decision, made twice: deleted versus recoverable
closed static artifact (files physically absent) against a complete lazy artifact with identical initial residency  ·  6 applications
json-server
did not start
File fixtures/db.json not found. A data file passed as a CLI argument, outside any module graph.
uptime-kuma
did not start
Cannot find module '../src/util' at server.js:49. A plain relative require in first-party code that the trace missed.
juice-shop
did not start
Please install sqlite3 package manually, then No configurations found in configuration directory. A computed require(dialectName) in Sequelize's dialect loader, plus an untraced config directory.
strapi
did not start
Cannot find module 'async-function/require.mjs'. Conditional-export and ESM subpath resolution the trace did not follow.
verdaccio
started
Started and served the held-out workload correctly.
express-realworld
started
Started correctly, including the Prisma query engine this corpus was chosen to stress.
Both artifacts encode the same judgement about which files are needed, from the same @vercel/nft trace, omitting 73 to 87% of the universe. They differ only in whether the omitted bytes still exist somewhere. The closed artifact failed to start on four of the six applications, by four different Node mechanisms; the lazy artifact started all six and matched the full artifact on every held-out request. The failures are at startup rather than on rare routes, and the held-out workload went on to read files the closed trace omits on every application that failed: 275 files on uptime-kuma, 732 on juice-shop, 232 on strapi.

require(dialectName) inside an ORM is invisible to a static tracer by construction, so juice-shop is the case a slimming product hits first. uptime-kuma is worse in one respect: the missed edge is a plain relative require in first-party code.

5.2 Whether the semantic layer is worth anything

Arms. Six arms over one content-identical complete image per application, differing only in the prefetch table. The information boundary was fixed in advance:

                                   B1   B2   B2d  B3a  B3b
file paths and sizes                *    *    *    *    *
TRAIN filesystem access log         -    *    *    -    *
directory membership                -    -    *    *    *
package identity and boundaries     -    -    -    *    *
@vercel/nft positive trace          -    -    -    *    *
resolver metadata semantics         -    -    -    *    *
opaque-boundary classification      -    -    -    *    -
anything derived from TEST          -    -    -    -    -

B3b receives exactly B2’s behavioural evidence plus the static rows, and no arm sees the held-out workload. B2d exists so the null is not a straw man: B3b expands around what training observed, so the generic arm may expand too, using filesystem structure alone. The headline comparison is B3b against the better of B2 and B2d, per application and per metric.

Metrics. Two byte currencies, never added together: file bytes for placement precision and recall, chunk bytes for what the substrate moved. Milestones are measured from the instant the image is asked for, and fetch state is sampled every 100 ms so bytes-by-milestone come off a curve rather than an assumption. No composite score is computed, and every relative number is printed next to its absolute.

Figure 5
Time from image request to application Ready, by prefetch arm
median ms  ·  Nydus RAFS v6 over an OCI registry at 25 ms / 100 Mbit  ·  n = 5, or 3 on the largest three  ·  each application on its own scale
eager (B0) best generic trace (B2 / B2d) Reducer semantics (B3b)
json-server3.2 MB
eager 447 ms
generic 434 ms
Reducer 538 ms
verdaccio59.9 MB
eager 2,850 ms
generic 1,452 ms
Reducer 1,440 ms
express-realworld107.6 MB
eager 1,928 ms
generic 491 ms
Reducer 893 ms
uptime-kuma200.7 MB
eager 7,454 ms
generic 3,783 ms
Reducer 3,374 ms
juice-shop270.5 MB
eager 8,648 ms
generic 3,328 ms
Reducer 4,138 ms
strapi915.0 MB
eager 20,213 ms
generic 6,838 ms
Reducer 5,274 ms
13.4 sof strapi's 14.9 s saving comes from commodity lazy delivery
1.5 sof it comes from the semantic layer
−46 msmedian advantage of semantics over the best generic arm
One content-identical image per application; the arms differ only in which files are fetched before the process starts. Eager downloads everything first, the docker pull analogue. Generic is the better of two arms trained on a filesystem access trace of the same workload. Reducer gets that identical trace plus package boundaries, the module graph and resolver metadata. Bold marks the faster of the two lazy arms. Lazy delivery is worth two to four times on every application but the smallest. The semantic layer on top of it has mixed signs, three wins and three losses, and its wins are the three largest universes.
Figure 6
Latency the generic arms defer into the first requests
p95 over the held-out request stream, ms  ·  worst generic arm shown  ·  each application on its own scale
eager worst generic Reducer, static only (B3a) Reducer, trace-trained (B3b)
juice-shop
eager 142 ms
generic 1,386 ms
static 145 ms
trained 1,209 ms
verdaccio
eager 187 ms
generic 526 ms
static 188 ms
trained 188 ms
express-realworld
eager 182 ms
generic 476 ms
static 176 ms
trained 183 ms
strapi
eager 34 ms
generic 43 ms
static 39 ms
trained 30 ms
json-server
eager 12 ms
generic 11 ms
static 10 ms
trained 13 ms
Reaching Ready sooner is not a win if the fetches were only postponed into production traffic. The generic arms leave held-out files unfetched and pay on the first requests: 339 ms deferred on verdaccio, 294 ms on express-realworld, 1,244 ms on juice-shop. Static uses no workload evidence at all, only Node semantics, and tracks the eager baseline within a few milliseconds on every application. It pays for that with a slower Ready, 5.6 s against the generic arm's 3.3 s on juice-shop, because forcing every opaque-boundary package hot is expensive. Neither Reducer arm dominates: static owns latency, trained owns readiness on the large applications and inherits the generic arm's blind spot on juice-shop.

File-level precision is the wrong lens on a chunk-addressed substrate. On verdaccio, B3b prefetches a larger file set than B2 at much lower file precision, 64.5% against 100%, and still moves fewer bytes over the network, 3.2 MB against 4.2 MB, in about the same number of remote requests. The wasted files ride inside chunks that were being fetched anyway, because the semantic ordering packed them next to files that were needed. The decision-relevant quantities are remote bytes, remote request count and early-request latency.

Semantic clustering does not reduce remote operations. Node is widely held to be hurt by the number of loose module files, so the hope was that packing the working set by package would fetch it in fewer round trips. The generic trace-ordered arm merges best: 41.6 files per request against B3b’s 25.9 on verdaccio, 18.0 against 8.1 on juice-shop. Nydus lays the prefetch table out contiguously in the order it is given, and a trace’s order is the order the application will read in. Where a Reducer arm does fetch fewer total bytes, as on uptime-kuma at 11.7 MB against 14.9 MB, the cause is coverage of held-out paths rather than clustering.

n8n is absent. It was preregistered for two criteria it uniquely satisfies, largest universe and highest static uncertainty, and its analysis phase ran over two hours at roughly 9 GB RSS without completing across 252,545 files. This is therefore a six-application result, strapi carries the large-universe criterion alone, and any claim about the 2 GB class is extrapolation.

6. Experiment 4: generalising beyond what the strategy was shown

Experiment 3’s held-out workloads were experiment 2’s recipes, which were never designed to hold anything back. On five of six applications the held-out phase read almost nothing the training phase had not already read: 9, 9 and 10 files on strapi, express-realworld and verdaccio. A generic trace prefetcher therefore starts with near-perfect recall, leaving almost no generalisation headroom for a semantic layer to capture. Experiment 4 builds the held-out surface on purpose.

Workload construction. Every long-tail group is one atomic unit of application behaviour, such as a publish-deprecate-unpublish lifecycle or an authenticate-then-use pair, and carries three declared fields: why a real deployment serves this path, the Node mechanism it exercises, and a category from rare-route, error-path, locale, tenant-config, optional-feature and admin-background. Every candidate came from reading the application’s route table and source. Legitimacy is checked empirically: a path counts only if the observation run shows it returning a status in its declared set, and prep-time validation refuses to proceed when one is wrong, which caught six. Categories an application does not have are declared absent rather than faked.

Arms and budgets. Every strategy returns a total ranking of the production universe rather than a set, and a budget is a greedy fill of that ranking. That is what makes the comparison exact: at a given budget every arm prewarms the same number of file bytes and differs only in which files it chose. Budgets swept at 5, 10, 20 and 40%.

The primary interval is not Ready. A strategy that reports Ready earlier by pushing work onto production requests has not won, so traffic is admitted at the later of ready and prewarm-complete, and the preregistered primary metric is container start to steady state. Steady state is a rolling 100-request window whose p99 stays under a band, and the band is not a tunable constant: it is the worst window a fully warm eager pod actually produced, computed from the eager trials alone and appended to the design as a dated amendment before any strategy trial ran.

408 trials were measured across six applications, four strategies, four prewarm budgets and four cold-traffic families, plus 30 eager-reference trials. Zero correctness mismatches: 384 arm-versus-eager comparisons, every request matched on status, on byte length, on body fingerprint where the body is deterministic, and on a declared semantic header where it is not.

Figure 7
How much execution surface a purpose-built long-tail workload actually held out
files the held-out workload read that the training workload never did  ·  5 real applications and 1 controlled fixture  ·  bar scale 0 to 710 files
nextjs * 710 files
juice-shop 669 files
strapi 273 files
uptime-kuma 10 files
verdaccio 1 file
json-server 0 files
This measurement is independent of every strategy, every timing and the substrate: it is the surface any predictor had to cover. The workloads were not casual. Each held-out group came from the application's own route table with file and line citations, carries a written argument for why a production deployment serves that path, and was struck from the analysis if it did not return its declared status. They include an import() behind new Function('specifier','return import(specifier)'), a computed require() in Express view-engine resolution, a lazy require('cheerio') in a plugin-only handler, and a publish, tag and unpublish lifecycle. Three of the five real applications still held out ten files or fewer.

* The fixture was constructed to have a long tail and is marked so it is never read as evidence about real applications.

The held-out column is small for a mechanical reason. uptime-kuma requires all 25 monitor-type modules at server/uptime-kuma-server.js:568-592 and all ~106 notification providers at server/notification.js:3-108 during startup, so pg, mysql2, mongodb, redis, mssql, oracledb, mqtt and playwright-core are all resident before the first request. strapi loads every enabled plugin, every route and every zod and yup locale at boot. Next.js App Router preloads every app route module at server start: 968 files resident before any traffic, against 377 with experimental.preloadEntriesOnStart: false. A planner cannot beat a trace on paths the trace already saw, and on these applications the startup path has already seen nearly everything.

Figure 8
First request into a long-tail path the training workload never exercised
ms  ·  arrival position 1 only, the one contamination-free latency measurement  ·  10% prewarm budget  ·  each group on its own scale
E0 eager G1 trace G2 trace + distance R1 semantics, untrained R2 semantics + trace
juice-shop, lazy import('ethers'), 19 MB
E0 118 ms
G1 1,131 ms
G2 1,041 ms
R1 120 ms
R2 729 ms
strapi, lazy require('cheerio')
E0 148 ms
G1 785 ms
G2 417 ms
R1 158 ms
R2 155 ms
uptime-kuma, per-locale build chunk
E0 11 ms
G1 245 ms
G2 249 ms
R1 256 ms
R2 257 ms
Every arm prewarms the same file bytes and differs only in which files it chose. E0 fetched the whole image before admitting traffic and is the floor. G1 is the literal training trace; G2 widens it by directory distance, which approximates package widening without knowing what a package is. R1 uses Node semantics and no workload evidence; R2 gets G1's trace plus those semantics. Where the tail is module-shaped, R1 serves at eager latency and the generic arms pay a second. Where it is a build asset rather than a module edge, as with uptime-kuma's per-locale chunk in dist/assets, there is no edge for semantics to follow and every arm pays 245 to 257 ms. R2 holds strictly more information than R1 and is worse on juice-shop: the trace anchors the ranking to what the training run happened to touch, and the semantic tiers below it never reach the budget.
Figure 9
Equal bytes is not equal time
strapi, every arm prewarming the identical 45.8 MB of file bytes at the 5% budget  ·  each measure on its own scale  ·  bold marks the best arm on each measure
G1 trace G2 trace + distance R1 semantics, untrained R2 semantics + trace E0 eager reference
prewarm durationseconds
G1 12.7 s
G2 20.6 s
R1 22.6 s
R2 24.6 s
E0 22.7 s
post-admission faultscount
G1 7
G2 4
R1 0
R2 1
E0 0
time to steady stateseconds, the primary metric
G1 19.2 s
G2 27.5 s
R1 28.8 s
R2 31.9 s
E0 28.6 s
The budget is denominated in file bytes, so every arm spends the same allowance and differs only in which files it buys. R1 buys 66,628 small scattered files against G1's 6,579, which costs nearly twice the chunk bytes and 78% more prewarm time. Traffic is admitted when prewarm completes, so the arm that predicts the long tail best admits traffic ten seconds later and reaches steady state last. On the preregistered primary interval G1 wins by 9.5 s by prewarming less: seven remote faults costing about 3 MB are cheaper than ten seconds of prefetch. At this link speed and in this budget range, prewarm time dominates fault time.

Does more training close the gap? Re-rank the generic strategies analytically against a training set augmented with one more long-tail category’s observed first touches, with no semantic information entering. On strapi and on the fixture, one extra trained category lifts the generic arm past the semantic one. On juice-shop the entire matrix buys 19 files and generic still trails. The argument that generic profiling needs an exhausting workload matrix does not hold in general.

Is the size term doing the work? All three widening strategies share one rule, key = distance + log2(size+1), and the design required a sensitivity check with the size term removed. On the real applications it changes almost nothing and the ordering is unchanged. On the fixture it dominates the outcome, for reasons taken up in §6.1. Both are reported.

6.1 The controlled fixture, and what it is allowed to conclude

benchmarks/longtail/fixture-app is a self-hosted Next.js 15 App Router application authored in this repository: 11,918 files, 362.6 MB, and the only application in the corpus carrying all six long-tail categories. It exists because of the risk Figure 7 confirmed. If every real application holds out ten files, the experiment learns that there is nothing to generalise to and nothing about how a strategy generalises when there is. The fixture is a tree whose tail is known by construction, so a win can be attributed to a mechanism. Every mechanism in it is an ordinary ecosystem pattern: a per-locale message catalogue read from disk in the i18next-fs-backend shape, a per-tenant JSON config selecting a dynamically imported renderer, an admin export behind a heavy optional library, per-format dynamic imports on a report route, lazily loaded error reporting, and a feature flag gating a conditional import. None of it is contrived to defeat tracing, and serverExternalPackages keeps the heavy leaf libraries as real node_modules requires rather than letting them be inlined into a server chunk.

The tree is stacked in the hypothesis’s favour, so DESIGN §14 fixed the rule before the run: a strong fixture result without real-application support is reported as negative or inconclusive for product differentiation. A fixture-only win counts as a loss. The test the fixture had to pass is therefore not whether it showed an advantage, which it was built to do, but whether it showed one the real applications lack. On the smallest budget reaching eager-equivalent p99:

                 G1      G2     R1     R2
fixture          never   40%    20%    20%
strapi (real)    never   10%     5%    40%

R1 needs half of G2’s budget in both, so the fixture reproduces the strapi ratio instead of exceeding it, which is what makes it usable as an explanation of the strapi result. That agreement is R1-specific. R2 ties R1 on the fixture and needs eight times R1’s budget on strapi, so the fixture is materially kinder to R2 than the real case, and R2 against best(G1, G2) was the preregistered headline comparison.

The fixture is also where the outcome depends most on the shared widening rule. Dropping the size term moves G2’s coverage from 1,049 files to 452 and R2’s from 1,032 to 536, and without that term R2 leads G2. The same check changes almost nothing on the real applications. The fixture carries both the strongest result in the experiment and the most fragile one.

Its most useful output was negative, and it was about the intended deployment shape. The fixture has a module-shaped long tail only because experimental.preloadEntriesOnStart: false is set. Next’s production server otherwise preloads every app route module at startup, measured here at 968 files resident before any traffic against 377 with the opt-out set. Self-hosted Next.js App Router is the shape this direction was aimed at.

7. Experiment 5: the same comparison under real retrieval economics

Experiment 4 left one explanation standing. Its budget was denominated in file bytes, and Figure 9 showed that an equal file-byte budget is not an equal time budget: the semantic arm bought many small scattered files, paid 78% more prewarm time for the same allowance, and lost the primary interval by admitting traffic later. If that was the whole story, correcting the currency should hand the semantic arm its result back. Experiment 5 implements the correction.

The retrieval unit. nydusd’s prefetch worker coalesces requested chunks into backend HTTP range requests bounded by its own merging_size. Partition the chunk axis into consecutive units whose compressed span fits that bound, and a unit is exactly what the substrate turns into one backend request. The partition depends only on the image, so both arms see the same units, and a file inside an already-selected unit costs nothing further. The median compressed chunk is about 500 bytes, so a unit holds tens of files. Selecting whole units converts “many small scattered files” into “few large contiguous reads” without touching the image, and it is available to the generic arm and the semantic arm on identical terms.

The cost model, calibrated before it was allowed to select anything.

prewarm ms  =  fixedMs  +  alpha * backend requests  +  beta * backend bytes

fixedMs   181.07 ms        gapBridge   2 048 B
alpha      10.446 ms/req   maxRange  131 072 B   (the substrate's own merging_size)
beta        1.5566e-5 ms/B  (about 15.6 ms per MB)

Fitted on 86 application-free probes: R² 0.998 against the measured backend counters, and 11.0% MAPE predicting real prewarm milliseconds on the deployed manifests from the physical map alone, with no measured counter anywhere in the prediction. One set of coefficients prices every arm, both controls and the oracle. There is no per-arm and no per-application term. The estimator under-predicts, so a manifest costs slightly more than planned and never less, which is the safe direction when the admission gate is a wall clock.

Arms. A 2 x 2 of prediction source against cost-awareness. G1 and R1 take the longest prefix of their ranking whose predicted cost fits the budget. G1C and R1C spend the same budget on whole units by descending value per unit cost. Both rankings are experiment 4’s, loaded from that experiment’s preserved artefacts rather than re-derived. E0 is the eager reference and O is the physical oracle, which is analysis only and never deployed. Three applications, one of them adverse: uptime-kuma, whose long tail is a build asset rather than a module edge and whose band is the tightest in the corpus. Budgets are measured prewarm milliseconds, on a ladder taken from experiment 4’s measured distributions.

252 trials on the primary network profile, 40 on a preregistered faster profile, 212 application-free calibration probes. Zero correctness mismatches across 272 arm-versus-eager comparisons, and 22 of 22 mechanical fairness invariants hold.

Figure 10
What cost-awareness bought, and who it bought it for
strapi, time from container start to steady state  ·  median of four cold-traffic families  ·  each rung is an equal measured prewarm budget  ·  each rung on its own scale  ·  bold marks the best arm at that rung
G1 generic, cost-unaware G1C generic, cost-aware R1 semantic, cost-unaware R1C semantic, cost-aware
2 s rungmeasured 1.9 s
G1 26.0 s
G1C 24.8 s
R1 26.4 s
R1C 26.8 s
4 s rungmeasured 3.3 s
G1 24.9 s
G1C 22.5 s
R1 25.0 s
R1C 22.3 s
8 s rungmeasured 6.2 s
G1 22.9 s
G1C 17.5 s
R1 23.5 s
R1C 18.7 s
14 s rungmeasured 11.3 s
G1 20.7 s
G1C 19.2 s
R1 22.7 s
R1C 19.7 s
22 s rungmeasured 19.8 s
G1 25.2 s
G1C 25.2 s
R1 23.2 s
R1C 25.2 s
28.1 seager reference, whole image
+5.4 s / +4.8 scost-awareness at the 8 s rung, generic / semantic
11 of 15cells across three apps where G1C beats R1C
The 2 × 2 is prediction source against cost-awareness. G1 and R1 take the longest prefix of their ranking that fits the budget, which is experiment 4's rule re-denominated in milliseconds. G1C and R1C spend the same budget on whole retrieval units instead. Both rankings are the frozen experiment-4 rankings, loaded from that experiment's preserved artefacts so they cannot drift. At the 8 s rung cost-awareness is worth 5.4 s to the generic ranking and 4.8 s to the semantic one, so experiment 4's diagnosis was right and the fix accrues to both sources. It is a net effect against an asymmetry: at that rung G1 prewarms for 5.5 s and G1C for 6.9 s, so G1C spent 1.4 s more prewarming and still admitted traffic 5.0 s sooner.

The clearest statement of what changed is experiment 4’s own semantic manifest, re-priced in physical units and set beside this experiment’s:

strapi                              files    chunks   backend req   prewarm
experiment 4  R1 @ 40% file bytes  106 512   68 803        1 567    25.3 s measured
experiment 5  R1C @ the 8 s rung   106 663   61 811          537     6.2 s measured

The same content resident, one third of the requests, four times faster, because it was bought as contiguous regions instead of as individually valuable scattered files.

The cliff is gone, and cost-awareness rather than semantics is what removes it. juice-shop’s lazy import('ethers') group was experiment 4’s headline result, where static semantics served the first request in 120 ms against the generic arms’ 1,041 to 1,131 ms. At the 6.1 s rung here:

G1  1 055 ms      G1C  126 ms      R1  463 ms      R1C  122 ms      eager  122 ms

The generic ranking reaches the eager baseline as completely as the semantic one. The property experiments 3 and 4 both attributed to Node semantics belongs to the budget.

On the primary metric the semantic arm does not win. T_steady for R1C minus G1C, across fifteen cells: R1C is ahead in four, by 0.01 to 0.40 s inside an interval of 11 to 27 s; G1C is ahead in eleven, by up to 2.0 s. There is one genuine semantic win. On strapi at the 3.3 s rung the worst post-admission window p99 is 130 ms for R1C against 228 ms for G1C, with the four traffic families disjoint (130/129/132/151 against 250/214/246/228), and it carries the equal-outcome result: R1C reaches eager-like post-admission latency at 3.3 s where G1C needs 6.2 s. Three things stop it being the wedge. It does not reproduce on juice-shop, which has the richer module-shaped long tail at 669 held-out files against strapi’s 273, and which ties at 6.1 s. It does not move the headline, because at that rung R1C’s T_steady is 22.3 s and G1C’s is 22.5 s: T_steady on strapi is dominated by application readiness at 16.8 to 21.5 s, not by prewarming. And it is a property of the link, not of the prediction.

The faster link removes it. The sensitivity profile was preregistered before any result existed, at 5 ms and 1000 Mbit:

strapi, the 3.3 s rung, max post-admission window p99
  25 ms / 100 Mbit    G1C  228 ms    R1C  130 ms
   5 ms / 1000 Mbit   G1C  131 ms    R1C  130 ms

Fault counts are unchanged across profiles, since the manifests and residency are identical, so the gap was never coverage. G1C’s misses were expensive because the link was slow. Experiment 4 named network speed as its most load-bearing limitation and expected a faster link to favour the semantic arms. It does the opposite.

Why the split is interpretable. The two arms are good at different halves of the timeline, and the frozen rankings say why. G1C’s value is the training trace, which is the startup path, so it reaches application-ready sooner: 12.7 s under G1C against 14.0 s under R1C at the 6.2 s rung, even though R1C had more content resident at admission, 111,838 files against 111,175. R1C’s value is static module structure, which covers the long tail, so it takes the lower post-admission p99 at tight budgets. T_steady is admission plus post-admission warming, and on these applications admission dominates, so the generic arm wins the metric by being better at the part that dominates it.

Figure 12
Under equal retrieval cost the two prediction sources select the same content
Jaccard overlap of the resident sets the semantic and generic arms choose  ·  rung 1 is the tightest budget, rung 5 the widest  ·  axis 0 to 1 in every panel
cost-aware pair (G1C / R1C) cost-unaware pair (G1 / R1)
strapi, cost-awareG1C / R1C
rung 1 0.570
rung 2 0.652
rung 3 0.931
rung 4 1.000
rung 5 1.000
strapi, cost-unawareG1 / R1
rung 1 0.013
rung 2 0.023
rung 3 0.026
rung 4 0.049
rung 5 0.112
juice-shop, cost-awareG1C / R1C
rung 1 0.509
rung 2 0.791
rung 3 0.990
rung 4 1.000
rung 5 1.000
juice-shop, cost-unawareG1 / R1
rung 1 0.037
rung 2 0.058
rung 3 0.106
rung 4 0.119
rung 5 1.000
uptime-kuma, cost-awareG1C / R1C
rung 1 0.532
rung 2 0.768
rung 3 0.981
rung 4 0.981
rung 5 1.000
uptime-kuma, cost-unawareG1 / R1
rung 1 0.016
rung 2 0.019
rung 3 0.058
rung 4 0.058
rung 5 0.041
This is measured on the manifests, before any latency exists. Without cost-awareness the two sources genuinely disagree: they pick 1 to 12% the same files. Charge them both in measured milliseconds and let them buy contiguous units, and the disagreement disappears, reaching identical selections within three rungs on all three applications. The verdict does not rest on a tie in outcomes that could be measurement noise. It rests on a tie in plans.
Figure 11
Perfect knowledge of the future costs three times more than knowing nothing
strapi  ·  the oracle is exactly the files the held-out workload first-touches, priced in the same units and computed only after every deployed manifest was frozen  ·  each measure on its own scale
physical oracle (analysis only) G1C, cost-aware generic
backend requestsone HTTP range request each
oracle 1,697
G1C 2 s 132
G1C 8 s 536
files made residentcount
oracle 5,118
G1C 2 s 44,531
G1C 8 s 108,134
prewarm timeseconds
oracle 18.1 s *
G1C 2 s 1.9 s
G1C 8 s 6.2 s
The oracle is not a deployable strategy; it is the lower bound on logical content, 9.6 MB. Its physical cost is 1,697 scattered range requests, and on a request-bound substrate that is what gets charged. A cost-aware arm buying 21 times more content chosen only for contiguity arrives in a seventh of the time. Three properties of the image produce this: 22,870 of strapi's 75,245 chunks back more than one file, the median compressed chunk is about 500 bytes, and the blob is one-dimensional in the builder's directory-walk order. The quantity a predictor optimises, which files will be needed, is not the quantity the substrate charges for.

* predicted by the cost model; the two G1C figures are measured. The model fits deployed manifests to 11.0% MAPE and under-predicts, so the oracle's real cost is if anything higher.

The adverse control behaved. R1C wins no rung on uptime-kuma and G1C is ahead at all five. The cost-unaware semantic arm R1 is the only arm in the experiment that never becomes eager-matched. Nothing in the cost model manufactures a semantic advantage.

Waste turns out to be the wrong thing to minimise. At strapi’s 6.2 s rung the cost-aware arms make about 106,000 files and 420 MB resident that no workload touches, against the cost-unaware arms’ 85,000 and 370 MB. The cost-aware arms waste more and still win by five seconds, because the waste is contiguous and therefore nearly free. On this substrate the scarce resource is requests.

8. Threats to validity

  1. One primary network profile, 25 ms and 100 Mbit, with a preregistered second profile at 5 ms and 1000 Mbit run on two applications in experiment 5. Experiments 3 and 4 carry the single-profile caveat in full. Experiment 4 named the slow link as the most likely place a semantic advantage was hiding; measuring it removed the only one that remained.
  2. Only the application universe is lazy. Node and the base OS are eager in every arm, which understates the lazy advantage uniformly.
  3. Still cold is not never needed. Experiment 2’s runs last minutes. Some of the surviving cold set is untested functionality rather than dead code.
  4. Single machine, single run per cell. Medians over rotated repetitions with min and max reported, no cross-machine reproduction, three repetitions rather than five on the largest applications.
  5. The largest size class is unmeasured. n8n is absent from experiments 3 and 4, and the semantic advantage was the only quantity that grew with application size.
  6. Three applications never executed in experiment 2, each a materialisation defect in the frozen corpus rather than a property of virtualisation. bulletproof-nodejs is missing an argon2 native binding on its startup require path; nestjs-realworld has 47 empty top-level directories in node_modules from a recorded npm failure; ghost ships its monorepo workspace packages as TypeScript source only and cannot boot from its pinned tree, which means ghost’s preserved Plan A and Plan B numbers describe a tree that cannot run. That is a finding about the corpus, recorded as one.
  7. Per-category first-hit latency is only measurable for groups reaching arrival position 1, at most four groups per application, and two applications’ family rotations collide, giving three uncontaminated first-arrival orders instead of four.
  8. The fixture was built to favour the hypothesis and is reported separately everywhere. It reproduces strapi’s R1 ratio rather than exceeding it, but it is kinder to R2 than the real case, and its outcome is the one most sensitive to the widening rule (§6.1).
  9. T_steady is dominated by application readiness on these applications, 12.7 to 21.5 s of a 17.5 to 26.8 s interval on strapi. Experiment 5 therefore measures prewarming’s contribution to a metric prewarming only partly controls, which is the operator’s own framing but compresses every difference. The p99 and fault columns are the sensitive ones.
  10. The preregistered steady-state band is unreachable by the reference arm. Strapi’s eager cold pass peaks at 132 ms against a band of 80 ms taken from the eager arm’s warm pass, and T_steady is undefined in 2 of 4 eager trials. The headline therefore uses the eager arm’s own worst cold window as the threshold, an analysis choice made after the trials and labelled as one. Both are reported and the ordering of G1C and R1C is the same under either.
  11. The cost model is accurate in the regime the arms deploy and not outside it, 11.0% MAPE on the deployed manifests against 28.8% including a file-listing regime no arm uses. The re-priced experiment-4 manifests fall in the latter: their chunk, request and overfetch counts are exact geometry, but their predicted durations should be read as ordering rather than magnitude.
  12. The blob’s chunk order is the image builder’s directory walk. A different packer changes which content is physically cheap, and changes it for both arms. The preregistration recorded this as the structural reason to expect the null. It is a property of how OCI images are built rather than of this benchmark.
  13. Experiment 5 covers three applications. The other three from experiment 4 had held-out surfaces of 0, 1 and 10 files and could not discriminate anything. Nydus also cannot abort a prefetch, so equal cost is achieved by sizing manifests against measured probes; prefetch had completed before admission in all 63 cells, so no reported post-admission request is residual prefetch.

9. What the five experiments establish

A closed static artifact, the artifact a dependency-slimming product ships, omitting 73 to 87% of the universe, failed to start on four of six applications by four different Node mechanisms. The complete lazy artifact making the identical placement decision ran all six. Under an immutable-and-complete model a placement mistake degrades to a fetch. That is a reliability argument for lazy delivery, and not an argument that the semantic layer is the product.

Lazy delivery is worth ten to fifteen seconds on a large application, and it is commodity: 13.4 of strapi’s 14.9 second saving comes from nydus-image --prefetch-policy fs plus a workload trace, with no Node knowledge involved. It also needs a good initial residency set to be worth anything, since the no-prefetch arm is slower than an eager pull on four of six applications: thousands of sequential 25 ms faults cost more than one bulk download.

Experiments 3 and 4 both found one reliable semantic property: static Node semantics with no workload evidence held early-request latency at the eager baseline where generic trace prefetch deferred 0.3 to 1.2 seconds into the first requests. Experiment 5 shows that property belongs to the budget rather than to the semantics. Charge the generic ranking in measured milliseconds and let it buy contiguous retrieval units, and it removes the same one-second cliff as completely: 126 ms against the semantic arm’s 122 ms and eager’s 122 ms, where the cost-unaware generic arm took 1,055 ms.

Under equal measured retrieval cost the two prediction sources stop disagreeing. Resident-set overlap between the semantic and generic manifests runs 0.51 to 0.57 at the tightest budget and reaches 1.00 within three rungs, against 0.01 to 0.12 for the same two sources without cost-awareness. That is visible in the manifests before any latency is measured, which is a stronger statement than a tie in outcomes. The physical oracle then closes it: prefetching exactly the files strapi’s workload touches costs 1,697 backend requests and a predicted 18.1 s, while 21 times more content chosen only for contiguity costs 132 requests and a measured 1.9 s. Prediction accuracy is not the binding constraint, so a better predictor cannot recover the wedge.

What survives is not the planner. Cost-aware, unit-granularity prewarming needs a chunk map and an arithmetic model: no Node analysis, no dependency graph, no training run, and it ports to any lazy-OCI substrate. Recoverability needs a complete immutable artifact and a lazy mount. Both are enabling technology for lazy delivery. Semantic prewarm planning as product differentiation is retired, and per experiment 5’s own decision rule no further planner variant is proposed.

Three things are open rather than answered. The blob’s chunk order is the image builder’s directory walk, so a different packer changes which content is physically cheap, for both arms equally. The 2 GB, 250,000-file class was never measured, and n8n is absent from experiments 3 through 5. And nothing across these five experiments speaks to whether a vulnerable function can sit in a hazard-free island inside an otherwise saturated application, which is the question the first post left open and the one place the retired analyzer might still be load-bearing.