Growth Is Not Reclamation

The server died at eight thousand commands with two words: region OOM. The language's memory model had been designed around programs that end — a fixed-capacity region that is never given back, which every short-lived tool tolerated and a server could not. The immediate fix was to make regions grow instead of abort, and to make the byte arena stop racing across threads. The honest part was writing down what the fix did not do: the server now survived, but its memory still grew with every command it ever served.

merememory-modelregionsserverlanguage-design

The crash message was two words, region OOM, and it was honest in a way the system around it was not. The language’s memory model is region-based: values live in bump regions, scoped region blocks free at exit, and everything else lands in one program-lifetime region reclaimed when the process ends. That design is coherent — for programs that end. Every value the server touched per command — the parsed argument strings, the reply being assembled, the cons cells threading them together — landed in that program-lifetime region, which had a fixed four-megabyte capacity and a hard abort on overflow. Roughly eight thousand commands’ worth of small strings filled it, and the process stopped. Nothing about this was mysterious. It was the memory model’s core assumption — the process ends soon — meeting the one program whose whole point is not to.

Aborting was the bug, capacity was not

The first repair separated two things that the crash had fused: the region’s fixed size, and its behaviour on overflow. A region became a chain of bump blocks — when the current block fills, a geometrically larger one is chained on, and because blocks never move, every pointer already handed out stays valid. Scoped blocks free their whole chain at exit as before. Growth is cheap, deterministic, and unlocked the immediate problem: the same stress test that had died at eight thousand commands now ran to eighty thousand and beyond, memory expanding smoothly instead of hitting a wall. The same pass also fixed a quieter danger next door: the flat byte arena used by the descriptor-level FFI shared one bump pointer across every thread, unguarded and unchecked — spawned handlers had been racing on it and could silently write past its end. It gained a mutex and a bounds check that fails loudly. Neither change was clever; both were the difference between a runtime that assumes a single short-lived caller and one that expects to be leaned on.

Writing down what the fix does not do

The important sentence in that release’s changelog was not about what was fixed. It said: growth is not reclamation. The server no longer crashed, but every command still deposited a few hundred bytes into a region that would never shrink — memory now grew in proportion to total traffic served, forever. For a demo that is a footnote; for the idea of writing real servers in the language it is a disqualification, just one with a longer fuse than the abort had. It would have been easy to close the episode with the passing stress test and let the curve speak later. The project’s discipline — the same one that keeps PAIN files in every dogfood repo — required naming the residual problem in the same breath as the fix, and that named problem immediately became the top of the language’s work queue rather than a surprise waiting in some future production story.

The shape of the real problem

Naming it precisely also revealed its structure. Per-command values die logically the moment a reply is written — nothing references them — but the runtime has no way to know that, because ninety-nine percent of allocations and the one percent that must survive (the values actually stored in the map, the messages in flight between threads) are interleaved in the same region. The language already had scoped regions that free at exit; what it lacked was, first, any way for ordinary values — strings, lists — to live in one, and second, any account of how a value that must outlive the scope gets out safely. That is not an allocator problem; it is an ownership problem, and it split cleanly into two stages — make containers own their contents, then make scopes reclaim what they allocate — which are the next two episodes. The server that surfaced the bill sat waiting, unchanged, as the measuring stick the repair would eventually be held against.

← Back to Mere: Building a Language