~/side $ ./fastr // goes_brrrrrrrr @ 148k ev/s
FASTR
a Nostr relay that goes brrrrrrrr, written in Odin
[01] WHAT IT IS
A relay has one job: move events. fastr does that job at 148 thousand events per second on an 8-core box, and makes every other relay reconsider its life choices.
It's a Nostr relay written
in Odin. Events live in
BASED, a binary append-only store built for this exact workload; queries read straight
off the mmap and stop the moment the answer is complete. Your relay is slow.
Your memory usage is embarrassing. You already knew this in your heart.
the supported NIPs:
EVENT, REQ, EOSE: the core publish/subscribe flow every client speaks.
NIP-09
event deletion
Deletion requests tombstone events; compaction reclaims the bytes later.
NIP-11
relay info
Limits and supported NIPs as a JSON document over HTTP.
NIP-17
private DMs
Kind-1059 gift wraps are only handed to their p-tagged recipient.
NIP-40
expiration
Expiry lives in the index record itself; expired events stop matching.
NIP-42
client auth
AUTH challenges for anything that needs a proven identity.
NIP-45
event counts
COUNT queries: the number without shipping the events.
NIP-62
request to vanish
A pubkey can ask to be erased; vanished.r makes sure it stays gone.
NIP-70
protected events
Events only their author may publish, enforced via NIP-42 auth.
NIP-77
negentropy sync
Set reconciliation, so mirrors fetch only what they're missing.
[02] BENCHMARKS
500k events, 8 CPU / 8 GB RAM, 50k queries, measured 2026-06-10. fastr vs strfry:
| metric | fastr | strfry | diff |
|---|---|---|---|
| ingest throughput (ev/s) | 148,251 | 2,378 | 62× |
| ingest OK p50 latency (µs) | 48 | 3,081 | 64× |
| ingest OK p99 latency (µs) | 80 | 6,801 | 85× |
| REQ query throughput (q/s) | 58,868 | 6,622 | 9× |
| REQ→EOSE p50 latency (µs) | 128 | 1,107 | 9× |
| REQ→EOSE p99 latency (µs) | 181 | 2,220 | 12× |
| peak RSS @ 500k events | 130 MB | 548 MB | 4× |
| disk usage @ 500k events | 119 MB | 526 MB | 4× |
| disk I/O written | 119 MB | 76,052 MB | 639× ← not a typo |
| CPU Mcycles (user) | 110,124 | 265,301 | 2× |
| CPU Mcycles (kernel) | 20,439 | 777,892 | 38× |
| syscalls | 3,761,077 | 76,390,183 | 20× |
| context switches | 1,105,063 | 17,609,468 | 16× |
| cold start (µs) | 13,121 | 23,607 | 2× |
strfry wrote 83 GB of journal just to ingest 500k immutable events.
fastr wrote 119 MB. The whole harness ships in the repo; reproduce it with
docker build -f Dockerfile.bench -t fastr-bench . && docker run --rm --privileged fastr-bench.
[03] ENGINEERING
BASED (Binary Append Store for Event Data): id, pubkey and sig stored as raw bytes, 64-char hex strings packed to 32. An event never becomes a struct; it transcodes to JSON wire format in one pass with zero heap allocations.
Index records are scanned newest-first with cheapest-first filter gates, fields read straight from the mmap. A prefix-max of created_at stops the scan once nothing older can crack the top-N, and exact-id lookups skip the scan entirely.
Append the blob, append a 92-byte index record. No B-tree to rebalance, no journal to replay. That's how 639× less disk I/O happens.
Every matching event is encoded into a single batch with EOSE appended, handed to the write task as one message. N events, one syscall.
Live events try_send to subscribers. A slow client drops events; it never stalls the writer or the rest of the relay.
Every 6 hours, once tombstones pass 1,000, a background task rebuilds the files. Deletes, expirations and vanish requests cost nothing until then.
[04] RUN IT
# one-time: clone + build static libsecp256k1 $ just vendor # build -> ./fastr $ just build # serve on 0.0.0.0:8080 $ ./fastr # bulk import; bare events or ["EVENT", {...}] # envelopes, because the world is chaotic $ ./fastr import ./data events.jsonl # deploy: auto-detects systemd, OpenRC, runit, # FreeBSD rc.d or launchd $ sudo ./deploy/install.sh
configuration: env vars
| FASTR_PORT | 8080 | listen port |
| FASTR_ADDR | 0.0.0.0 | bind address |
| FASTR_DATA_DIR | ./data | data directory |
| FASTR_MAX_CONNECTIONS | 1024 | max websocket conns |
| FASTR_MAX_SUBSCRIPTIONS | 20 | subs per connection |
| FASTR_MAX_FILTERS | 10 | filters per sub |
| FASTR_MAX_LIMIT | 500 | max events per REQ |
| FASTR_MAX_MESSAGE_BYTES | 131072 | max message size |
| FASTR_COMPACT_INTERVAL | 21600 | compaction interval (s) |
[05] SOURCE
one package per directory
pack/the BASED format: encode, decode, JSON transcodenostr/event validation, filtersstore/storage engine: blobs, index, tags, compactionws/WebSocket server + message handlersnegentropy/NIP-77 set reconciliationsecp256k1/bindings to the vendored static libcmd/config, NIP-11 endpoint, the binarybench/ qbench/ smoke/load generators + end-to-end checks
honest footnote
strfry is respectable, battle-tested software with years of production scars. If you want safe and proven, run strfry. If you want speed, run fastr. This is young alpha software, expect bugs; the "three years on the biggest relays" achievement is still loading. But the numbers are real. Beat them by 25% with the same features and the project gets renamed to slowstr. Not even joking. AGPLv3.
READ THE CODE ⇗