<!-- ⚠ GENERATED by nornir from .nornir/README.md — DO NOT EDIT this file -->

<!-- nornir:class: readme -->

# gunnar

[![OpenSSF Best Practices](https://www.gunnar.rs/api/repos/nordisk/ci-status/raw?path=badges/openssf-gunnar.svg)](https://www.bestpractices.dev/projects/16084)

<img src=".nornir/assets/gunnar.webp" alt="gunnar" width="320">

**Fast setup:** [the guide](.nornir/gunnar-quickstart.md) · [PDF](docs/gunnar-fast-setup.pdf) — four lists of commands, one picture per setup.

**Features:** [what is new, with status, source path and commit](docs/FEATURES.md).

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`](crates/gunnar/README.md).

## Gunnar by hand

<!-- gunnar-by-hand:begin -->

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.

```bash
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:

```bash
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.

```bash
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.

```bash
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.

```bash
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

```bash
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.

```bash
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):

```bash
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.

```bash
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](.nornir/gunnar-quickstart.md), §3. Stop the three programs: `kill %1 %2 %3`.

<!-- gunnar-by-hand:end -->

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/`](attestations/), the release key in
[`slsa/`](slsa/); how to verify (cosign, dwarves or Holger) and what still keeps us below
L3: [docs/SLSA.md](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.

<!-- bench-readme:begin: generated by `cargo xtask bench-readme write` from crates/gunnar-perfbench/results/; change the bench, never this block -->
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 |

<!-- bench-readme:end -->

## 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](CONTRIBUTING.md): how to get the code, report a bug and propose a change.
- [CLA.md](CLA.md): the Contributor License Agreement (version 1.1) that every outside contribution needs; accept it with one line in [CLA-SIGNATURES.md](CLA-SIGNATURES.md).
- [SECURITY.md](SECURITY.md): how to report a vulnerability privately (gunnar@gunnar.rs).
- [docs/THREAT-MODEL.md](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](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](.nornir/detail.md)
