Skip to content

Performance

This page records stress test results for llmproxy 1.0.0 using the built-in harness at cmd/stress.

What the harness measures

The harness boots a fake OpenAI-compatible upstream and the proxy itself in a single process, over real TCP sockets. It seeds a provider, a model, and an API key into a fresh SQLite database (WAL mode) in a temporary directory, then fires concurrent chat completion requests split between unary and SSE streaming according to -stream-ratio. It reports throughput, latency percentiles per request type, heap growth, and whether usage accounting kept up (usage events recorded N/N).

Because the load generator, the proxy, and the upstream all share one process and one machine, they compete for the same CPU. The numbers below are therefore a conservative floor for proxy-only throughput, not a ceiling. Usage accounting is written asynchronously off the request path, and the harness verifies at the end that every request produced a usage event.

This is a synthetic in-process benchmark, not a network benchmark. Real deployments are bounded by upstream model latency, which is typically measured in seconds, several orders of magnitude above the proxy overhead measured here.

Machine and software

CPU Apple M4 Max, 16 cores
Memory 64 GiB
OS macOS 26.5.1
Go go1.26.1 darwin/arm64
llmproxy 1.0.0

Each scenario was run twice and the better run is shown. All runs completed with zero errors and full usage accounting.

Results

2,000 requests, concurrency 100, half streaming:

requests:      2000 (concurrency 100)
completed:     2000  errors: 0
wall time:     0.21s  throughput: 9340.7 req/s
unary   (1000): p50=8.0ms p95=29.8ms p99=47.8ms max=75.9ms
stream  (1000): p50=7.4ms p95=26.5ms p99=41.1ms max=85.4ms
usage events recorded: 2000/2000
heap: 2.8 MiB -> 8.9 MiB

10,000 requests, concurrency 200, half streaming:

requests:      10000 (concurrency 200)
completed:     10000  errors: 0
wall time:     1.17s  throughput: 8514.3 req/s
unary   (5000): p50=15.6ms p95=68.3ms p99=101.4ms max=197.6ms
stream  (5000): p50=15.6ms p95=69.6ms p99=106.2ms max=198.8ms
usage events recorded: 10000/10000
heap: 2.8 MiB -> 19.7 MiB

50,000 requests, concurrency 500, half streaming:

requests:      50000 (concurrency 500)
completed:     50000  errors: 0
wall time:     6.87s  throughput: 7275.6 req/s
unary   (25000): p50=46.0ms p95=207.6ms p99=314.1ms max=973.0ms
stream  (25000): p50=46.3ms p95=204.2ms p99=320.1ms max=697.1ms
usage events recorded: 50000/50000
heap: 2.9 MiB -> 43.5 MiB

10,000 requests, concurrency 200, all streaming:

requests:      10000 (concurrency 200)
completed:     10000  errors: 0
wall time:     1.22s  throughput: 8227.6 req/s
unary   (0): n/a
stream  (10000): p50=16.3ms p95=70.9ms p99=109.3ms max=276.0ms
usage events recorded: 10000/10000
heap: 2.8 MiB -> 10.7 MiB

10,000 requests, concurrency 200, all unary:

requests:      10000 (concurrency 200)
completed:     10000  errors: 0
wall time:     1.20s  throughput: 8354.3 req/s
unary   (10000): p50=16.2ms p95=69.8ms p99=104.6ms max=204.6ms
stream  (0): n/a
usage events recorded: 10000/10000
heap: 2.9 MiB -> 20.6 MiB

Streaming and unary requests cost about the same, and throughput degrades gently as concurrency rises well past the core count.

Why the numbers hold up

The results depend on one structural rule: the proxy never holds a database connection across an upstream call. Authentication does a single query and returns before the upstream request starts, and the streaming relay holds no store handle at all. Usage events are recorded asynchronously after the response completes. A slow upstream can therefore never pin a SQLite connection, so database throughput stays independent of upstream latency.

Reproducing

Run the default scenario with:

just stress

or run a custom scenario directly:

go run ./cmd/stress -requests N -concurrency C -stream-ratio R

The harness needs no configuration or running services; it creates its own temporary database and cleans up after itself.