Counting
Relays that support NIP-45 can count events matching a filter without sending them. Relay.count() sends a single COUNT request, and RelayPool.count() sends it to several relays and reports each relay's result separately.
Counting on one relay
count() returns an Observable that emits one validated response and completes. Refusals, malformed responses and timeouts arrive on the error channel.
import { lastValueFrom } from "rxjs";
const { count, approximate } = await lastValueFrom(relay.count({ kinds: [1], authors: [pubkey] }));
console.log(`Found ${count} events`, approximate ? "(approximate)" : "");The second argument is an optional request ID and the third takes options:
relay
.count({ kinds: [7], "#e": [id] }, "reactions", { retries: 2, timeout: 5_000, onAuthRequired })
.subscribe({ next: (res) => console.log(res.count), error: console.error });onSentruns immediately before each COUNT frame is written, including authentication resends; use it to distinguish wire response latency from readiness and authentication waiting.timeoutis one deadline for the whole request, including retries (default 10s, or the relay'scountTimeout). It pauses while authenticating. Passfalseto disable it.retriesretries connection failures and takes precedence over itsreconnectalias.onAuthRequired,waitForAuthandauthTimeoutwork like other requests. See Authentication.
Validated responses
Every response has a non-negative integer count. The optional approximate flag is a boolean and the optional hll is a 512-character hex sketch, normalized to lowercase. Unknown fields are kept.
An invalid reply errors with RelayCountResponseError, and an expired deadline errors with RelayCountTimeoutError.
Counting across relays
pool.count() emits a cumulative record of RelayOutcome values keyed by normalized relay URL each time a relay settles. Relays missing from a snapshot are still pending, and one relay's failure never affects the others.
pool.count(relays, { kinds: [1], authors: [pubkey] }).subscribe((outcomes) => {
for (const [url, outcome] of Object.entries(outcomes))
console.log(url, outcome.ok ? outcome.value.count : outcome.error);
});RelayGroup.count() behaves the same, and both accept the same ID and options as Relay.count().
Merging counts with HyperLogLog
Relays overlap, so summing their counts double-counts events. When relays return hll sketches, merge them with mergeHllRegisters() and estimate the size of the union with estimateHllCardinality().
import { estimateHllCardinality, mergeHllRegisters } from "applesauce-relay";
pool.count(relays, filter).subscribe((outcomes) => {
const sketches = Object.values(outcomes).flatMap((o) => (o.ok && o.value.hll ? [o.value.hll] : []));
if (sketches.length) console.log("Estimated total:", estimateHllCardinality(mergeHllRegisters(sketches)));
});Failed relays and responses without a sketch can't contribute, so the estimate only covers the relays that returned one.
Integration
To load per-event metrics that components share, cache and refresh, use the count service. It runs NIP-45 COUNT requests through a pool and can balance requests across trusted relays.
Best Practices
- Never sum counts from overlapping relays. Merge HLL sketches instead, and don't treat a missing sketch as zero.
- Treat pool snapshots as provisional until the Observable completes.
- Always handle the error channel; relays without NIP-45 support refuse or ignore
COUNT. - Pass a stable request ID when you need to correlate logs.