# Performance

Performance is a standing design goal, not an afterthought. These are
`zig-nostr`'s own numbers, measured with the in-repo benchmark and reproducible
on your own machine. No competitor comparisons; just what the library does.

## Local store: query & ingest

Measured on an Apple Silicon Mac, `ReleaseFast`, warm cache, best of 50 runs.
Events are spread across 100 authors; the feed query is a 20-author, `kind:1`
home timeline returning 500 notes.

| Store size | Feed query (500 notes) | Timeline query (1 author) | Profile query | Ingest |
|----|----|----|----|----|
| 20,000 events | 0.25 ms | 0.09 ms | 0.007 ms | ~170k events/s |
| 100,000 events | 0.28 ms | 0.24 ms | 0.008 ms | ~149k events/s |

The headline isn't just that a feed query takes **~0.28 ms**: it's that going
from 20k to 100k stored events barely moves it. The profile query makes the same
point sharply: fetching one account's `kind:0` out of a hundred thousand events
reads a single index entry, so it costs **8 microseconds** and does not care how
much history sits behind it. That's the **bounded query
planner**: it walks the indexes newest-first and stops at `limit`, so latency
tracks the page size you ask for, not the size of the store.

### A long follow list

A client that follows hundreds of people queries for all of them at once, and the store merges one stream per author, newest first. The merge takes the newest candidate from a binary heap of the live streams rather than scanning all of them for every note it returns, so adding authors costs a sift of the heap and not another pass over every author. The order of the results is unchanged. This is a 60-note `kind:1` query with one stream per author, in microseconds, `ReleaseFast`, best of five runs, before and after the heap.

| Authors followed | Before | After |
|----|----|----|
| 20 | 46.6 | 46.0 |
| 128 | 114.5 | 93.2 |
| 512 | 357.8 | 253.9 |
| 2,048 | 1,430.1 | 1,006.4 |

What remains at large follow lists is mostly opening a cursor per author, which a heap does not change.

## Reproduce the library numbers

```sh
git clone https://github.com/zig-nostr/nostr
cd nostr
BENCH_N=100000 zig build bench -Doptimize=ReleaseFast
```

`BENCH_AUTHORS` sets how many authors the store is spread across, which is also how many streams the wide feed query merges.

The benchmark source is
[`src/bench.zig`](https://github.com/zig-nostr/nostr/blob/main/src/bench.zig).
Absolute numbers vary with hardware; the *shape*, flat feed latency as the
store grows, is the point.

## deed and nak

deed 0.5.0 beats nak v0.21.2 on every benchmark run here: faster or smaller on all 53 comparisons where both have the feature, with no ties. Times are the minimum of N runs, release builds of both, on an Apple M2 Pro over loopback.

| Benchmark | deed | nak | deed is |
|----|----|----|----|
| Startup (`--version`) | 1.9 ms | 121 ms | 64x faster |
| Memory at startup | 2.0 MB | 69.4 MB | 35x smaller |
| Binary size | 5.4 MB | 43.4 MB | 8x smaller |
| Verify 10,000 events | 32 ms | 1,967 ms | 62x faster |
| Sign 10,000 drafts | 32 ms | 1,414 ms | 44x faster |
| `req`, latest 500 notes | 15.9 ms | 226 ms | 14x faster |
| `req --paginate`, 49,500 notes | 1.34 s | 9.60 s | 7x faster |
| Publish 100 events to each of 4 relays | 46 ms | 280 ms | 6x faster |
| Gift wrap 1,000 messages | 123 ms | 601 ms | 4.9x faster |
| Gift unwrap 1,000 messages | 115 ms | 761 ms | 6.6x faster |
| NIP-77 sync, 50,000 events, 1% missing | 146 ms | 790 ms | 5.4x faster |
| Relay serving, publish 10,000 events | 1.40 s | 3.54 s | 2.5x faster |
| Relay serving, 32 clients each asking for the latest 500 of 10,000 | 16.33 ms | 74.92 ms | 4.6x faster |
| Relay serving, 200 requests in a row on one connection | 99.05 ms | 515 ms | 5.2x faster |
| Relay serving, 32 clients, 100,000 events stored | 16.90 ms | 68.67 ms | 4.1x faster |

Both tools ran the same work on the same fixtures, signed with throwaway keys. Fixtures and relays bind 127.0.0.1 only: nak's relay is `nak serve`, and deed was checked to produce the same output as nak for each row (event counts, cross-unwrapping of each other's gift wraps, cross-decrypting each other's payloads). Timing uses `hyperfine -N` with a warm-up, and the minimum is reported because noise on a shared machine only adds time. A comparison within 5 percent counts as a tie. Rows where nak lacks the feature, such as reading from a local store, are not counted. The full table, with every row and the method, is in deed's [`BENCHMARKS.md`](https://github.com/zig-nostr/deed/blob/main/BENCHMARKS.md). The relay serving rows compare `deed serve` with `nak serve` under the same client; `nak serve` caps a request at 500 events, so each query asks for 500, and the relay is put under load with 32 clients at once or 200 requests in a row, since a single query mostly measures the client.

## deed

What the [command line](/deed) costs to run, measured on an Apple M2 Pro with the deed 0.3.1 binary from the release page (`ReleaseSafe`, stripped). Each number is the best of several runs, and every one is the whole command, starting the process included. The macOS Apple Silicon size and the two 10,000-event stream rates are from deed 0.5.0; the other rows are from 0.3.1.

| Size | Binary | Download |
|----|----|----|
| macOS, Apple Silicon (0.5.0) | 5.4 MB | 2.8 MB |
| macOS, Intel | 2.4 MB | 1.7 MB |
| Linux, x86_64 | 2.7 MB | 1.8 MB |
| Linux, aarch64 | 2.4 MB | 1.8 MB |

| Command | |
|----|----|
| starting any process at all, for scale | 1.57 ms |
| `deed version` | 2.34 ms |
| `deed event`, signing one | 2.46 ms |
| `deed verify`, one | 3.05 ms |
| peak memory, a one-shot command | 1.7 MB |

| Streams of 10,000 through stdin | |
|----|----|
| signing (0.5.0) | 309,449 per second |
| verifying (0.5.0) | 312,868 per second |
| decoding npubs | 568,831 per second |
| peak memory, verifying | 3.9 MB |

| Local store, 100,000 events from 1,000 authors | |
|----|----|
| storing them from a loopback relay, each signature checked | 17,379 per second |
| one event by id | 3.25 ms |
| one profile | 3.21 ms |
| one author's latest 50 notes | 3.50 ms |
| the latest 500 notes | 5.03 ms |
| every note, 98,000 of them | 286 ms |

A lookup costs about a millisecond more than `deed version` does, starting deed and opening the store included. The store's own share is the fraction of a millisecond the library numbers at the top of this page show.

```sh
git clone https://github.com/zig-nostr/deed
cd deed
zig build -Doptimize=ReleaseSafe -Dstrip=true
python3 bench/run.py zig-out/bin/deed
```

It needs python3 and websocat, and talks only to a relay it runs on 127.0.0.1. The script is [`bench/run.py`](https://github.com/zig-nostr/deed/blob/main/bench/run.py), and [`BENCHMARKS.md`](https://github.com/zig-nostr/deed/blob/main/BENCHMARKS.md) has every number it prints.
