What each window and encoder setting costs on two workloads, what a stream holds at the least, how budgets behave as up to 2,000 streams open, and the defaults lith took from it.
The tests in bench/budget measured what a stream's encoder costs under each setting, on two workloads, and simulated memory budgets as streams open. Their CSV files are the source of the charts on this page. Measured on 2026-10-13 with Go 1.27.2, klauspost/compress v1.20.3 and andybalholm/brotli v1.2.6, pinned to the performance cores.
The two workloads:
The board: loadtest.Board's view rendered from SQLite, 200 frames of 142 KB, one card changed before each.
The page: a synthetic tab of a small business app, shaped like libretto's: lists of up to 72 entries, a form in a modal, a detail page and an overview, 150 frames of 2.4 to 36.9 KB (13.6 KB on average), most of them a view the tab saw before, with a change.
Settings
TestSettings writes each workload's frames to 64 streams in turn, as the hub does, and measures the heap the open streams keep after two collections, the bytes per frame on the wire after the first frame, and the CPU time per frame: the median of three runs, which differ by under 6%.
A window that cannot reach the frame before costs the board four times the bytes and over twice the CPU: below 2^18, a frame finds the previous one out of reach. The page loses less, since a window still holds several of its smaller frames: against 2^19, it sends twice the bytes at 2^17, three times at 2^16 and six times at 2^15. libretto's real frames lose less again, 9% more at 2^17 and 19% at 2^16, since its sessions repeat a few views more often than this page.
zstd with its full history holds 1.6 MiB at every window up to 2^19, so below 2^19 only ZstdLowMemory saves heap. Low memory costs CPU with the board's large frames, 84 µs per frame against 62 at 2^19 and 124 against 56 at 2^20, and 1 to 2 µs with the page's. On the board, brotli at quality 3 holds 0.86 MiB less than at quality 4 for the same window and sends about twice its bytes; both take four to six times zstd's CPU. The table of every setting, for both workloads, is in bench/RESULTS.md and settings.csv.
The least a stream holds
TestFloor (floor.csv, the page) finds what a compressed stream holds whatever its window: zstd with low memory 0.44 MiB at its smallest window, 2^10 (its 256 KiB hash table, 129 KiB of history and its block buffers), brotli at quality 2 or 3 0.60 MiB at windows up to 2^14, gzip 0.79 MiB. Below 0.44 MiB, a stream only goes out uncompressed.
A budget as streams grow
TestSimulation (simulation.csv) opens up to 2,000 streams of one workload and, after every ten, adds up the measured heap and bytes per frame of what each stream holds. The budgets are sse.Budget with MinWindow at the workload's floor (2^18 for the board, 2^17 for the page) and MaxWindow 2^19. In all but one policy, every open stream writes after every ten opens, so that tiers shrink and waiting streams take the room that frees up.
At open only, the first 77 streams take zstd 2^19 and leave no room for the rest. When streams that find no room make the others shrink, the budget holds as many streams as fit at the floor.
A budget of 1 GiB gives the board's streams zstd 2^19 up to about 600 streams, 2^19 with low memory up to about 950, 2^18 with low memory up to 1,236, and nothing beyond.
With Refuse, the streams beyond the floor's room get a 503 instead of every frame uncompressed: the bytes per frame of the streams that run stay those of the floor.
The board with and without a budget
TestLoadtest (loadtest.csv) runs the shared board in process while 200 producers change cards 800 times a second, about 40 cards per frame. Two runs each:
Streams
Policy
Cores busy
Bytes per frame
Heap
Frames dropped
200
lith's default
1.55, 1.63
2,177, 2,239
334 MiB
0
200
zstd 2^18, low memory
1.81, 1.89
3,984, 3,881
164 MiB
0
200
budget of 128 MiB: 154 at zstd 2^18 low memory, 46 raw
1.48, 1.47
35,718, 35,864
126 MiB
0
1,000
lith's default
4.06, 4.13
2,226, 2,171
1,660 MiB
0
1,000
zstd 2^18, low memory
4.52, 4.38
4,040, 3,996
816 MiB
0
1,000
budget of 128 MiB: 154 compressed, 846 raw
2.11, 1.95
121,272, 121,288
129 MiB
0
Every run held about 19.8 ticks a second. The streams the budget had no room for sent 142 KB per frame, 20 frames a second: with Refuse they would have been answered 503 instead.
Lowering the limit
TestPressure (pressure.csv): 500 board streams hold zstd 2^19 in a budget of 1 GiB, 828 MiB. After SetLimit(512 << 20), each moves to 2^18 with low memory at its next frame: 414 MiB reserved, and 407 MiB of live heap after a collection, from 828. That frame starts without history and takes 10,925 bytes.
What lith took from it
Since 2026-10-13, a hub makes a budget when it is given none: half the process's memory limit, windows from 2^19 down to 2^18, streams uncompressed when even 2^18 does not fit, and no change at all without a memory limit. A stream's own ZstdWindow and BrotliWindow cap what it gets. Pages with large frames pass a budget with Refuse, and hibernation is opt-in (Hibernation). Streams and compression shows the settings.
Measuring again
The tests run only with LITH_BUDGET=1, and write the CSV files next to them:
shell
LITH_BUDGET=1LITH_BUDGET_RUNS=3 taskset -c 0-11 go test ./bench/budget -run Settings -v -timeout 1h
-run picks the test: Settings, Floor, Simulation, Hibernation, WakeCost, Loadtest or Pressure. After a run, go generate ./content in site/ draws this page's charts from the new files.