Collecting events
fetch_events runs the same kind of finite request as stream_events, but buffers matching events and returns a
deduplicated collection only after the request terminates. It is convenient for a small result set that must be sorted,
compared, or passed to an API expecting a collection.
Prefer streaming when events can be processed independently, the response size is uncertain, or latency to the first result matters.
Choose between the two finite request shapes based on the consumer:
| Requirement | Prefer |
|---|---|
| Process each event independently | stream_events |
| Start work before all relays reach EOSE | stream_events |
| Keep memory bounded by downstream capacity | stream_events |
| Sort or compare the complete small result set | fetch_events |
| Pass a collection to an existing API | fetch_events |
fetch_events is not more authoritative than a stream. It uses the same relay request and termination model, then
materializes the successful events for convenience. The tradeoff is that the caller receives no value until the
operation ends and does not retain per-item relay provenance.
use std::time::Duration;
use nostr_sdk::prelude::*;
async fn fetch_events() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::default();
client.add_relay("wss://relay.damus.io").await?;
client.connect().await;
let filter = Filter::new().kind(Kind::TextNote).limit(20);
let events = client
.fetch_events(filter)
.timeout(Duration::from_secs(10))
.await?;
for event in events {
println!("{}", event.as_json());
}
Ok(())
}
import asyncio
from datetime import timedelta
from nostr_sdk import Client, Filter, Kind, KindStandard, RelayUrl, ReqTarget
async def fetch_events() -> None:
client = Client()
await client.add_relay(RelayUrl.parse("wss://relay.damus.io"))
await client.connect()
filter = Filter().kind(Kind.from_std(KindStandard.TEXT_NOTE)).limit(20)
events = await client.fetch_events(
ReqTarget.auto([filter]), timeout=timedelta(seconds=10)
)
for event in events:
print(event.as_json())
import {
Client,
Filter,
Kind,
KindStandard,
RelayUrl,
ReqTarget,
} from "@nostrdevkit/nostr-sdk-node";
async function fetchEvents() {
const client = new Client();
await client.addRelay(RelayUrl.parse("wss://relay.damus.io"));
await client.connect();
const filter = new Filter()
.kind(Kind.fromStd(KindStandard.TextNote))
.limit(20n);
const events = await client.fetchEvents(ReqTarget.auto([filter]), 10_000);
for (const event of events) {
console.log(event.asJson());
}
}
import {
Client,
Filter,
Kind,
KindStandard,
RelayUrl,
ReqTarget,
uniffiInitAsync,
} from "@nostrdevkit/nostr-sdk-web";
await uniffiInitAsync();
async function fetchEvents() {
const client = new Client();
await client.addRelay(RelayUrl.parse("wss://relay.damus.io"));
await client.connect();
const filter = new Filter()
.kind(Kind.fromStd(KindStandard.TextNote))
.limit(20n);
const events = await client.fetchEvents(ReqTarget.auto([filter]), 10_000);
for (const event of events) {
console.log(event.asJson());
}
}
import {
Client,
Filter,
Kind,
KindStandard,
RelayUrl,
ReqTarget,
} from "@nostrdevkit/nostr-sdk-react-native";
export async function fetchEvents() {
const client = new Client();
await client.addRelay(RelayUrl.parse("wss://relay.damus.io"));
await client.connect();
const filter = new Filter()
.kind(Kind.fromStd(KindStandard.TextNote))
.limit(20n);
const events = await client.fetchEvents(ReqTarget.auto([filter]), 10_000);
for (const event of events) {
console.log(event.asJson());
}
}
import java.time.Duration
import kotlinx.coroutines.runBlocking
import org.nostrdevkit.sdk.*
suspend fun fetchEvents() {
val client = Client()
client.addRelay(RelayUrl.parse("wss://relay.damus.io"))
client.connect()
val filter = Filter()
.kind(Kind.fromStd(KindStandard.TEXT_NOTE))
.limit(20u)
val events = client.fetchEvents(
ReqTarget.auto(listOf(filter)),
timeout = Duration.ofSeconds(10),
)
for (event in events) {
println(event.asJson())
}
}
import Foundation
import NostrSDK
func fetchEvents() async throws {
let client = Client()
let relay = try RelayUrl.parse(url: "wss://relay.damus.io")
_ = try await client.addRelay(url: relay)
await client.connect()
let filter = Filter()
.kind(kind: Kind.fromStd(e: .textNote))
.limit(limit: 20)
let events = try await client.fetchEvents(
target: ReqTarget.auto(filters: [filter]),
timeout: 10.0
)
for event in events {
print(try event.asJson())
}
}
using Nostr.Sdk;
public static class ReadExample
{
public static async Task FetchEvents()
{
var client = new Client();
await client.AddRelay(RelayUrl.Parse("wss://relay.damus.io"));
await client.Connect();
var filter = new Filter()
.Kind(Kind.FromStd(KindStandard.TextNote))
.Limit(20);
var events = await client.FetchEvents(
ReqTarget.Auto([filter]),
timeout: TimeSpan.FromSeconds(10)
);
foreach (var @event in events)
{
Console.WriteLine(@event.AsJson());
}
}
}
The filter and timeout are the same as in the stream example. Only the consuming shape changes: this call waits for termination and returns a collection.
Bound the collection
The filter limit is applied by each relay, not once to the merged collection. Use a selective filter, a timeout, and a global event cap when a strict collection bound is required.
The SDK validates and deduplicates returned events by ID. Replaceable events and deletions still follow database and NIP semantics rather than simple ID deduplication.
Fetch errors that prevent target resolution or request creation fail the operation. Relay-specific behavior can still produce a valid but incomplete collection. If the feature needs to know which relay supplied or failed to supply each result, use the stream form, where relay provenance and per-item errors remain visible.
Use fetch when the complete, bounded collection is the desired output. Use stream when provenance or early processing must remain visible.
Both finite forms end on an exit policy. The next chapter covers a request intended to stay open: a live subscription.