Navigation: Docs index · Getting started · API how-to · Custom events · API reference
This guide explains how to open and manage long-lived, non-blocking subscriptions to Nostr relays using NostrRelayClient.
- Use
NostrRelayClient.subscribe()to open a REQ subscription that streams relay messages to your callback. - The method returns immediately with an
AutoCloseable. Callingclose()sends aCLOSEto the relay and frees the underlying WebSocket resource. - Callbacks are dispatched on Virtual Threads, so expensive listener logic does not block inbound WebSocket I/O.
import nostr.base.Kinds;
import nostr.client.springwebsocket.NostrRelayClient;
import nostr.event.filter.EventFilter;
import nostr.event.filter.Filters;
import nostr.event.message.ReqMessage;
import java.util.List;
// Build a filter
EventFilter filter = EventFilter.builder()
.kinds(List.of(Kinds.TEXT_NOTE))
.limit(100)
.build();
Filters filters = new Filters(filter);
String subscriptionId = "my-sub-" + System.currentTimeMillis();
ReqMessage req = new ReqMessage(subscriptionId, filters);
// Open subscription
try (NostrRelayClient client = new NostrRelayClient("wss://relay.398ja.xyz")) {
AutoCloseable subscription = client.subscribe(
req,
message -> {
// Handle EVENT/EOSE/NOTICE payloads here
System.out.println("Received: " + message);
},
error -> {
System.err.println("Error: " + error.getMessage());
},
() -> {
System.out.println("Connection closed");
}
);
// Keep the subscription open while processing events
Thread.sleep(30_000);
subscription.close(); // sends CLOSE and releases resources
}NostrRelayClient.connectAsync("wss://relay.398ja.xyz")
.thenCompose(client -> client.subscribeAsync(
req.encode(),
message -> System.out.println("Event: " + message),
error -> System.err.println("Error: " + error),
() -> System.out.println("Closed")
))
.thenAccept(subscription -> {
// subscription is AutoCloseable — close when done
});- Each
subscribe()call opens a dedicated WebSocket connection. Keep the returned handle while you need the stream and callclose()when done. - Always close subscriptions to ensure a
CLOSEframe is sent to the relay and resources are freed. - After
close(), no further messages will be delivered to your listener.
- Message callbacks execute on Virtual Threads; processing can block safely, but bounded queues are still recommended when downstream systems are slower than relay throughput.
- For high-throughput feeds, consider batching or asynchronous processing to prevent socket stalls.
- The client limits accumulated events per blocking request to 10,000 by default (configurable) to prevent unbounded memory growth.
// Filter by multiple kinds and authors
EventFilter filter = EventFilter.builder()
.kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION))
.authors(List.of(pubKeyHex1, pubKeyHex2))
.since(System.currentTimeMillis() / 1000 - 86400) // last 24 hours
.limit(200)
.build();
// Filter by tag values
EventFilter tagFilter = EventFilter.builder()
.kinds(List.of(Kinds.TEXT_NOTE))
.addTagFilter("t", List.of("nostr", "bitcoin"))
.build();- Provide an
errorListenerto capture exceptions raised during subscription or message handling. - If the relay times out on a blocking send,
NostrRelayClientthrowsRelayTimeoutException(not an empty list). - Consider transient vs. fatal errors. The client uses Spring Retry with exponential backoff for transient I/O failures.
- Client:
nostr-java-client/src/main/java/nostr/client/springwebsocket/NostrRelayClient.java - Filters:
nostr-java-event/src/main/java/nostr/event/filter/EventFilter.java
For method signatures and additional details, see the API reference.