Building Fast Nostr Clients
Speed matters. Users won't wait for your clever architecture to finish loading their feed. They'll just leave.
The good news? You don't need exotic solutions to build a fast Nostr client. Simple approaches win.
The Core Insight
Here's the trick: start a short timeout counter after any relay in the group returns an EOSE.
That's it. That's the whole optimization.
When you query multiple relays simultaneously, you don't need to wait for all of them to finish. The moment one relay says "end of stored events" (EOSE), start a timer. Maybe 300ms, maybe 500ms—whatever feels responsive without being trigger-happy. When that timer expires, you're done. Close the subscription, render the results.
Why This Works
Nostr's decentralized nature means events are distributed across relays. You could wait forever for the slowest relay to respond. But in practice:
- Most relays that have your data will return it quickly
- The first relay to EOSE probably has what you need
- A short grace period catches stragglers without blocking indefinitely
- Users see content appear fast, which feels fast
The simple timeout approach beats complex orchestration every time.
The Alternative: Overthinking It
You could build elaborate systems:
- Relay reputation scoring
- Predictive caching layers
- Complex filter analysis
- Multi-stage queries with fallbacks
Some of these have their place (Alex has written about intrinsic limits of Nostr filters before). But for raw speed? A simple timeout after the first EOSE delivers 80% of the benefit with 5% of the complexity.
Simple Solutions Win
The best optimization is the one you actually ship. A clever algorithm that takes weeks to implement loses to a dumb timer you can add in 20 minutes.
Users don't care about your architecture. They care that the app feels fast. And "fast" means showing something quickly, not perfectly optimizing to show everything eventually.
Start the timer. Ship it. Move on to the next problem.
This post was inspired by a quick note on building Nostr clients that actually work.
Published at
2026-01-29 00:20:08 UTCEvent JSON
{
"id": "3cf50cc6ec49f46fc85b176ba9aed2b21b873b5ff98913d9fe3dd6f3697929f4",
"pubkey": "c839bc85846f24fc6b777548fe654672377f4cc2a04cab19cddec75b2f8b4dbd",
"created_at": 1769646008,
"kind": 30023,
"tags": [
[
"d",
"building-fast-nostr-clients"
],
[
"title",
"Building Fast Nostr Clients"
],
[
"summary",
"The simple trick to building fast Nostr clients: start a timeout after the first relay returns EOSE. Simple solutions win."
],
[
"published_at",
"1769646007"
],
[
"t",
"nostr"
],
[
"t",
"development"
],
[
"t",
"performance"
]
],
"content": "# Building Fast Nostr Clients\n\nSpeed matters. Users won't wait for your clever architecture to finish loading their feed. They'll just leave.\n\nThe good news? You don't need exotic solutions to build a fast Nostr client. Simple approaches win.\n\n## The Core Insight\n\nHere's the trick: **start a short timeout counter after any relay in the group returns an EOSE.**\n\nThat's it. That's the whole optimization.\n\nWhen you query multiple relays simultaneously, you don't need to wait for all of them to finish. The moment one relay says \"end of stored events\" (EOSE), start a timer. Maybe 300ms, maybe 500ms—whatever feels responsive without being trigger-happy. When that timer expires, you're done. Close the subscription, render the results.\n\n## Why This Works\n\nNostr's decentralized nature means events are distributed across relays. You could wait forever for the slowest relay to respond. But in practice:\n\n- Most relays that have your data will return it quickly\n- The first relay to EOSE probably has what you need\n- A short grace period catches stragglers without blocking indefinitely\n- Users see content appear fast, which *feels* fast\n\nThe simple timeout approach beats complex orchestration every time.\n\n## The Alternative: Overthinking It\n\nYou could build elaborate systems:\n- Relay reputation scoring\n- Predictive caching layers \n- Complex filter analysis\n- Multi-stage queries with fallbacks\n\nSome of these have their place (Alex has written about [intrinsic limits of Nostr filters](https://habla.news/a/naddr1qvzqqqr4gupzqprpljlvcnpnw3pejvkkhrc3y6wvmd7vjuad0fg2ud3dky66gaxaqqfkummnw3ez6enfd36x2unn94kxjmtfwsqrkey5) before). But for raw speed? A simple timeout after the first EOSE delivers 80% of the benefit with 5% of the complexity.\n\n## Simple Solutions Win\n\nThe best optimization is the one you actually ship. A clever algorithm that takes weeks to implement loses to a dumb timer you can add in 20 minutes.\n\nUsers don't care about your architecture. They care that the app feels fast. And \"fast\" means showing *something* quickly, not perfectly optimizing to show *everything* eventually.\n\nStart the timer. Ship it. Move on to the next problem.\n\n---\n\n*This post was inspired by a [quick note](https://gleasonator.dev/notice/AmfQfp4Cl87fmXrJlS) on building Nostr clients that actually work.*",
"sig": "2572f0db33330abea0d7324882f3b742849cbb8b6b762c464741fec1d45d7883229beda1478a18aaacadb489c7ef0b1363ced584e5537a780232b4ffd90aa6fd"
}