gunnar
Fast setup: the guide · PDF — four lists of commands, one picture per setup.
Features: what is new, with status, source path and commit.
Git server in Rust, with two engine variants: znippy-based (Apache Arrow IPC in a znippy archive) or gix-based (traditional filesystem).
- Built-in replication to a twin gunnar: self-hosted, the twin is any box that reaches the primary (
gunnar control twin join); a gunnar.rs instance's twin runs in the same zone as its main. - Vetra hardware appliance variant: OS-less, hardened kernel.
- Encrypted repositories, quantum-safe by default: files are sealed on the client, the key ring is wrapped only to members' ML-KEM-768 + X25519 keys, and the server holds ciphertext only.
- Free to host your own.
- The exact same code is hosted at https://www.gunnar.rs
- NVMe, io_uring, XFS on raid0, OS-less appliance kernel, WireGuard, Prometheus, Git LFS, smart HTTP, OCI, webhooks, replication to a twin, sshsig/ed25519 signed pushes, SLSA-shaped provenance, transparency log.
Building
Anyone can clone the repository read-only, with no registered SSH key:
git clone ssh://anonymous@git.gunnar.rs:2222/nordisk/gunnar.git
cd gunnar
cargo build --release -p gunnar-cli
A lone clone builds the server by itself, with nothing beside it:
git clone ssh://git.gunnar.rs:2222/nordisk/gunnar && cd gunnar && cargo build --release
./target/release/gunnar # serves on loopback; http://127.0.0.1:8080/healthz answers ok
cargo build --release at the root builds the default member, gunnar (the server: engine-znippy, SSH and smart HTTP with TLS via --http-tls-*; no mimalloc, no company SSO, no monetize: --features mimalloc,sso adds those). The committed Cargo.lock is the crates.io lock, so cargo build --locked and cargo install --locked --path crates/gunnar-cli --features client work from a lone clone too (cargo xtask lone-clone proves it on every push). The root takes no sibling patch in any checkout. In an estate checkout, with ../edda, ../znippy, ../Skidbladnir, ../tunnr, ../draupnir and ../monetize beside it, the detached estate workspaces (gunnar-front, gunnar-monetize-guest, the benches, installtest) build against those siblings through the [patch.crates-io] in their own manifests, with --manifest-path.
This page documents gunnar 1.6.0 — the version gunnar --version prints.
License: AGPL-3.0-only, © 2026 Vetra Automation AB and Ignalina ApS (Rickard Ernst Björn Lundin). Commercial terms: COMMERCIAL-LICENSING.md.
Install the server with cargo
cargo install gunnar
gunnar
A bare gunnar serves on loopback with no configuration: SSH on 127.0.0.1:2222, smart HTTP on 127.0.0.1:8080, and health at http://127.0.0.1:8080/healthz. The default build compiles no monetize crate; the other variants are opt-in cargo features, listed in crates/gunnar/README.md.
Gunnar by hand
This guide installs Gunnar's three products from their crates: gunnar (the server), gunnar-ui (the web console) and gunnar-cli (the command-line tool). It signs in to the console with a passkey, creates a repository, pushes to it over SSH, makes an encrypted (quantum-safe) repository and adds a twin. Every command is copy-pasteable on one Linux host. You need cargo, git, OpenSSH, curl and a browser. Everything listens on loopback. The integration test cargo xtask products runs exactly these blocks, in this order.
0. The places and the names. Everything lives under $G. The main server, its twin and the console each get their own ports. Your SSH key is ~/.ssh/id_ed25519 (made here if you have none); git and gunnar-cli sign in with it.
export G="${G:-$HOME/gunnar-guide}"
export SSH="${SSH:-127.0.0.1:2222}" HTTP="${HTTP:-127.0.0.1:8080}" CTRL="${CTRL:-127.0.0.1:50051}"
export TWIN_SSH="${TWIN_SSH:-127.0.0.1:2223}" TWIN_HTTP="${TWIN_HTTP:-127.0.0.1:8081}" TWIN_CTRL="${TWIN_CTRL:-127.0.0.1:50052}"
export UI="${UI:-3210}"
mkdir -p "$G" ~/.ssh
[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -q -t ed25519 -N "" -f ~/.ssh/id_ed25519
export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$G/known_hosts"
export GIT_AUTHOR_NAME=alice GIT_AUTHOR_EMAIL=alice@mock.invalid GIT_COMMITTER_NAME=alice GIT_COMMITTER_EMAIL=alice@mock.invalid
1. The server
Install the server and the command-line tool, and start the server with its own data root. It serves git over SSH and smart HTTP, and its control plane on loopback:
cargo install gunnar gunnar-cli
gunnar --data-root "$G/main" serve --ssh-bind "$SSH" --http-bind "$HTTP" --grpc-bind "$CTRL" > "$G/main.log" 2>&1 &
until curl -fsS "http://$HTTP/healthz" > /dev/null 2>&1; do sleep 0.2; done
The first call claims it: alice becomes its administrator, with your key. It works once.
cli() { gunnar-cli control "$@" -i ~/.ssh/id_ed25519; }
gunnar-cli control status --at "$CTRL"
cli bootstrap alice --key @"$HOME/.ssh/id_ed25519.pub" --at "$CTRL"
2. The console, and a passkey
Install the console, give it a key of its own, and enrol that key with the server (a machine with an admin grant: the console signs every call it makes for you). Then start it for http://localhost:3210; a passkey's relying party is a domain, and localhost is one.
cargo install gunnar-ui
ssh-keygen -q -t ed25519 -N "" -f "$G/ui-id"
export GUNNAR_UI_CONTROL="$CTRL" GUNNAR_UI_IDENTITY="$G/ui-id" GUNNAR_UI_DATA="$G/ui" GUNNAR_UI_STATE="$G/ui/state.redb"
mkdir -p "$G/ui" && gunnar-ui enroll --admin-key ~/.ssh/id_ed25519 --name gunnar-ui
GUNNAR_UI_BIND="127.0.0.1:$UI" GUNNAR_UI_WEBAUTHN_RP_ID=localhost GUNNAR_UI_WEBAUTHN_ORIGIN="http://localhost:$UI" gunnar-ui > "$G/ui.log" 2>&1 &
until curl -fsS "http://127.0.0.1:$UI/health" > /dev/null 2>&1; do sleep 0.2; done
Sign in with a passkey: the server mints a first-login link for alice (its secret is shown once), and you open it. The page registers your passkey (touch your security key, or use the one your browser keeps) and signs you in as alice, the administrator.
SECRET=$(cli invite create alice --first-only --at "$CTRL" | awk '$1 == "secret" {print $2}')
xdg-open "http://localhost:$UI/login?invite=$SECRET&user=alice"
3. A repository, and a push over SSH
cli repo create alice/hello --at "$CTRL"
git clone "ssh://git@$SSH/alice/hello.git" "$G/hello"
cd "$G/hello"
echo "hello, Gunnar" > README.md
git add README.md && git commit -q -m "the first commit" && git push -q origin HEAD:refs/heads/main
cd "$G"
The console shows it at http://localhost:3210/alice/hello.
4. An encrypted repository, quantum-safe
The files are sealed on your machine before they leave it, and the server holds ciphertext only. gunnar-cli crypt init makes the repository's key ring, wrapped to your encryption key: quantum-safe (ML-KEM-768 + X25519) unless you ask for --level classic. You have no encryption key yet, so it makes one, keeps it at the server wrapped to your SSH key, and commits the three files that make the repository encrypted.
cli repo create alice/vault --at "$CTRL"
git clone "ssh://git@$SSH/alice/vault.git" "$G/vault"
cd "$G/vault"
gunnar-cli crypt init --member alice
echo "the launch code is 0000-1111" > plan.txt
git add plan.txt && git commit -q -m "the plan" && git push -q origin HEAD:refs/heads/main
gunnar-cli crypt status | grep "quantum-safe"
cd "$G"
A plain clone, without your key, sees ciphertext only (no line of plan.txt says it):
git clone -q "ssh://git@$SSH/alice/vault.git" "$G/vault-plain"
if grep -q "launch code" "$G/vault-plain/plan.txt"; then echo "the server holds plaintext"; false; fi
5. A twin
A twin is a second Gunnar that holds a copy of every repository and can take over. Start it, claim it with the same key, make it a standby (read-only to people; it takes only its primary's replication pushes), and add it to the main. The main mints its own push key and prints the public line, which the twin authorises as a machine with an admin grant on the namespace (the twin creates each repository; the main asks the twin's control plane, --control, to make each one in its own object format). Then backfill it: adding a standby copies nothing that is already there.
gunnar --data-root "$G/twin" serve --ssh-bind "$TWIN_SSH" --http-bind "$TWIN_HTTP" --grpc-bind "$TWIN_CTRL" > "$G/twin.log" 2>&1 &
until curl -fsS "http://$TWIN_HTTP/healthz" > /dev/null 2>&1; do sleep 0.2; done
cli bootstrap alice --key @"$HOME/.ssh/id_ed25519.pub" --at "$TWIN_CTRL"
cli replication standby --at "$TWIN_CTRL"
cli replication add twin "ssh://git@$TWIN_SSH" --control "$TWIN_CTRL" --at "$CTRL" | tee "$G/twin-add.txt"
PRIMARY_KEY=$(sed -n 's/^ *\(ssh-ed25519 .*\)$/\1/p' "$G/twin-add.txt" | head -n 1)
cli user add primary --key "$PRIMARY_KEY" --kind machine --at "$TWIN_CTRL"
cli grant primary admin --namespace alice --at "$TWIN_CTRL"
cli replication resync twin --at "$CTRL"
for try in $(seq 120); do cli replication list --at "$CTRL" | grep -q "copied.*alice/hello" && break; sleep 1; done
cli replication list --at "$CTRL" | grep "copied.*alice/hello"
git clone -q "ssh://git@$TWIN_SSH/alice/hello.git" "$G/hello-from-the-twin"
git -C "$G/hello-from-the-twin" log --oneline | grep "the first commit"
The twin takes over with cli replication promote --at "$TWIN_CTRL" (and the old main steps down with cli replication demote and cli replication standby); see the guide, §3. Stop the three programs: kill %1 %2 %3.
The old program names, and what they are now:
| was | is |
gunnar from gunnar-cli (the git client) |
gunnar-cli (cargo install gunnar-cli): every verb it had, and control |
gunnar-solo |
cargo install gunnar-cli --features server: both halves in one program |
gunnar-service |
cargo install gunnar-cli --features systemd: the server as a systemd service |
gunnar-ui-server |
gunnar-ui (cargo install gunnar-ui); gunnar-ui-server is the same program under its old name |
Release provenance (SLSA Build L3)
Releases are built by dwarves, the build server, in its isolated release-build mode and carry signed SLSA v1 provenance: one in-toto statement per release, every artifact by its sha256, signed on the owner's YubiKey. The bundles are in attestations/, the release key in slsa/; how to verify (cosign, dwarves or Holger) and what still keeps us below L3: docs/SLSA.md.
Complexity and measurements
n objects, r refs, e refs in a batch, k batch size, s object size, |W|/|H| want/have closure. Read off the znippy engine's code.
| operation | znippy |
put_pack |
O(bytes), index deferred |
put_refs |
O(r) + O(e·log n) |
get |
O(log n) + O(s) |
has |
O(log n) |
size |
O(log n) |
extents |
O(k·log n) |
refs |
O(r) |
update_ref |
O(log r) |
head |
O(log r) |
header |
O(log n) |
reachable |
O(1) map lookup + bitmap OR |
gc |
O(n) compact |
Measured: the table below is rendered from the newest result file of each bench in crates/gunnar-perfbench/results/ by cargo xtask bench-readme write, and xtask/tests/bench_readme.rs is red when it is anything else. The weekly CI run (bench-gunnar in the dwarves forge, on oden) commits a new result and this block. The older hand-copied rows (a synthetic 16 384-commit repository, the gitea comparison, 2026-08 and 2026-09 on the desktop) are gone: no bencher run backs them today.
Every number here comes from a bencher run, rendered from its result file; none is typed in. Wall time per operation (p50, p99), CPU per operation (server and client), the server's peak RSS during that step (heap = RssAnon, all = VmHWM, which also counts mapped pack pages), and the load1 of the machine at that step. A cost row is what a query pays to get there: the first push, the index build after a restart, the rebuild after a push.
Source: gunnar-perfbench-index-build (crates/gunnar-perfbench/results/2026-11-01-oden-gunnar-perfbench-index-build.json), measured on oden (32 threads, 499 GiB RAM) on 2026-11-01, load1 77.8 to 102.3 during the run (74.6 before it), built from gunnar 172aa1a3 + znippy-plugin-git 0.2.3 + GP-7 (lane gitidx after-gp7 binary). Real clients: git 2.47.3, OpenSSH_10.0p2 Debian-7+deb13u4, OpenSSL 3.5.6 7 Apr 2026, git-lfs/3.6.1 (GitHub; linux amd64; go 1.24.4). One release build with both engines (--features engine-gix), the engine picked per run by GUNNAR_ENGINE.
| repository | step | engine | p50 | p99 | server CPU/op | client CPU/op | peak RSS heap / all | rate | run |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: index build: a restarted server's first fetch of a new commit (znippy-plugin-git builds its reach index here and persists it), HTTP (n=1) | znippy | 15.6 s | 15.6 s | 13.4 s | 70.0 ms | 396 MiB / 695 MiB | — | oden, 2026-11-01, load 102.3 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | memory: memory of the idle server, just started (22 repositories) | znippy | — | — | — | — | 5 MiB / 24 MiB | — | oden, 2026-11-01, load 77.8 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | memory: memory held after the run, not what one request needs: the handle cache (up to 256 open repositories, stores::DEFAULT_OPEN_REPOS, each with its redb cache) and the reach bitmaps (22 repositories) |
znippy | — | — | — | — | 353 MiB / 657 MiB | — | oden, 2026-11-01, load 102.3 |
Source: gunnar-perfbench-window-after-gp7 (crates/gunnar-perfbench/results/2026-11-01-oden-gunnar-perfbench-window-after-gp7.json), measured on oden on 2026-11-01, load1 1.6 to 3.1 during the run (1.8 before it), built from gunnar 172aa1a3 + znippy-plugin-git 0.2.3 + GP-7 (lane gitidx after-gp7 binary). Lane gitidx quiet bench window (oden freshly rebooted, nothing else running), GUNPERF_ONLY=clone,fetch, znippy only; with GP-7 a git.git clone takes the reach index.
| repository | step | engine | p50 | p99 | server CPU/op | client CPU/op | peak RSS heap / all | rate | run |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: rebuild after a push: push one commit, fetch it (znippy: the index update; gix: a walk), HTTP (n=10) | znippy | 119 ms | 152 ms | 55.0 ms | — | — / 936 MiB | — | oden, 2026-11-01, load 2.6 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: rebuild after a push: push one commit, fetch it (znippy: the index update; gix: a walk), SSH (n=10) | znippy | 141 ms | 150 ms | 57.0 ms | — | — / 1.1 GiB | — | oden, 2026-11-01, load 2.8 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | clone (bare, warm), HTTP (n=3) | znippy | 11.5 s | 11.6 s | 930 ms | 35.1 s | 883 MiB / 1.2 GiB | 27.2 MB/s | oden, 2026-11-01, load 3.1 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | clone (bare, warm), SSH (n=3) | znippy | 11.5 s | 22.1 s | 4.9 s | 35.8 s | 913 MiB / 1.2 GiB | 27.2 MB/s | oden, 2026-11-01, load 1.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | fetch with nothing new, HTTP (n=20) | znippy | 29.1 ms | 31.3 ms | 1.50 ms | 28.0 ms | 792 MiB / 1.1 GiB | — | oden, 2026-11-01, load 2.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | fetch with nothing new, SSH (n=20) | znippy | 34.1 ms | 36.5 ms | 2.50 ms | 31.5 ms | 792 MiB / 1.1 GiB | — | oden, 2026-11-01, load 3.1 |
Source: gunnar-perfbench (crates/gunnar-perfbench/results/2026-10-31-oden-gunnar-perfbench.json), measured on oden (32 threads, 499 GiB RAM) on 2026-10-31, load1 31.3 to 137.5 during the run (64.4 before it), built from gunnar 172aa1a3. Real clients: git 2.47.3, OpenSSH_10.0p2 Debian-7+deb13u4, OpenSSL 3.5.6 7 Apr 2026, git-lfs/3.6.1 (GitHub; linux amd64; go 1.24.4). One release build with both engines (--features engine-gix), the engine picked per run by GUNNAR_ENGINE.
| repository | step | engine | p50 | p99 | server CPU/op | client CPU/op | peak RSS heap / all | rate | run |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: first push of the whole repository, HTTP (n=1) | znippy | 38.6 s | 38.6 s | 37.2 s | 1.9 s | 2.2 GiB / 4.6 GiB | 8.5 MB/s | oden, 2026-10-31, load 84.2 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: first push of the whole repository, HTTP (n=1) | gix | 26.6 s | 26.6 s | 30.2 s | 1.7 s | 1.8 GiB / 2.5 GiB | 12.3 MB/s | oden, 2026-10-31, load 50.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: first push of the whole repository, SSH (n=1) | znippy | 34.1 s | 34.1 s | 36.8 s | 1.6 s | 1.8 GiB / 3.9 GiB | 9.6 MB/s | oden, 2026-10-31, load 69.1 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: first push of the whole repository, SSH (n=1) | gix | 27.4 s | 27.4 s | 30.7 s | 1.6 s | 1.7 GiB / 2.3 GiB | 12.0 MB/s | oden, 2026-10-31, load 59.2 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: cold start: a restarted server's first clone (znippy: its index build, when the select takes the index), HTTP (n=1) | znippy | 50.5 s | 50.5 s | 19.8 s | 32.0 s | 242 MiB / 541 MiB | 6.2 MB/s | oden, 2026-10-31, load 90.2 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: cold start: a restarted server's first clone (znippy: its index build, when the select takes the index), HTTP (n=1) | gix | 39.0 s | 39.0 s | 13.2 s | 33.2 s | 251 MiB / 838 MiB | 8.0 MB/s | oden, 2026-10-31, load 65.4 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: cold start: a restarted server's first push of one commit and its fetch (znippy: the index build), HTTP (n=1) | znippy | 103 s | 103 s | 92.2 s | 200 ms | 809 MiB / 1.1 GiB | — | oden, 2026-10-31, load 82.8 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: cold start: a restarted server's first push of one commit and its fetch (znippy: the index build), HTTP (n=1) | gix | 55.1 s | 55.1 s | 47.9 s | 160 ms | 85 MiB / 410 MiB | — | oden, 2026-10-31, load 68.0 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: rebuild after a push: push one commit, fetch it (znippy: the index update; gix: a walk), HTTP (n=10) | znippy | 944 ms | 7.2 s | 1.2 s | 70.0 ms | 3.6 GiB / 5.7 GiB | — | oden, 2026-10-31, load 126.0 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: rebuild after a push: push one commit, fetch it (znippy: the index update; gix: a walk), HTTP (n=10) | gix | 47.8 s | 55.4 s | 47.1 s | 100 ms | 1.9 GiB / 2.5 GiB | — | oden, 2026-10-31, load 31.3 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: rebuild after a push: push one commit, fetch it (znippy: the index update; gix: a walk), SSH (n=10) | znippy | 4.5 s | 142 s | 13.0 s | 86.0 ms | 3.6 GiB / 5.8 GiB | — | oden, 2026-10-31, load 137.5 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | cost: rebuild after a push: push one commit, fetch it (znippy: the index update; gix: a walk), SSH (n=10) | gix | 49.6 s | 58.8 s | 47.7 s | 87.0 ms | 1.9 GiB / 2.8 GiB | — | oden, 2026-10-31, load 74.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | memory: memory of the idle server, just started (22 repositories) | znippy | — | — | — | — | 6 MiB / 26 MiB | — | oden, 2026-10-31, load 64.4 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | memory: memory of the idle server, just started (22 repositories) | gix | — | — | — | — | 4 MiB / 23 MiB | — | oden, 2026-10-31, load 75.5 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | memory: memory held after the run, not what one request needs: the handle cache (up to 256 open repositories, stores::DEFAULT_OPEN_REPOS, each with its redb cache) and the reach bitmaps (22 repositories) |
znippy | — | — | — | — | 932 MiB / 1.2 GiB | — | oden, 2026-10-31, load 84.6 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | memory: memory held after the run, not what one request needs: the handle cache (up to 256 open repositories, stores::DEFAULT_OPEN_REPOS, each with its redb cache) and the reach bitmaps (22 repositories) |
gix | — | — | — | — | 150 MiB / 481 MiB | — | oden, 2026-10-31, load 76.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | clone (bare, warm), HTTP (n=3) | znippy | 49.1 s | 51.2 s | 19.7 s | 32.7 s | 2.7 GiB / 5.1 GiB | 6.4 MB/s | oden, 2026-10-31, load 77.4 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | clone (bare, warm), HTTP (n=3) | gix | 33.3 s | 36.1 s | 13.5 s | 34.5 s | 2.0 GiB / 2.9 GiB | 9.4 MB/s | oden, 2026-10-31, load 48.5 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | clone (bare, warm), SSH (n=3) | znippy | 53.5 s | 55.1 s | 20.2 s | 32.6 s | 2.6 GiB / 5.0 GiB | 5.8 MB/s | oden, 2026-10-31, load 117.5 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | clone (bare, warm), SSH (n=3) | gix | 35.8 s | 41.9 s | 13.8 s | 34.8 s | 2.0 GiB / 2.9 GiB | 8.7 MB/s | oden, 2026-10-31, load 58.0 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | fetch with nothing new, HTTP (n=20) | znippy | 101 ms | 515 ms | 1.50 ms | 31.5 ms | 2.7 GiB / 5.1 GiB | — | oden, 2026-10-31, load 104.2 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | fetch with nothing new, HTTP (n=20) | gix | 97.7 ms | 241 ms | 16.0 ms | 31.0 ms | 1.9 GiB / 2.8 GiB | — | oden, 2026-10-31, load 51.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | fetch with nothing new, SSH (n=20) | znippy | 123 ms | 393 ms | 3.00 ms | 36.5 ms | 2.8 GiB / 5.2 GiB | — | oden, 2026-10-31, load 83.3 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | fetch with nothing new, SSH (n=20) | gix | 62.0 ms | 106 ms | 17.5 ms | 33.5 ms | 1.9 GiB / 2.8 GiB | — | oden, 2026-10-31, load 51.8 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | git ls-remote (process start included), HTTP (n=50) | znippy | 18.2 ms | 112 ms | 1.80 ms | 11.8 ms | 815 MiB / 1.1 GiB | — | oden, 2026-10-31, load 83.8 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | git ls-remote (process start included), HTTP (n=50) | gix | 39.4 ms | 98.0 ms | 16.0 ms | 11.8 ms | 31 MiB / 362 MiB | — | oden, 2026-10-31, load 70.3 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | git ls-remote (process start included), SSH (n=50) | znippy | 29.4 ms | 174 ms | 3.20 ms | 15.0 ms | 814 MiB / 1.1 GiB | — | oden, 2026-10-31, load 83.6 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | git ls-remote (process start included), SSH (n=50) | gix | 47.6 ms | 137 ms | 16.8 ms | 15.2 ms | 30 MiB / 361 MiB | — | oden, 2026-10-31, load 68.1 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | info/refs, protocol v0 (rate at c=16), HTTP (n=1000) | znippy | 1.45 ms | 13.1 ms | 1.46 ms | — | 901 MiB / 1.2 GiB | 1 027 req/s | oden, 2026-10-31, load 88.9 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | info/refs, protocol v0 (rate at c=16), HTTP (n=1000) | gix | 16.1 ms | 52.0 ms | 15.4 ms | — | 117 MiB / 448 MiB | 84 req/s | oden, 2026-10-31, load 84.8 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | info/refs, protocol v2 (rate at c=16), HTTP (n=1000) | znippy | 0.10 ms | 4.09 ms | 0.11 ms | — | 903 MiB / 1.2 GiB | 22 703 req/s | oden, 2026-10-31, load 90.5 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | info/refs, protocol v2 (rate at c=16), HTTP (n=1000) | gix | 0.11 ms | 5.04 ms | 0.12 ms | — | 118 MiB / 449 MiB | 14 675 req/s | oden, 2026-10-31, load 87.2 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | ls-refs (rate at c=16), HTTP (n=1000) | znippy | 1.61 ms | 6.67 ms | 1.49 ms | — | 906 MiB / 1.2 GiB | 1 020 req/s | oden, 2026-10-31, load 88.7 |
| git.git (411 105 objects, 1 006 refs, 318 MB pack) | ls-refs (rate at c=16), HTTP (n=1000) | gix | 18.1 ms | 88.6 ms | 15.6 ms | — | 118 MiB / 449 MiB | 71 req/s | oden, 2026-10-31, load 81.2 |
| linux (the Linux kernel's history) | pending: not run yet | — | — | — | — | — | — / — | — | — |
| big, synthetic (34 objects, 1 ref, 269 MB pack) | cost: first push of the whole repository, HTTP (n=3) | znippy | 8.8 s | 8.9 s | 1.8 s | 6.8 s | 1.2 GiB / 2.9 GiB | 30.5 MB/s | oden, 2026-10-31, load 57.2 |
| big, synthetic (34 objects, 1 ref, 269 MB pack) | cost: first push of the whole repository, HTTP (n=3) | gix | 8.6 s | 9.2 s | 4.0 s | 6.8 s | 1.5 GiB / 2.0 GiB | 31.1 MB/s | oden, 2026-10-31, load 55.7 |
| lfs, synthetic (83 objects, 1 ref, 0.34 MB pack) | LFS upload, HTTP (n=3) | znippy | 1.2 s | 1.4 s | 1.1 s | 463 ms | 929 MiB / 1.2 GiB | 235.0 MB/s | oden, 2026-10-31, load 88.7 |
| lfs, synthetic (83 objects, 1 ref, 0.34 MB pack) | LFS upload, HTTP (n=3) | gix | 1.5 s | 1.8 s | 1.1 s | 267 ms | 148 MiB / 478 MiB | 176.4 MB/s | oden, 2026-10-31, load 79.0 |
| lfs, synthetic (83 objects, 1 ref, 0.34 MB pack) | LFS download, HTTP (n=3) | znippy | 408 ms | 487 ms | 190 ms | 457 ms | 932 MiB / 1.2 GiB | 668.4 MB/s | oden, 2026-10-31, load 84.6 |
| lfs, synthetic (83 objects, 1 ref, 0.34 MB pack) | LFS download, HTTP (n=3) | gix | 723 ms | 862 ms | 187 ms | 467 ms | 150 MiB / 481 MiB | 377.0 MB/s | oden, 2026-10-31, load 76.9 |
| small, synthetic (121 objects, 1 ref, 0.05 MB pack) | cost: first push of the whole repository, HTTP (n=3) | znippy | 37.9 ms | 42.7 ms | 10.0 ms | 13.3 ms | 16 MiB / 43 MiB | 1.2 MB/s | oden, 2026-10-31, load 64.4 |
| small, synthetic (121 objects, 1 ref, 0.05 MB pack) | cost: first push of the whole repository, HTTP (n=3) | gix | 78.3 ms | 101 ms | 13.3 ms | 20.0 ms | 18 MiB / 42 MiB | 0.6 MB/s | oden, 2026-10-31, load 75.5 |
| small, synthetic (121 objects, 1 ref, 0.05 MB pack) | clone (bare, warm), HTTP (n=20) | znippy | 47.3 ms | 75.6 ms | 1.00 ms | 20.0 ms | 2.6 GiB / 4.9 GiB | 1.0 MB/s | oden, 2026-10-31, load 92.4 |
| small, synthetic (121 objects, 1 ref, 0.05 MB pack) | clone (bare, warm), HTTP (n=20) | gix | 42.8 ms | 148 ms | 2.00 ms | 20.0 ms | 1.8 GiB / 2.3 GiB | 1.1 MB/s | oden, 2026-10-31, load 61.2 |
| small, synthetic (121 objects, 1 ref, 0.05 MB pack) | info/refs, protocol v2 (rate at c=16), HTTP (n=1000) | znippy | 0.16 ms | 11.8 ms | 0.14 ms | — | 873 MiB / 1.2 GiB | 20 997 req/s | oden, 2026-10-31, load 85.8 |
| small, synthetic (121 objects, 1 ref, 0.05 MB pack) | info/refs, protocol v2 (rate at c=16), HTTP (n=1000) | gix | 0.19 ms | 3.57 ms | 0.12 ms | — | 85 MiB / 416 MiB | 15 101 req/s | oden, 2026-10-31, load 73.7 |
| wide, synthetic (11 263 objects, 1 ref, 7.0 MB pack) | clone (bare, warm), HTTP (n=5) | znippy | 316 ms | 443 ms | 20.0 ms | 244 ms | 2.6 GiB / 4.9 GiB | 22.2 MB/s | oden, 2026-10-31, load 93.5 |
| wide, synthetic (11 263 objects, 1 ref, 7.0 MB pack) | clone (bare, warm), HTTP (n=5) | gix | 342 ms | 438 ms | 86.0 ms | 238 ms | 1.8 GiB / 2.3 GiB | 20.5 MB/s | oden, 2026-10-31, load 61.3 |
| wide, synthetic (11 263 objects, 1 ref, 7.0 MB pack) | info/refs, protocol v2 (rate at c=16), HTTP (n=1000) | znippy | 0.099 ms | 3.08 ms | 0.11 ms | — | 882 MiB / 1.2 GiB | 12 134 req/s | oden, 2026-10-31, load 88.8 |
| wide, synthetic (11 263 objects, 1 ref, 7.0 MB pack) | info/refs, protocol v2 (rate at c=16), HTTP (n=1000) | gix | 0.090 ms | 2.34 ms | 0.10 ms | — | 102 MiB / 433 MiB | 14 343 req/s | oden, 2026-10-31, load 79.9 |
Source: znippy-plugin-git-store (crates/gunnar-perfbench/results/2026-10-31-oden-znippy-plugin-git-store.json), measured on oden on 2026-10-31, load1 69.8 to 71.7 during the run (70.2 before it), built from znippy-plugin-git 0.2.3 from crates.io. Store level, no server: plugins/native/znippy-plugin-git/examples/reach_real_repo.rs, one process; peak RSS is the process's VmHWM so far; CPU is the process's.
| repository | step | engine | p50 | p99 | server CPU/op | client CPU/op | peak RSS heap / all | rate | run |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | cost: ingest: the repository's pack stored and indexed (n=1) | znippy-plugin-git 0.2.3 | 11.5 s | — | 14.7 s | — | — / 1.2 GiB | — | oden, 2026-10-31, load 70.2 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | cost: index build: first select of every tip (once per repository, then persisted) (n=1) | znippy-plugin-git 0.2.3 | 15.6 s | — | 12.8 s | — | — / 1.4 GiB | — | oden, 2026-10-31, load 69.8 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | cost: rebuild after a push: the first select after it (n=1) | znippy-plugin-git 0.2.3 | 27.0 ms | — | 27.0 ms | — | — / 1.4 GiB | — | oden, 2026-10-31, load 71.4 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | cost: restart: reopen and the first fetch-shaped select (n=1) | znippy-plugin-git 0.2.3 | 274 ms | — | 256 ms | — | — / 1.4 GiB | — | oden, 2026-10-31, load 71.4 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | cost: push of one commit, the fold included (n=1) | znippy-plugin-git 0.2.3 | 341 ms | — | 247 ms | — | — / 1.4 GiB | — | oden, 2026-10-31, load 71.4 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | select of every tip, warm (the clone question) (n=3) | znippy-plugin-git 0.2.3 | 24.0 ms | — | 0.000 ms | — | — / 1.4 GiB | — | oden, 2026-10-31, load 71.7 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | fetch-shaped select (master against master^) (n=3) | znippy-plugin-git 0.2.3 | mean 164 ms | — | 158 ms | — | — / 1.4 GiB | — | oden, 2026-10-31, load 71.7 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | refs(), the advertisement (n=200) | znippy-plugin-git 0.2.3 | mean 0.80 ms | — | 0.80 ms | — | — / 1.2 GiB | — | oden, 2026-10-31, load 69.8 |
| git.git (411 105 objects, 1 007 refs, 292 MB pack) | cost: put every ref once (n=1) | znippy-plugin-git 0.2.3 | 3.00 ms | — | 3.00 ms | — | — / 1.2 GiB | — | oden, 2026-10-31, load 69.8 |
Thanks
- Its inventors: Vetra Automation AB and Ignalina ApS (Danskjävlarna).
- Ragnar Groot Koerkamp — the ridiculously fast static search tree.
- Christopher Berner — the neat redb.
Contributing, security and changes
- CONTRIBUTING.md: how to get the code, report a bug and propose a change.
- CLA.md: the Contributor License Agreement (version 1.1) that every outside contribution needs; accept it with one line in CLA-SIGNATURES.md.
- SECURITY.md: how to report a vulnerability privately (gunnar@gunnar.rs).
- docs/THREAT-MODEL.md: the threat model: what Gunnar protects, its trust boundaries, the attackers, the code that stops each attack, and the known gaps.
- CHANGELOG.md: what changed in each release, and the vulnerabilities each one fixed.
Sponsors
None so far — we would love to have one. Contact gunnar@gunnar.rs.
AI-generated details: .nornir/detail.md