millivolt Run it locally
Documentation menu

Performance and resource usage

This page is the single source for the published performance and resource numbers. It records what was measured, how it was measured, and what the result does not prove. The performance and footprint section in operations owns the implementation cost model and go_memory_limit behavior.

These are scoped local measurements of the native proxy process, not deployment guarantees. They were taken on 2026-10-20 UTC. The footprint, fast-response, streaming concurrency, writer, storage and query tables used source revision facb699; the sustained-capacity table used revision 54d1276; the streaming throughput table used revision ca6ead9 on branch perf/e2e-streaming. Measurements used Go 1.27.2 on Linux/amd64 under WSL2. The host was a shared Intel Core Ultra 9 285H with 12 visible logical CPUs and 62 GiB of visible RAM. The earlier tables were measured with builds and tests finished; the streaming throughput ramp ran under variable background load. The public example configuration was used, with private listen and database paths supplied by scripts/dev.sh. Debug captures and body previews were off. No browser or live observer was attached during the load stages. CPU and RSS describe the native Go process, not Docker's container-wide accounting. Percentiles are nearest-rank; RSS was sampled every 100 ms unless stated otherwise.

Method

Item Setting
Date 2026-10-20 UTC
Source revision, footprint and ramps facb69900ac64c19ec3262fcb88b6428d33a68d2
Source revision, sustained capacity 54d1276
Source revision, streaming throughput ca6ead9 (perf/e2e-streaming)
Streaming throughput load Shared host, 1-minute load average 2.6 to 13.1
Go toolchain 1.27.2, CGO_ENABLED=0
Platform Linux/amd64 under WSL2
CPU Intel Core Ultra 9 285H, 12 visible logical CPUs
Visible RAM 62 GiB
Configuration Public example with private listen and database paths
Debug and body previews Off
Browser and live observer None during load stages
Upstream Local synthetic server on loopback
Load shape Closed-loop HTTP stages, then drain
RSS sampling 100 ms unless the idle sample states otherwise
CPU unit 100% equals one logical CPU
Accounting check Durable rows, tokens, cost and drop delta

Artifact and idle footprint

The image was built from the current Dockerfile. The binary was built with CGO_ENABLED=0, -trimpath, -buildvcs=false, and -ldflags="-s -w".

Artifact or state Bytes Display Notes
Static amd64 binary 19,611,808 18.7 MiB Stripped, no CGO
amd64 runtime image 35,109,551 33.5 MiB Docker-reported content size
Compressed image archive 9,047,520 8.63 MiB docker save piped through gzip -9
Idle process, empty SQLite 27,004 KiB 26.4 MiB After a 15-second quiet interval
Idle process CPU 2 scheduler ticks 0.067% Two ticks over 30 seconds at 100 Hz

The image size includes the distroless base, dependency notices, and the embedded UI. It is not a compressed download size, a registry digest, or a persistent-volume estimate. Idle RSS and CPU describe one process on this host. A short sample cannot prove that a longer idle period uses exactly the same resources.

Fast-response throughput

Each stage used a fresh disposable database and the same synthetic response: ten content frames, usage 100/10, and fixture cost 0.001. The upstream added no intentional delay. Each stage started requests for five seconds, then let active requests drain. The final row is a separate ten-second run used to reach about 90,000 durable rows without a storage drop.

Client workers HTTP requests Requests/s p50 p95 p99 Proxy CPU Peak proxy RSS Durable rows
1 4,259 851.5 0.99 ms 2.24 ms 3.41 ms 42.8% 47.8 MiB Exact, 4,259
8 14,997 2,998.0 1.62 ms 8.32 ms 15.03 ms 150.1% 85.6 MiB Exact, 14,997
32 35,746 7,147.0 3.66 ms 9.61 ms 13.66 ms 322.9% 98.0 MiB Exact, 35,746
128 55,713 11,114.5 10.37 ms 23.04 ms 32.88 ms 492.8% 114.3 MiB Exact, 55,713
128, 10 s 90,620 9,055.1 12.42 ms 29.92 ms 42.77 ms 404.4% 115.0 MiB Exact, 90,620

All HTTP responses succeeded in these stages, including the 128-worker stage. That is not a promise that every configuration and history size stays exact under overload. The durable queue is bounded and best-effort; the deliberate overload fixture below shows what a drop result looks like.

At the ten-second 128-worker stage, the separate load generator and synthetic upstream process used 218.8% CPU and 31.8 MiB peak RSS. The proxy figure excludes that process.

Sustained capacity

The sustained capacity point is the highest tested closed-loop level that completed five minutes with zero durable drops and exact accounting. Each level started from a fresh disposable database. The upstream response and client shape were the same as the fast-response table: ten content frames, usage 100/10, and fixture cost 0.001. A level that dropped rows is an overload observation, not a capacity result.

Workers Duration HTTP requests Requests/s p50 p95 p99 Proxy CPU Peak proxy RSS Durable rows Drops
32 300.0 s 3,745,048 12,483.4 2.12 ms 5.13 ms 11.49 ms 409.9% 99.4 MiB Exact, 3,745,048 0
48 300.0 s 4,249,881 14,166.2 2.84 ms 6.96 ms 13.14 ms 475.8% 113.7 MiB Exact, 4,249,881 0
64 300.0 s 4,138,385 13,794.5 3.89 ms 9.66 ms 16.85 ms 456.9% 117.4 MiB 4,088,177 50,208
128 300.0 s 3,789,537 12,631.5 8.28 ms 23.15 ms 40.91 ms 415.5% 134.7 MiB 3,355,678 433,859

The separate load generator and synthetic upstream used 249.0% CPU and 230.9 MiB peak RSS at the 48-worker exact point, and 206.5% CPU and 186.1 MiB at the 128-worker overload point. The proxy figures exclude that process.

The exact five-minute result is 14,166.2 requests/s at 48 workers, with p95 6.96 ms and p99 13.14 ms. The 64-worker point produced 50,208 drops (1.2% of successful requests) and the 128-worker point produced 433,859 drops (11.4%). Those rows show that the bounded durable queue is not a promise of lossless storage at every load. They are not evidence of a universal capacity ceiling; host, configuration, response size, history growth, and attached observers all change the result.

Streaming concurrency

The synthetic upstream emitted ten content frames across approximately one second, followed by usage and a terminator. Each stage started requests for five seconds and then drained. Latency includes the intentional upstream delay, so these numbers are not proxy-only overhead.

These stages measure concurrency and end-to-end latency for that fixed fixture. They do not measure SSE event rate, bytes per second, token throughput, TTFT, chunk-size or hold-duration effects, sustained streaming, or the point where streaming saturates. First-byte p95 is the first response byte, not the first generated token.

Client workers HTTP requests Requests/s p50 p95 p99 First-byte p95 Proxy CPU Peak proxy RSS Durable rows
1 5 1.0 1,009 ms 1,010 ms 1,010 ms 2.86 ms 0.6% 27.9 MiB Exact, 5
8 40 7.9 1,013 ms 1,023 ms 1,024 ms 10.31 ms 2.0% 30.3 MiB Exact, 40
32 160 31.5 1,011 ms 1,017 ms 1,028 ms 10.62 ms 6.5% 35.6 MiB Exact, 160
128 640 124.7 1,010 ms 1,051 ms 1,090 ms 41.30 ms 21.6% 45.7 MiB Exact, 640
512 2,560 490.5 1,014 ms 1,114 ms 1,137 ms 101.64 ms 74.1% 99.1 MiB Exact, 2,560

The 512-worker stage reached 512 upstream connections and remained exact. This is a configured concurrency result on one host, not a universal request-capacity ceiling. The 512 level also matches the example configuration's max_conns_per_host. No higher level, sustained streaming stage, or automatic saturation sweep was run; see Limitations. The generator and upstream used 69.9% CPU and 65.8 MiB peak RSS at that stage.

Streaming throughput

scripts/perf.sh streaming runs the longer end-to-end throughput ramp. Each level starts a fresh private scripts/dev.sh instance and sends requests through the real proxy to a local synthetic SSE upstream. The defaults are 1, 8, 32, 64, 128, 256, 512 and 1024 clients for 60 seconds per level, with 100 content frames over a roughly one-second stream.

Each result line reports requests/s, SSE events/s, response bytes/s, output tokens/s, end-to-end latency, first-byte p95, proxy CPU/RSS and durable rows. The event, byte and token counts come from responses the client compared byte-for-byte with the fixture, so they describe what the client received; a failed response does not contribute. The ramp stops at the first failed stage or durable drop. The per-level JSON carries sse_events_per_second, stream_bytes_per_second and output_tokens_per_second alongside the existing request and latency fields.

This fixture emits 100 content frames per one-second stream, ten times the ten-frame concurrency fixture above, so its CPU and byte rates are not directly comparable to that table. The 2026-10-20 run measured:

Workers HTTP requests Requests/s SSE events/s Output tokens/s Response KiB/s p50 p95 p99 First-byte p95
1 56 0.9 95 93 4.7 1,073 ms 1,083 ms 1,085 ms 3.15 ms
8 456 7.5 767 752 38.2 1,063 ms 1,074 ms 1,083 ms 2.59 ms
32 1,824 30.2 3,076 3,016 153.3 1,061 ms 1,067 ms 1,073 ms 3.89 ms
64 3,648 60.1 6,130 6,010 305.4 1,064 ms 1,074 ms 1,096 ms 4.58 ms
128 7,296 120.2 12,256 12,016 610.5 1,064 ms 1,071 ms 1,105 ms 5.29 ms
256 14,080 231.6 23,620 23,157 1,176.6 1,066 ms 1,267 ms 1,447 ms 19.13 ms
512 29,184 479.3 48,884 47,925 2,435.1 1,064 ms 1,078 ms 1,165 ms 9.17 ms
1024 29,381 473.0 48,243 47,297 2,403.2 2,135 ms 2,183 ms 2,241 ms 1,097.42 ms
Workers Proxy CPU Peak proxy RSS Upstream peak Durable rows Drops
1 1.4% 28.3 MiB 1 Exact, 56 0
8 6.2% 32.7 MiB 8 Exact, 456 0
32 15.9% 42.7 MiB 32 Exact, 1,824 0
64 26.8% 54.6 MiB 64 Exact, 3,648 0
128 54.3% 76.8 MiB 128 Exact, 7,296 0
256 94.2% 109.3 MiB 256 Exact, 14,080 0
512 220.0% 147.4 MiB 512 Exact, 29,184 0
1024 217.5% 183.4 MiB 512 Exact, 29,381 0

At 512 clients the proxy reached 479.3 requests/s, 48,884 SSE events/s, 2,435.1 KiB/s (about 2.4 MiB/s) and 47,925 output tokens/s with exact accounting and no drops. At 1024 clients the upstream peak stayed at 512, throughput stayed flat (473.0 requests/s, 48,243 events/s), and p50 latency doubled from about 1.06 s to 2.14 s; first-byte p95 rose from 9.17 ms to 1,097.42 ms. The extra clients queued behind the configured 512 upstream connections. The proxy used about 2.2 CPU cores at both levels, so this plateau is the configured connection cap, not a demonstrated proxy CPU ceiling. Every level completed with exact durable accounting and zero drops.

This run was taken under variable background load (1-minute load average 2.6 at the start and 13.1 at the end), so its absolute rates are lower and noisier than a quiet-host run. The plateau and the doubled 1024-client latency are the same-host comparison; do not compare the absolute numbers across hosts.

The 1024-client level is deliberately above the example configuration's max_conns_per_host of 512. It measures queueing and the throughput plateau, not 1024 simultaneous upstream connections. This is a fixed-level ramp, not an automatic saturation search; override the levels with PERF_STREAMING_LEVELS when a different range is needed.

Durable writer and overload

These are isolated Go benchmarks from the test overlay, not HTTP runs. Every row and drop count is verified against successful requests. The overload benchmark deliberately fills the bounded queue, so its drop count is a fixture result, not a recommended operating point.

Path Durable rows/s Allocations
Direct batch insert, 256 records per batch 15,397 436,164 B/op, 7,281 allocs/op
Queued Record plus Flush, 8,192 records per batch 31,608 13,649,079 B/op, 232,863 allocs/op
Overload fixture Wire requests/s Durable rows/s Dropped rows
32 workers 74,840 15,760 136,067
128 workers 70,620 13,157 101,213

The overload fixture confirms the accounting invariant durable + dropped = successful requests. Increasing queue capacity alone cannot make sustained overload lossless. A higher HTTP success rate is not a complete-accounting result unless the durable row count and drop delta agree.

Storage and retained projection

The storage fixture stores complete records, indexes, projection triggers, and marshaled response headers in SQLite. Disk bytes include the database, WAL, and shared-memory sidecar after a final flush.

Measure 10,000 records 100,000 records
SQLite bytes total 6.66 MiB 45.35 MiB
SQLite bytes per record 698.4 475.5
Projection retained, built 449.6 B/record 470.8 B/record
Projection interned dimensions 441.9 B/record 429.0 B/record
Projection scanned decode 571.1 B/record 566.6 B/record
Projection transient allocation 2,815 B/record 3,045 B/record

The projection process benchmark is a relative measure inside the test binary, not the deployed proxy's RSS. At 100,000 records it measured 93.84 MiB process RSS and a 76.50 MiB projection delta; at 10,000 records it measured 37.61 MiB process RSS and a 20.27 MiB delta. The test binary and its linked packages are part of the absolute figure.

A loaded proxy run reached a 115.0 MiB peak with 90,620 durable rows while no dashboard projection was preloaded. A full dashboard preload over an authorized 100,000-row history was not remeasured for this page. Use the projection delta above for the retained analytical cost, not the loaded-process peak as a substitute.

Query and metadata paths

The query benchmarks seed a 100,000-row scratch database and decode a 200-row page. These are single-iteration measurements on a shared host, so small differences are not meaningful.

Query Before cursor With cursor Allocations before cursor
Provider equality 4.76 ms 4.48 ms 1.39 MB, 5,858 allocs
Model equality 4.25 ms 4.55 ms 1.38 MB, 5,848 allocs
Client equality 57.06 ms 32.16 ms 1.38 MB, 5,846 allocs
Key-hash equality 47.20 ms 26.54 ms 23.7 KB, 269 allocs
Conversation equality 60.58 ms 36.11 ms 23.7 KB, 268 allocs
Status equality 56.06 ms 33.68 ms 700 KB, 3,145 allocs

The request metadata path was measured separately. It performs one bounded metadata decode and allocates 16 bytes per call.

Metadata body Time per request Throughput Allocations
1 KiB 431.9 ns 2,558.6 MB/s 16 B, 1 alloc
100 KiB 7,504 ns 13,657.6 MB/s 16 B, 1 alloc
1 MiB 108,203 ns 9,691.5 MB/s 16 B, 1 alloc

The 1 MiB case is about 108 microseconds on this host. It is a metadata decode measurement, not a promise that every 1 MiB request body is fully processed in that time.

Reproduce

scripts/perf.sh owns the repeatable local run: bench, stress, streaming, sustained, image, or all. It uses a fresh private scripts/dev.sh instance for each stress mode and stops it through the script's own lifecycle owner.

streaming runs the end-to-end SSE throughput ramp: levels 1, 8, 32, 64, 128, 256, 512 and 1024 for 60 seconds each, with 100 content frames over a one-second stream. PERF_STREAMING_LEVELS, PERF_STREAMING_DURATION, PERF_STREAMING_HOLD, PERF_STREAMING_CHUNKS and PERF_STREAMING_TIMEOUT override those defaults.

sustained runs the fast-response fixture for five minutes at each level in PERF_SUSTAINED_LEVELS (default 128), with PERF_SUSTAINED_DURATION defaulting to 300s. It does not run the held streaming fixture.

The individual commands below are the same evidence path. Use a fresh DEV_COPY_DB=0 instance for each workload. Never target the main instance on port 8080. The stress harness verifies that the target is the private scripts/dev.sh instance before sending traffic.

(
  set -e
  trap 'scripts/dev.sh stop' EXIT
  export MILLIVOLT_OPERATOR_TOKEN='dev-instance-credential'
  DEV_COPY_DB=0 scripts/dev.sh
  go run ./cmd/stress -target http://127.0.0.1:8081 \
    -concurrency 1,8,32,128 -duration 5s -stream-duration 0
)

Use a separate fresh instance for the streaming ramp:

(
  set -e
  trap 'scripts/dev.sh stop' EXIT
  export MILLIVOLT_OPERATOR_TOKEN='dev-instance-credential'
  DEV_COPY_DB=0 scripts/dev.sh
  go run ./cmd/stress -target http://127.0.0.1:8081 \
    -concurrency 1,8,32,128,512 -duration 5s -stream-duration 1s
)

The isolated benchmarks use the test overlay:

scripts/check.sh go test -run='^$' -bench='BenchmarkProjectionFootprint|BenchmarkStoreDiskBytes' \
  -benchtime=1x -count=1 ./internal/web
scripts/check.sh go test -run='^$' -bench='BenchmarkProjectionProcessRSS' \
  -benchtime=1x -count=1 ./internal/web
scripts/check.sh go test -run='^$' -bench='BenchmarkStorageWriter' \
  -benchtime=2s -benchmem -count=1 ./internal/storage
scripts/check.sh go test -run='^$' -bench='Benchmark(QueryBeforeEquality|QueryBeforeCount)' \
  -benchtime=1x -benchmem -count=1 ./internal/storage
scripts/check.sh go test -run='^$' -bench='Benchmark(RequestMetadata|ProxyDurableContention)' \
  -benchtime=2s -benchmem -count=1 ./internal/proxy

The image measurements use a disposable local image:

docker build --platform linux/amd64 -t millivolt-perf .
docker image inspect millivolt-perf --format '{{.Size}}'
docker save millivolt-perf | gzip -9 | wc -c

Idle RSS and CPU were sampled from /proc/<pid>/status and /proc/<pid>/stat after a 15-second settle. The process was started and stopped only through scripts/dev.sh.

Limitations

  • The host was shared. Other workloads can change absolute rates and latency.
  • The upstream was a local synthetic server. Provider latency, TLS, retries, payload shape, and real identity cardinality are not represented.
  • These are short closed-loop bursts, not sustained production capacity. No result here is a capacity ceiling or a guarantee.
  • The streaming concurrency table is a five-second ramp on a ten-frame, roughly one-second fixture at 1, 8, 32, 128 and 512 workers. The streaming throughput table is a 60-second-per-level ramp on a 100-frame, roughly one-second fixture at 1, 8, 32, 64, 128, 256, 512 and 1024 workers. Both are synthetic-fixture measurements, not universal maxima. The throughput table's 1024 level is above the example max_conns_per_host of 512 and measures queueing; it is a fixed level list, not an automatic saturation search. Its token rate is the fixture's usage count, not provider-token throughput, and the run was taken under variable background load.
  • The five-minute sustained sweep uses the fast-response fixture with no intentional stream hold (-stream-duration 0) at 32, 48, 64 and 128 workers. Its 64-worker durable-drop boundary is a writer/accounting boundary, not a maximum streaming rate. The streaming throughput ramp is the only sustained held-stream run; it stops at its fixed levels and does not search beyond 1024.
  • Performance checks are manual (scripts/perf.sh), not CI. Each level is one run per invocation on one host; there is no repetition or variance study.
  • Durable accounting is best-effort. The fast stages happened to remain exact; the overload benchmark intentionally does not.
  • Container-wide CPU/RSS, cold-start time, browser rendering, arm64 image size, disk throughput, and a full dashboard preload over a large authorized history were not measured for this page.
  • The image archive is a local artifact, not a published registry digest.
  • The source revision is the runtime tree before the later documentation, site, test, and login-page color commits in the same change set. Those commits do not change the request accounting or storage paths measured here.
  • The current numbers supersede the older v0.1.0 example. Do not compare them directly across differing hosts, workloads, or revisions.
search

Search the docs

Search titles, headings, and body text. Press / from anywhere.