Build1 publisher3 min readPublished
Hard per-bucket caps across rings, pools and arenas, not just GOP/frame resets, keep streamerOS under 152 MB
A dev.to teardown publishes a seven-bucket memory budget for a Rust streaming stack, summing to 146 to 148 MB. The design logic is checkable line by line, but the 12-hour heap curve that would confirm it is not in the post.
The Engineer · Build desk

What happened
- The author of streamerOS, a Rust stack doing RTMP/WebRTC ingest, overlay composition and encoding, reports clean heap profiles at 30 minutes and both fragmentation and small leaks appearing past six hours.
- The rewrite binds each allocation to a lifetime bucket with its own cap: 64 MB video ring, 30 MB asset arena, 16 MB segment arena, 16 MB network pool, 8 MB scratch, 2 MB audio, and 10 to 12 MB of misc.
- Hot short-lived work goes into bumpalo bump arenas that are dropped in one shot at a GOP, scene tick or render frame boundary rather than freed object by object.
- State that outlives an epoch moves into slotmap handle tables, because pointers into an arena die the moment that arena is reset.
- The teardown presents the budget as a constraint the team holds during a 12-hour run without publishing a measured heap or RSS series for it.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint A hard cap per bucket with fail-fast on pressure turns a slow drift into an allocation failure at a named site, so the on-call signal is one bucket refusing work rather than an OOM kill at hour nine.
- decision Anyone copying this table has to decide what a scene reload means in their own product, because that event is the only thing clearing 30 MB of the 42 MB that has no timed reset.
- capability Addresses staying stable for a whole epoch lets the compositor read slices in place, which is what makes an 8 MB per-frame scratch bucket enough.
Add the seven lines up and the ceiling is 146 MB with misc at the bottom of its stated range, 148 MB at the top [4][1]. The post's 152 MB target leaves 4 to 6 MB [2][5]. Everything the table does not name has to fit in that slack.
What makes the caps hold is where each reset lands. A bumpalo reset advances the bump pointer back and leaves the pages hot in cache [7]. It does not walk the objects, so Drop never runs on them [8]. The segment arena resets once per 2 s GOP [4], which across a 12-hour run is 21600 resets [3]. Peak occupancy of that bucket is therefore one GOP of overlay work, and the length of the run drops out of the arithmetic entirely. The scratch arena gets the same treatment per frame at 8 MB [4]. Stable addresses for the life of an epoch are what let the compositor take slices straight out of the arena [7], which is the part that keeps the scratch bucket small.
Two buckets have no timed boundary. The asset arena is 30 MB and resets only on scene reload [4]. Misc, at 10 to 12 MB, is described as monitored rather than capped [4]. That is up to 42 MB, about 28 percent of the 148 MB ceiling, with no periodic zeroing [4]. Sessions in the handle table hold interned string IDs rather than owned Strings [11], and the published table has no line item for the interner itself [7]. If a run under this scheme drifts, those are the buckets to profile first.
The 64 MB video ring is the number least likely to transfer. It is 43 percent of the ceiling [5], and its note reads "Fixed N frames, no growth" [4] without stating N, the resolution, or the pixel format. So the line cannot be checked against a frame size. Copying 64 MB into another pipeline requires the same fixed resolution, the same ring depth, and an encoder that never asks for a frame the ring does not already own.
The adoption cost is destructors. Anything whose Drop must execute, including guards, file handles, and channel senders, cannot be parked inside arena-owned values [8]. Arenas also stay thread-confined, with handles or Bytes crossing actor boundaries instead of &'arena T [9]. That is an API constraint on every message type in the system, not a local change. Cross-epoch state moves into slotmap tables, which avoid ABA-style key reuse [10], though a key you forget to remove is still a key you forget to remove.
The diagnosis the author gives is fragmentation and small leaks together, appearing past six hours after clean profiles at 30 minutes [2]. Arenas answer those two differently. Fragmentation goes away because the allocator only bumps and rewinds. A leak inside a resettable bucket dies at the next reset; a leak in the handle table does not.
What the post does not publish is a heap curve [15]. The table is a constraint the team says it holds during a 12-hour run [4], and the illustrative render loop stops at 7200 GOPs, roughly a third of the way into the run being budgeted [14][6]. The design I would copy today is the hard cap per bucket with fail-fast on pressure [12], because it converts a slow drift into a loud error at a named site. The 64 MB I would not copy without measuring my own frames.
What to watch
- A published RSS reading at hour one against hour twelve would turn this budget from a design constraint into a measurement.
- Whether the asset arena ever gets a timed reset boundary, since scene reload is the only thing that currently clears its 30 MB.
- Where the string interner's own bytes are capped, given that no line in the published table carries it.