NAOMS Devlog

Building a sovereign, local-first memory & identity system โ€” in the open, honestly.

The Thousand Events Behind You

Finishing setup while the system writes a thousand records behind you, without the thing you are typing into going numb. The interactive write path is built so that unrelated writes never queue together and the listening loop is never the thing doing the work.

Product Architect free August 27, 2026ยท8 min readยทperformance

How the write path is built

Setting up a memory system writes a lot of records at once. Onboarding lays down the initial state of who you are and what you have brought with you; a thousand events is not unusual. Meanwhile you are still typing into the thing. Those two activities have to coexist, and the design that lets them is four properties, none of them glamorous.

Writes to different places don't queue behind each other. Records here live in chains โ€” append-only sequences of small signed entries, one chain per subject, each with branches the way a document has drafts. The lock that serialises writing is held per chain and branch rather than across the whole store, so two unrelated writes are unrelated and never wait in the same line. That matters precisely because bulk seeding and your typing are, structurally, unrelated writes.

The expensive call doesn't block the part that's listening. One loop watches your connection and reacts to what you do. The heavy work of turning a written record into the shape the rest of the system reads runs on a separate, asynchronous path, so that loop stays free to notice you. That is the whole difference between slow and numb: work being heavy was never the problem, work being in the way was.

Writes are batched. A thousand events arriving as a thousand separate transactions is a thousand chances to interrupt something; arriving in batches, far fewer.

The bulk writing happens somewhere else entirely. There is a second worker โ€” its own isolated execution context, with its own connection to the encrypted store โ€” and it is started automatically the moment that store is opened, not switched on by a setting or reached for under load. Every bulk-class job goes through it: first-boot setup, seeding the rules the system governs itself by, and importing a pile of files you brought with you. It never sees your passphrase; the main side derives the key and hands it over once, so the worker can open its own connection without ever holding the secret that produced it.

That last property is enforced rather than merely intended, and the distinction is the interesting part. It would have been easy to write the bulk path so that it usually runs on the worker and quietly falls back to the connection-serving side when something is misconfigured โ€” which is exactly the arrangement that looks fine until the day it doesn't. Instead, doing heavy writing on the connection-serving side throws. Not a warning, not a slow path: a refusal, with its own named error, plus a build rule that rejects code reintroducing the import that would make it possible. The instruction that shaped it was "impossible, not avoided."

The half that isn't about speed

A large part of this design isn't performance at all โ€” it's a guard layer for a failure that writing faster and more concurrently makes likelier: two writers disagreeing about what the latest state is. The nightmare version of that ends with a corrupted history and a system that recovers by wiping the database.

So the pointer to the newest entry in a chain is updated by compare-and-swap โ€” a write that says "set this to the new value, but only if it still holds the value I read", so a writer working from stale information fails instead of silently overwriting someone else. Disagreements raise a typed, loudly-logged error rather than passing quietly. A watchdog watches the main loop and, if it stalls past a bound, kills the process loudly instead of hanging โ€” because a process that is wedged but still running is worse than one that has visibly died. And a damaged chain can be quarantined and repaired on its own, rather than the whole store being reset.

Each of those is a decision about what the system does when its own assumptions break โ€” which is the part of a design you only get to make once, before the concurrency exists.

Why responsiveness is an architectural property

The reason to describe all of this as architecture rather than as tuning is that none of it can be added later by making things faster.

Per-lane locking is a decision about what is allowed to be concurrent. Doing the heavy conversion asynchronously is a decision about what the listening loop is permitted to be doing. Batching is a decision about the shape of a write. Putting bulk writing on its own worker is a decision about who owns a write. Compare-and-swap pointers and a loud watchdog are decisions about what happens when those go wrong. Each one is structural, and each one is the sort of thing that is very hard to retrofit into a system that assumed the opposite.

Which is why the repository enforces several of them as rules: reintroducing a blocking call on the hot path, an unguarded pointer update, or the import that would let bulk writing back onto the connection-serving side all fail the build. A property that lives in the shape of the code has to be defended in the shape of the code, or it erodes the first time someone tidies up an awkward-looking line without knowing why it was awkward.

What was wrong

There's a specific kind of bad that software does when it's busy. Not slow โ€” numb. The window is there, the cursor blinks, and nothing you do lands for a second and a half. Then everything you did lands at once.

That happens when the part of the program listening to you is the same part doing the work, and the work doesn't pause to check whether you've said anything. Which is what onboarding used to provoke: the seeding writes and your typing shared one line, and the loop that should have been watching your connection was busy turning records into their read-ready shape.

The measurement recorded at the time: the slowest few percent of interactive writes took 269 milliseconds. That figure is the one worth quoting โ€” not the average, but the tail, because the tail is by definition the bad moments, and the bad moments are what someone remembers about an interface. 269ms is comfortably past the point where a person stops feeling like they are manipulating an object and starts feeling like they are sending requests to something. It's not "slow" in the sense of a progress bar. It's slow in the sense that the illusion of directness breaks.

How it was fixed

Per-lane locking, asynchronous conversion, batching and the separate bulk worker, in that combination โ€” plus the guard layer that had to exist before any of the added concurrency was safe.

The sign-off records the slowest few percent of interactive writes coming down to about 13 milliseconds, from 269; the delay in the listening loop sitting at roughly 40 milliseconds against a 50-millisecond bar; and throughput up 4.8ร—, though that last one is the number to brag about rather than the number a person feels. Those are the figures the work recorded when it closed. We are quoting them, not re-measuring them.

Two independent reviews signed off with no blockers, and a deep pass over the edge cases found and fixed three real defects on the way.

The part where we were wrong about our own design

The separate worker described in the first section is not in this article because it was the obvious idea. It is here because it was built, measured, disproven, and then brought back.

The first close of this work recorded the worker as a dead end. The intuitive design โ€” hand the heavy writing to another execution context and let the main one breathe โ€” had been built and had not, on the benchmark being run at the time, moved the number that mattered. The verdict written down was that it was disproven and left dormant. That was an honest reading of an honest measurement.

Then a different workload arrived: an import of thirty-five thousand files, which starved the connection-authenticating path for more than a minute. Nothing about the earlier measurement became false. It had simply been taken against a load that never held the writing side busy for long enough to matter, and the new one did. The worker came back โ€” not as an optimisation this time but as the owner of every bulk write, always started, with the alternative made to throw.

Both verdicts live in the same closing document. The first is near the top; the reversal is a hundred and thirty lines further down, in a section written two weeks later, and it names the earlier verdict explicitly in order to overturn it. Read only the top and you come away confident about something that is no longer true.

There are two things worth taking from that, and the second is the one we'd defend. The first is ordinary: a benchmark measures a load, not a design. "We tried that and it didn't help" is a claim with a workload hidden inside it, and the workload is the part that expires.

The second is about the record rather than the engineering. A document that records a decision and then, later and in place, records that decision being reversed is more useful than one that only ever contains the current answer โ€” and it is also more dangerous, because it now contains a sentence that was true and is not. The remedy isn't to delete the superseded verdict; it's to read the whole file. An artefact that keeps its own history has to be read to the end, and the convenient stopping point is usually the top.

And the user-visible result of all of it is that during setup, while a thousand things are being written down behind you, the cursor keeps blinking and what you type appears. Which nobody will ever notice, because the entire point is that there is nothing to notice.


Written by AI agents from real project logs; owned and edited by Mujo.

โ† more in Product   home โœฆ   all โ†’