Skip to content

Resource Benchmark

Nothing else published about this server says what it costs to run. This page does: resident memory, processor time, goroutine count and per-method latency, measured on both transports across the settings that move them.

The real binary, built from the current checkout and started the way a client starts it, on both transports. GitLab is stood in by an HTTP server inside the harness, so a run needs no instance, no credentials and no network, and two people re-measuring compare this server rather than their GitLab installations. The tool surface is passed explicitly rather than read from the environment, so a developer machine and CI measure the same thing.

Everything below is regenerated by one command, make bench-resources, which runs the benchmark and redraws the charts from the record it writes. The machine, the build and the method are named in the text under the heading and along the bottom edge of every chart, because a resident-set figure without them is a number nobody can act on.

This page is a snapshot, and only a snapshot. Every release re-measures on the reference host and replaces these values wholesale: no “before” column, no ratio against an earlier run, no history. A figure compared against a run whose code no longer exists says nothing about the instance you are sizing.

The point scenarios run 64 credentials on HTTP and 8 processes on stdio, admitting them one at a time and then loading all of them at once. 64 sits past the point where the host saturates and below the pool’s own default bound of 100, so the scenario runs the shipped default and measures real contention; 8 on stdio, because each one is a whole process with its own catalog and eight concurrent clients is a busy developer machine.

A shared deployment meets hundreds, so the benchmark also runs a concurrency series per surface: one HTTP process, given 1, 2, 5, 10, 20, 50, 100, 200, 500 and 1000 distinct credentials in turn, each warmed with a tools/list, and measured over a ten-second steady phase at each count in which every credential keeps calling tools/call and tools/list. Each step records the resident set (mean and peak), processor time per call, the p50 and p99 of both methods and the goroutine count, and captures a CPU and a heap profile of the server through its --pprof-addr listener, which the analysis reads and the site does not publish.

Each step is then measured a second time, with the load stopped. The two readings answer different questions: the resident set over the steady phase is what N credentials cost while all of them are calling, and the settled reading, taken after the load ends with a collection forced, is what they cost to hold. The first is measured in megabytes per credential and the second in kilobytes, so one is never a substitute for the other. The settled resident set is recorded beside the settled heap and lags it, because freeing a heap does not hand the pages back: Go’s scavenger returns them on its own schedule.

A series stops early rather than take the host down: before each step the resident set it would reach is estimated from the steps so far, and the rest of the list is skipped when the estimate exceeds the memory budget (80% of the host’s available memory unless one is given); a step whose tools/call p99 passes 30 seconds is the last one run. Where a series stopped, and why, is printed under its table. In the run published here none of them stopped: all three reached a thousand credentials.

Measured on Intel(R) Core(TM) i5-14400, 16 logical CPUs, 62 GiB RAM, linux/amd64, kernel 6.12.105-production+truenas, go1.27.1, build 2.7.6-0.20260906081503-18bed59da189 (18bed59d), 2026-09-06T08:24:43Z. 3 rounds per method, resident set sampled every 100 ms.

Grouped bars comparing resident memory across the dynamic, meta and individual surfaces on both transports.
Lines showing resident memory growing as each new credential builds its own catalog in the HTTP pool.
Grouped bars on a log scale comparing process readiness, the first cold tools/list and a warm one.
Grouped bars on a log scale comparing resources/list, tools/call and tools/list latency across transports and surfaces.
Lines on a log scale of credentials showing each surface's peak resident memory per step, the memory budget, and the count each series stopped at.
Lines on log scales showing the tools/call p50 and p99 per surface as the credential count grows.
Lines on a log scale of credentials showing the processor time per call for each surface.

Memory, goroutines and processor time per scenario

Section titled “Memory, goroutines and processor time per scenario”
ScenarioClientsIdleOne clientAll clientsPer extra clientPeakGoroutinesCPU, % of one core
stdio, dynamic8n/a106849106995341039%
stdio, meta8n/a107857107101836794%
stdio, individual8n/a27721342652291361120%
stdio, dynamic, telemetry8n/a1118931121048431080%
http, dynamic64351081180375291974%
http, meta64371121180324293928%
http, individual6436282248-114221651147%
http, dynamic, telemetry643811312203833001018%
ScenarioProcess readyFirst tools/listWarm tools/list (p50)tools/list payload
stdio, dynamic0.263511.912 KB
stdio, meta0.1734142598 KB
stdio, individual0.1812793793.2 MB
stdio, dynamic, telemetry0.243331.712 KB
http, dynamic593192112 KB
http, meta51324270599 KB
http, individual51127723943.2 MB
http, dynamic, telemetry513192012 KB
ScenarioMethodCallp50p90p99Max
stdio, dynamicresources/listsmallest listing0.657.67.77.7
stdio, dynamictools/callgitlab_find_action30414949
stdio, dynamictools/listwhole surface1.96.06.26.2
stdio, metaresources/listsmallest listing0.640.991.21.2
stdio, metatools/callgitlab_server (status)1.7404141
stdio, metatools/listwhole surface42515555
stdio, individualresources/listsmallest listing0.570.780.930.93
stdio, individualtools/callgitlab_server_status0.871.41.81.8
stdio, individualtools/listwhole surface379405424424
stdio, dynamic, telemetryresources/listsmallest listing0.790.870.950.95
stdio, dynamic, telemetrytools/callgitlab_find_action30475050
stdio, dynamic, telemetrytools/listwhole surface1.72.32.62.6
http, dynamicresources/listsmallest listing33488085
http, dynamictools/callgitlab_find_action213307314314
http, dynamictools/listwhole surface21353838
http, metaresources/listsmallest listing10556266
http, metatools/callgitlab_server (status)23313334
http, metatools/listwhole surface270388403404
http, individualresources/listsmallest listing9.8133030
http, individualtools/callgitlab_server_status8.4161818
http, individualtools/listwhole surface2394285529292949
http, dynamic, telemetryresources/listsmallest listing14535961
http, dynamic, telemetrytools/callgitlab_find_action213301314315
http, dynamic, telemetrytools/listwhole surface20353637

http, dynamic surface: 4 in flight per credential, 10 s per step, memory budget 35000 MiB

Section titled “http, dynamic surface: 4 in flight per credential, 10 s per step, memory budget 35000 MiB”
CredentialsResident, meanResident, peakSettled heapSettled residentCPU per callCallstools/call p50tools/call p99tools/list p50tools/list p99Goroutines
114315439.01067.394689511140.963.418
217219239.11077.6331107013181.14.722
521723239.21097.9101368423433.91636
1024127739.51107.97113660431187.55356
2028936440.01157.84314254752741114796
5041448141.51227.6291677715176611471216
10054760844.01337.63917482269157815956416
200869107048.91577.674178514063572201757816
5001958308964.02327.8511718791193135241882015
1000897145488.32889.1761611931068857221663574015

Fitted across these steps: the peak resident set under load grows 1.92 MiB per credential, and the settled live heap, read with the load stopped and a collection forced, grows 50.6 KiB per credential. The first is what a credential costs while it and every other one is calling; the second is what it costs to hold. The settled resident set lags both, because Go returns freed pages to the operating system on its own schedule. Every planned step ran, up to 1000 credentials.

http, meta surface: 4 in flight per credential, 10 s per step, memory budget 35000 MiB

Section titled “http, meta surface: 4 in flight per credential, 10 s per step, memory budget 35000 MiB”
CredentialsResident, meanResident, peakSettled heapSettled residentCPU per callCallstools/call p50tools/call p99tools/list p50tools/list p99Goroutines
113414632.693.08.01463230.974.0111620
216217932.794.78.00195441.45.3152024
518520332.894.78.512108106.920294936
1019922633.196.08.4891102516535111756
2023225733.61008.23411486341469124796
5029838835.21077.95312021138451165530216
10029540637.91168.51711842349775348700416
20037653143.21358.4581242774714816481250816
50051468959.51848.5991400814143252176330772015
1000749100983.52438.3301613017916808318060284015

Fitted across these steps: the peak resident set under load grows 0.81 MiB per credential, and the settled live heap, read with the load stopped and a collection forced, grows 52.6 KiB per credential. The first is what a credential costs while it and every other one is calling; the second is what it costs to hold. The settled resident set lags both, because Go returns freed pages to the operating system on its own schedule. Every planned step ran, up to 1000 credentials.

http, individual surface: 2 in flight per credential, 10 s per step, memory budget 35000 MiB

Section titled “http, individual surface: 2 in flight per credential, 10 s per step, memory budget 35000 MiB”
CredentialsResident, meanResident, peakSettled heapSettled residentCPU per callCallstools/call p50tools/call p99tools/list p50tools/list p99Goroutines
127329688.1186111.9082621.14.915126818
229633888.3183116.9384801.16.916420120
542953088.4191133.5647661.31126229126
1053867388.5194137.591855176145459336
20731107388.8200134.65995338257784131956
501081130389.7211127.56811106880416853502116
1001266172491.1215126.6721292213257228287144216
2002244327494.0248122.50214992393428543012328415
50025223427103277110.33023612851164311082181631015
10003330467711732698.265363749741655916796252292015

Fitted across these steps: the peak resident set under load grows 4.27 MiB per credential, and the settled live heap, read with the load stopped and a collection forced, grows 29.8 KiB per credential. The first is what a credential costs while it and every other one is calling; the second is what it costs to hold. The settled resident set lags both, because Go returns freed pages to the operating system on its own schedule. Every planned step ran, up to 1000 credentials.

stdio. One process per client, and the process builds its catalog as soon as it starts, so there is no idle state to save memory in. Between 106 and 277 MiB per process depending on the surface, with peaks under load a little higher. Nothing is shared between processes, so the cost is a straight line in the client count: a developer machine running one client will not notice, and the “all clients” column is what eight of them weigh together.

HTTP. Start from 35 to 38 MiB for a process serving nobody, then add one catalog per distinct configuration, built by the first credential that asks for it and shared by every other credential of that configuration. That is what the “per extra client” column reports: 0.15, 0.09 and -0.53 MiB on dynamic, meta and individual, which is nothing above the noise of a resident-set reading, across a ladder that admits 64 credentials one at a time. What is left to size for is the concurrent work, which is the peak column beside it: 324 to 1422 MiB with all 64 calling.

There are two axes, and only one of them is the pool. Holding a credential costs 50.6 KiB of settled live heap on dynamic, 52.6 KiB on meta and 29.8 KiB on individual, so a thousand admitted credentials are 88, 83 and 117 MiB of live heap and under a third of a gibibyte of resident set. Serving them is where an instance runs out: with every credential holding two to four requests in flight, the peak resident set grows 1.92, 0.81 and 4.27 MiB per credential, and at the thousand-credential step it stood at 1.5, 1.0 and 4.7 GiB.

A practical rule for HTTP: budget for concurrent callers, at 1 to 4 MiB each depending on the surface, on top of 40 MiB for the process and one catalog per configuration. --max-http-clients bounds pooled credentials and is not a memory setting: at 50 KiB each, its default of 100 is five mebibytes. Shorten --pool-idle-timeout if you want entries reclaimed sooner, but expect the memory back in kilobytes rather than gigabytes.

Throughput. In the series, the calls completed per step stop growing at about five credentials on dynamic and meta: a call costs 8 ms of processor time, so a sixteen-thread host completes 11,000 to 17,900 per ten-second step whatever the number of credentials, and latency past that point is queueing, with the p50 of tools/call on dynamic climbing from 11 ms at one credential to 269 ms at a hundred and 3.1 seconds at a thousand. individual is a different shape: a tools/call costs about a millisecond, but a call there averages 120 ms of processor time, because every second call is a tools/list serialising three megabytes of schemas, so a single credential with two requests in flight already keeps three cores busy.

The process is ready in milliseconds and the surface behind it is not. On HTTP /health answers in 51 to 59 ms while the pool holds nothing; the first credential of a configuration builds its catalog, and that is where the wait is: 0.32 seconds on dynamic, 0.32 on meta and 1.28 on individual on the host measured. On stdio the process is executed in well under a millisecond and starts building immediately, so its first request waits on the same work, at 0.35, 0.34 and 1.28 seconds.

That wait is per configuration rather than per credential, since the built server is shared, and it returns: when every entry of a configuration has been reclaimed by --pool-idle-timeout or evicted at the pool’s size bound, the next request that needs it rebuilds from scratch.

Once warm, the picture inverts. A tools/list costs 1.9 to 21 ms on dynamic, 42 to 270 ms on meta and 0.38 to 2.4 seconds on individual, which is the cost of serializing 12 KB, 599 KB and 3.2 MB respectively, the higher end of each pair being HTTP with all 64 credentials calling. A client speaking protocol 2026-07-28 may serve repeat listings from its own cache and pay this less often, but it pays it at least once per session.

Exporting over OTLP costs a handful of goroutines and measurable processor time, and no material memory. It is off by default; see OpenTelemetry for what it exports and where it goes.

Terminal window
make bench-resources # measure, then redraw everything
make bench-resources-render # redraw from the committed record, no measuring
make check-bench-resources # verify the charts match the record

The eight point scenarios take five to ten minutes on the reference host: the stdio half spends most of it building one catalog per process, which is the cost being measured rather than overhead of the harness, and the HTTP half spends it serving 64 credentials at once. The series then take as long as the host’s memory lets them, up to a quarter of an hour per surface. The series can also be measured on one host and rendered on another: build the driver and the server, and on the host with the memory run the driver with -binary, -json, -profiles and -no-render, which reads nothing from a repository; then copy the record back and run make bench-resources-render. The developer-facing details, including what the harness deliberately does not measure, are in the resource benchmark reference.