The Cost of a Save

Saving one wiki page took three and a half minutes. The fix was not a faster fetch — it was admitting that a save had no business fetching at all, and that a scheduler which rescans the vault per source turns a queue into a lie.

There was a page of links. Not a note—a list of 154 unique URLs, mostly repositories. Then Save.

Three minutes and thirty-four seconds later the page existed. Nothing had crashed and nothing was frozen. The app had decided that writing a page meant reading the internet first, one link after another, in the request. That is the kind of slowness that does not look like a bug, because every line in the log says status=200. It looks like thoroughness. It was two unrelated things welded together.

The Network in the Request

SaveWikiPage wrote the page, diffed its URL set, and then called the ordinary URL intake for every new link: fetch, readability extraction, HTML to Markdown, favicon, language detection, snapshot, index row. Exactly right for a link. Exactly wrong inside a save—especially because the page was written last. For three and a half minutes there was no page on disk, only 154 sources pointing at a page that did not exist. Ninety of those seconds were fetches. The rest was worse.

The Queue Behind It

The pump decides which step runs next: it walks every source, finds the first incomplete step, and queues it—it has to look at all of them, because the pipeline’s order is a statement about the whole vault. What it also did was rescan the vault once per queued source, loading a full row with derived aggregates and a step record from disk. Quadratic, and synchronous: the pump held its lock for the whole pass. The log was exact—155 passes after the save, 781 milliseconds each, 120 seconds, one pass per link. The same mistake made the next step crawl: one outline every 78 seconds, for 120 documents two and a half hours of appearing to do nothing.

The fix is a boundary, not parallelism: a save ends at the vault. The page is written, the links are diffed, the call returns. A new link is claimed immediately—a row and a record at the URL’s stable address, capture pending, later steps skipped because there is nothing to read yet—and the fetch in the background fills that row instead of filing a second source beside it. A link is a source before it has bytes. A link that never answers keeps its row, red, with the reason recorded: claimed, not read.

With the save fixed, the queue became visible—and it was not honest. A step marked itself executing after publishing its status, so the only surface that answers “what is the app doing?” answered “nothing”. The reason a job waits was a constant naming whichever of four holders the gate actually had. And the claim watchdog released claims that were executing, because its rule was “older than a run budget” instead of “nothing behind it”—160 sources freed at once, each free to be derived twice. Underneath sat one more lie with a large effect: a source’s .meta sidecar was written through a temp file with a fixed name, so two writers shared it—eighteen unparseable records in a vault of two hundred, each taking the source’s identity with it.

The Shelf Above the Claim

Everything else in the system is derived; only a page’s tags were typed by a human. Saving one now queues a small bounded model call that writes three to six coarse tags—a tag is the shelf a page is filed under, one level above a claim’s topic. The derived tags live apart from the written ones, as auto_tags: a run that overwrote human tags would be a machine editing notes, and one that re-added a deleted tag would be worse. Both appear in the cloud and filter the same; the derived ones are marked, and deleting one sticks. The tags then got what topics already had—a curated vocabulary with two namespaces and an alias table. A rename rewrites one line of frontmatter or one claim label; neither re-derives a document.

What It Cost

The page became the test that forced every layer at once: 154 sources, 14,000 claims, 423 distill runs, 25 million tokens, about ninety minutes. The lesson is not that saves must be fast. It is that a pipeline whose steps are strictly ordered over the whole vault must be asked what that ordering costs per decision. A global rule enforced by a per-source scan invites quadratic behaviour, and it hid for weeks behind a status=200.


Three and a half minutes to save a page is not a performance problem but a category error with a stopwatch: a save that reads the internet, a scheduler that rescans the vault per source, a queue that reports waiting while it executes, a watchdog that releases running work, and a sidecar two writers shared. None of them were slow; all of them were lying about what they were doing, and the app wrote the lie down, pass by pass, until someone read it. The chapter’s repair is a boundary—a save ends at the vault—and its general lesson is that global ordering enforced by per-source scans invites quadratic behaviour.

AI Insights: Speed Is an Honesty Problem
Source Code: MPL-2.0 · Content: CC BY-SA 4.0 · Credits & AI Disclosure