Docs Performance

The stream budget

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%.

Bytes per frame of the 142 KB board, by windowbench/budget/settings.csv
05K10K15Kbytes per framewindowzstd, fastest, window 2^15: 11,234 bytes per frame2^15zstd, fastest, window 2^16: 10,872 bytes per framebrotli, quality 4, window 2^16: 9,716 bytes per frame2^16zstd, fastest, window 2^17: 10,831 bytes per framebrotli, quality 4, window 2^17: 9,648 bytes per frame2^17zstd, fastest, window 2^18: 2,703 bytes per framebrotli, quality 4, window 2^18: 986 bytes per frame2^18zstd, fastest, window 2^19: 1,471 bytes per framebrotli, quality 4, window 2^19: 389 bytes per frame2^19zstd, fastest, window 2^20: 794 bytes per framebrotli, quality 4, window 2^20: 161 bytes per frame2^2011,23410,8729,71610,8319,6482,7039861,471389794161
  • zstd, fastest
  • brotli, quality 4
The numbers
windowzstd, fastestbrotli, quality 4
2^1511,234
2^1610,8729,716
2^1710,8319,648
2^182,703986
2^191,471389
2^20794161

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.

Heap per stream with zstd, by windowbench/budget/settings.csv
0123MiB per streamwindowfull history, window 2^15: 1.622 MiB per streamlow memory, window 2^15: 0.525 MiB per stream2^15full history, window 2^16: 1.657 MiB per streamlow memory, window 2^16: 0.626 MiB per stream2^16full history, window 2^17: 1.656 MiB per streamlow memory, window 2^17: 0.688 MiB per stream2^17full history, window 2^18: 1.656 MiB per streamlow memory, window 2^18: 0.813 MiB per stream2^18full history, window 2^19: 1.656 MiB per streamlow memory, window 2^19: 1.063 MiB per stream2^19full history, window 2^20: 2.656 MiB per streamlow memory, window 2^20: 1.563 MiB per stream2^201.6220.5251.6570.6261.6560.6881.6560.8131.6561.0632.6561.563
  • full history
  • low memory
The numbers
windowfull historylow memory
2^151.6220.525
2^161.6570.626
2^171.6560.688
2^181.6560.813
2^191.6561.063
2^202.6561.563

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.

Heap the encoders hold as board streams openbench/budget/simulation.csv
01,0002,0003,0004,000MiBopen streams05001,0001,5002,0003,312.0no limit, zstd 2^19, open streams 0: 0.0 MiBno limit, zstd 2^19, open streams 100: 165.6 MiBno limit, zstd 2^19, open streams 200: 331.2 MiBno limit, zstd 2^19, open streams 300: 496.8 MiBno limit, zstd 2^19, open streams 400: 662.4 MiBno limit, zstd 2^19, open streams 500: 828.0 MiBno limit, zstd 2^19, open streams 600: 993.6 MiBno limit, zstd 2^19, open streams 700: 1,159.2 MiBno limit, zstd 2^19, open streams 800: 1,324.8 MiBno limit, zstd 2^19, open streams 900: 1,490.4 MiBno limit, zstd 2^19, open streams 1,000: 1,656.0 MiBno limit, zstd 2^19, open streams 1,100: 1,821.6 MiBno limit, zstd 2^19, open streams 1,200: 1,987.2 MiBno limit, zstd 2^19, open streams 1,300: 2,152.8 MiBno limit, zstd 2^19, open streams 1,400: 2,318.4 MiBno limit, zstd 2^19, open streams 1,500: 2,484.0 MiBno limit, zstd 2^19, open streams 1,600: 2,649.6 MiBno limit, zstd 2^19, open streams 1,700: 2,815.2 MiBno limit, zstd 2^19, open streams 1,800: 2,980.8 MiBno limit, zstd 2^19, open streams 1,900: 3,146.4 MiBno limit, zstd 2^19, open streams 2,000: 3,312.0 MiB1,626.0every stream at zstd 2^18 low memory, open streams 0: 0.0 MiBevery stream at zstd 2^18 low memory, open streams 100: 81.3 MiBevery stream at zstd 2^18 low memory, open streams 200: 162.6 MiBevery stream at zstd 2^18 low memory, open streams 300: 243.9 MiBevery stream at zstd 2^18 low memory, open streams 400: 325.2 MiBevery stream at zstd 2^18 low memory, open streams 500: 406.5 MiBevery stream at zstd 2^18 low memory, open streams 600: 487.8 MiBevery stream at zstd 2^18 low memory, open streams 700: 569.1 MiBevery stream at zstd 2^18 low memory, open streams 800: 650.4 MiBevery stream at zstd 2^18 low memory, open streams 900: 731.7 MiBevery stream at zstd 2^18 low memory, open streams 1,000: 813.0 MiBevery stream at zstd 2^18 low memory, open streams 1,100: 894.3 MiBevery stream at zstd 2^18 low memory, open streams 1,200: 975.6 MiBevery stream at zstd 2^18 low memory, open streams 1,300: 1,056.9 MiBevery stream at zstd 2^18 low memory, open streams 1,400: 1,138.2 MiBevery stream at zstd 2^18 low memory, open streams 1,500: 1,219.5 MiBevery stream at zstd 2^18 low memory, open streams 1,600: 1,300.8 MiBevery stream at zstd 2^18 low memory, open streams 1,700: 1,382.1 MiBevery stream at zstd 2^18 low memory, open streams 1,800: 1,463.4 MiBevery stream at zstd 2^18 low memory, open streams 1,900: 1,544.7 MiBevery stream at zstd 2^18 low memory, open streams 2,000: 1,626.0 MiB1,005.6budget of 1 GiB, open streams 0: 0.0 MiBbudget of 1 GiB, open streams 100: 165.6 MiBbudget of 1 GiB, open streams 200: 331.2 MiBbudget of 1 GiB, open streams 300: 496.8 MiBbudget of 1 GiB, open streams 400: 662.4 MiBbudget of 1 GiB, open streams 500: 828.0 MiBbudget of 1 GiB, open streams 600: 993.6 MiBbudget of 1 GiB, open streams 700: 744.1 MiBbudget of 1 GiB, open streams 800: 850.4 MiBbudget of 1 GiB, open streams 900: 956.7 MiBbudget of 1 GiB, open streams 1,000: 813.0 MiBbudget of 1 GiB, open streams 1,100: 894.3 MiBbudget of 1 GiB, open streams 1,200: 975.6 MiBbudget of 1 GiB, open streams 1,300: 1,004.9 MiBbudget of 1 GiB, open streams 1,400: 1,005.0 MiBbudget of 1 GiB, open streams 1,500: 1,005.1 MiBbudget of 1 GiB, open streams 1,600: 1,005.2 MiBbudget of 1 GiB, open streams 1,700: 1,005.3 MiBbudget of 1 GiB, open streams 1,800: 1,005.4 MiBbudget of 1 GiB, open streams 1,900: 1,005.5 MiBbudget of 1 GiB, open streams 2,000: 1,005.6 MiB127.0budget of 128 MiB, open streams 0: 0.0 MiBbudget of 128 MiB, open streams 100: 106.3 MiBbudget of 128 MiB, open streams 200: 125.2 MiBbudget of 128 MiB, open streams 300: 125.3 MiBbudget of 128 MiB, open streams 400: 125.4 MiBbudget of 128 MiB, open streams 500: 125.5 MiBbudget of 128 MiB, open streams 600: 125.6 MiBbudget of 128 MiB, open streams 700: 125.7 MiBbudget of 128 MiB, open streams 800: 125.8 MiBbudget of 128 MiB, open streams 900: 125.9 MiBbudget of 128 MiB, open streams 1,000: 126.0 MiBbudget of 128 MiB, open streams 1,100: 126.1 MiBbudget of 128 MiB, open streams 1,200: 126.2 MiBbudget of 128 MiB, open streams 1,300: 126.3 MiBbudget of 128 MiB, open streams 1,400: 126.4 MiBbudget of 128 MiB, open streams 1,500: 126.5 MiBbudget of 128 MiB, open streams 1,600: 126.6 MiBbudget of 128 MiB, open streams 1,700: 126.7 MiBbudget of 128 MiB, open streams 1,800: 126.8 MiBbudget of 128 MiB, open streams 1,900: 126.9 MiBbudget of 128 MiB, open streams 2,000: 127.0 MiB
  • no limit, zstd 2^19
  • every stream at zstd 2^18 low memory
  • budget of 1 GiB
  • budget of 128 MiB
The numbers
open streamsno limit, zstd 2^19every stream at zstd 2^18 low memorybudget of 1 GiBbudget of 128 MiB
00.00.00.00.0
100165.681.3165.6106.3
200331.2162.6331.2125.2
300496.8243.9496.8125.3
400662.4325.2662.4125.4
500828.0406.5828.0125.5
600993.6487.8993.6125.6
7001,159.2569.1744.1125.7
8001,324.8650.4850.4125.8
9001,490.4731.7956.7125.9
1,0001,656.0813.0813.0126.0
1,1001,821.6894.3894.3126.1
1,2001,987.2975.6975.6126.2
1,3002,152.81,056.91,004.9126.3
1,4002,318.41,138.21,005.0126.4
1,5002,484.01,219.51,005.1126.5
1,6002,649.61,300.81,005.2126.6
1,7002,815.21,382.11,005.3126.7
1,8002,980.81,463.41,005.4126.8
1,9003,146.41,544.71,005.5126.9
2,0003,312.01,626.01,005.6127.0
Streams that compress as board streams openbench/budget/simulation.csv
05001,0001,5002,000compressed streamsopen streams05001,0001,5002,0002,000no limit, zstd 2^19, open streams 0: 0 compressed streamsno limit, zstd 2^19, open streams 100: 100 compressed streamsno limit, zstd 2^19, open streams 200: 200 compressed streamsno limit, zstd 2^19, open streams 300: 300 compressed streamsno limit, zstd 2^19, open streams 400: 400 compressed streamsno limit, zstd 2^19, open streams 500: 500 compressed streamsno limit, zstd 2^19, open streams 600: 600 compressed streamsno limit, zstd 2^19, open streams 700: 700 compressed streamsno limit, zstd 2^19, open streams 800: 800 compressed streamsno limit, zstd 2^19, open streams 900: 900 compressed streamsno limit, zstd 2^19, open streams 1,000: 1,000 compressed streamsno limit, zstd 2^19, open streams 1,100: 1,100 compressed streamsno limit, zstd 2^19, open streams 1,200: 1,200 compressed streamsno limit, zstd 2^19, open streams 1,300: 1,300 compressed streamsno limit, zstd 2^19, open streams 1,400: 1,400 compressed streamsno limit, zstd 2^19, open streams 1,500: 1,500 compressed streamsno limit, zstd 2^19, open streams 1,600: 1,600 compressed streamsno limit, zstd 2^19, open streams 1,700: 1,700 compressed streamsno limit, zstd 2^19, open streams 1,800: 1,800 compressed streamsno limit, zstd 2^19, open streams 1,900: 1,900 compressed streamsno limit, zstd 2^19, open streams 2,000: 2,000 compressed streams1,236budget of 1 GiB, open streams 0: 0 compressed streamsbudget of 1 GiB, open streams 100: 100 compressed streamsbudget of 1 GiB, open streams 200: 200 compressed streamsbudget of 1 GiB, open streams 300: 300 compressed streamsbudget of 1 GiB, open streams 400: 400 compressed streamsbudget of 1 GiB, open streams 500: 500 compressed streamsbudget of 1 GiB, open streams 600: 600 compressed streamsbudget of 1 GiB, open streams 700: 700 compressed streamsbudget of 1 GiB, open streams 800: 800 compressed streamsbudget of 1 GiB, open streams 900: 900 compressed streamsbudget of 1 GiB, open streams 1,000: 1,000 compressed streamsbudget of 1 GiB, open streams 1,100: 1,100 compressed streamsbudget of 1 GiB, open streams 1,200: 1,200 compressed streamsbudget of 1 GiB, open streams 1,300: 1,236 compressed streamsbudget of 1 GiB, open streams 1,400: 1,236 compressed streamsbudget of 1 GiB, open streams 1,500: 1,236 compressed streamsbudget of 1 GiB, open streams 1,600: 1,236 compressed streamsbudget of 1 GiB, open streams 1,700: 1,236 compressed streamsbudget of 1 GiB, open streams 1,800: 1,236 compressed streamsbudget of 1 GiB, open streams 1,900: 1,236 compressed streamsbudget of 1 GiB, open streams 2,000: 1,236 compressed streams154budget of 128 MiB, open streams 0: 0 compressed streamsbudget of 128 MiB, open streams 100: 100 compressed streamsbudget of 128 MiB, open streams 200: 154 compressed streamsbudget of 128 MiB, open streams 300: 154 compressed streamsbudget of 128 MiB, open streams 400: 154 compressed streamsbudget of 128 MiB, open streams 500: 154 compressed streamsbudget of 128 MiB, open streams 600: 154 compressed streamsbudget of 128 MiB, open streams 700: 154 compressed streamsbudget of 128 MiB, open streams 800: 154 compressed streamsbudget of 128 MiB, open streams 900: 154 compressed streamsbudget of 128 MiB, open streams 1,000: 154 compressed streamsbudget of 128 MiB, open streams 1,100: 154 compressed streamsbudget of 128 MiB, open streams 1,200: 154 compressed streamsbudget of 128 MiB, open streams 1,300: 154 compressed streamsbudget of 128 MiB, open streams 1,400: 154 compressed streamsbudget of 128 MiB, open streams 1,500: 154 compressed streamsbudget of 128 MiB, open streams 1,600: 154 compressed streamsbudget of 128 MiB, open streams 1,700: 154 compressed streamsbudget of 128 MiB, open streams 1,800: 154 compressed streamsbudget of 128 MiB, open streams 1,900: 154 compressed streamsbudget of 128 MiB, open streams 2,000: 154 compressed streams77budget of 128 MiB, at open only, open streams 0: 0 compressed streamsbudget of 128 MiB, at open only, open streams 100: 77 compressed streamsbudget of 128 MiB, at open only, open streams 200: 77 compressed streamsbudget of 128 MiB, at open only, open streams 300: 77 compressed streamsbudget of 128 MiB, at open only, open streams 400: 77 compressed streamsbudget of 128 MiB, at open only, open streams 500: 77 compressed streamsbudget of 128 MiB, at open only, open streams 600: 77 compressed streamsbudget of 128 MiB, at open only, open streams 700: 77 compressed streamsbudget of 128 MiB, at open only, open streams 800: 77 compressed streamsbudget of 128 MiB, at open only, open streams 900: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,000: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,100: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,200: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,300: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,400: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,500: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,600: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,700: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,800: 77 compressed streamsbudget of 128 MiB, at open only, open streams 1,900: 77 compressed streamsbudget of 128 MiB, at open only, open streams 2,000: 77 compressed streams
  • no limit, zstd 2^19
  • budget of 1 GiB
  • budget of 128 MiB
  • budget of 128 MiB, at open only
The numbers
open streamsno limit, zstd 2^19budget of 1 GiBbudget of 128 MiBbudget of 128 MiB, at open only
00000
10010010010077
20020020015477
30030030015477
40040040015477
50050050015477
60060060015477
70070070015477
80080080015477
90090090015477
1,0001,0001,00015477
1,1001,1001,10015477
1,2001,2001,20015477
1,3001,3001,23615477
1,4001,4001,23615477
1,5001,5001,23615477
1,6001,6001,23615477
1,7001,7001,23615477
1,8001,8001,23615477
1,9001,9001,23615477
2,0002,0001,23615477

At 2,000 streams:

Policy Board: compressed heap bytes per frame Page: compressed heap bytes per frame
no limit, zstd 2^19 2,000 3,312 MiB 1,471 2,000 3,294 MiB 189
zstd at the floor, low memory 2,000 1,626 MiB 2,703 2,000 1,278 MiB 379
budget of 128 MiB, at open only 77 129 MiB 136,654 77 129 MiB 13,183
budget of 128 MiB 154 127 MiB 131,336 182 118 MiB 12,491
budget of 128 MiB, 503 154, 1,846 refused 125 MiB 2,703 182, 1,818 refused 116 MiB 379
budget of 1 GiB 1,236 1,006 MiB 55,940 1,456 931 MiB 4,003
  • 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=1 LITH_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.