1 Overview
Some numbers are expensive to compute on the client. To show a follower count, a client must download every kind-3 follow list that references a pubkey. Then it counts them and throws them away. That wastes bandwidth on both sides.
Clients can cache the result. A separate indexing server can also cache it. Both options erode the decentralization of the network. They create a second-layer protocol on top of Nostr.
This NIP moves the counting to the relay. A relay may support the verb COUNT. The client sends a query id and one or more NIP-01 filters. The relay answers with a single integer. An optional HyperLogLog extension lets clients merge counts from many relays into one estimate.
COUNT is optional relay hardware. Probe before you rely on it. If a relay does not implement the verb, expect a NOTICE or silence, and fall back to counting raw events yourself.
CLOSED.2 The COUNT Verb
The client sends a COUNT message. It carries a query id and one or more filters. The filters follow the same rules as filters for the NIP-01 verb REQ. Multiple filters are OR'd together. The relay aggregates all matches into a single count result.
["COUNT", <query_id>, <filters JSON>...]
The relay answers with a COUNT response on the same query id. The payload is a JSON object with a count key. A relay may use probabilistic counting to reduce compute cost. If it does, it may set the approximate key in the response.
["COUNT", <query_id>, {"count": <integer>}] -- with optional flags -- ["COUNT", <query_id>, {"count": <integer>, "approximate": <true|false>}] ["COUNT", <query_id>, {"count": <integer>, "hll": "<hex>"}]
| Key | Type | Rule |
|---|---|---|
| count | integer | The number of events that match the OR'd filters. Required. |
| approximate | boolean | Optional. May be set when the relay used probabilistic counting. |
| hll | hex string, 512 chars | Optional. HyperLogLog registers for this count. See §3. |
A relay can refuse to fulfill a COUNT request. Whenever it refuses, it must return a CLOSED message for that query id. The CLOSED shape and its status prefixes come from NIP-01. A relay that requires login first uses the auth-required prefix from NIP-42.
The source spec is inconsistent about the second element's name. The general sections call it <query_id>; the HyperLogLog sections and two examples still call it <subscription_id>. A 2025 change renamed it to "query id" because COUNT is not a subscription, but the rename missed the HLL sections. Both names denote the same field.
3 HyperLogLog
A count from one relay is a local truth. Users read from many relays, and the same reaction can sit on several of them. Plain integers from different relays cannot be merged without double counting.
HyperLogLog (HLL) fixes this. It is a probabilistic sketch: a small fixed-size register array that estimates the number of distinct items. A relay may return an HLL value together with the count, hex-encoded, in the hll key. Clients merge the HLL values from multiple relays and get one reasonable estimate for reaction counts, comment counts, and follower counts. This saves many millions of bytes of bandwidth for everybody.
Relay-side algorithm
This is the only part needed for interoperability. Follow these steps to build the HLL value for a filter:
- Receive the filter. Check that it is eligible for HLL (see the offset rules below). Compute the deterministic
offsetfor the filter. - Initialize 256 registers to
0. - For every event that the filter counts, do this:
- Read the byte at position
offsetof the eventpubkey. That byte value is the register indexri. - Count the leading zero bits starting at position
offset+1of the eventpubkey. Add1. - Compare the result with the value stored at register
ri. If the new number is bigger, store it.
- Read the byte at position
On the client side, merging is simple. Walk through all 256 registers of each HLL value from each relay. Keep the highest value for each register, regardless of the relay. Then estimate the absolute count from the merged registers.
Computing the offset
The offset used above is derived deterministically from the filter that the client sent. Compute it like this:
- Take the first tag attribute in the filter. Tag attributes carry the
#prefix, for example#p. - From that attribute, take its first item. It is a string.
- Obtain a 32-byte hex string from it:
- If the string is an event id or a pubkey in hex, use it as it is.
- If the string is an address (
<kind>:<pubkey>:<d-tag>), use the<pubkey>part. - If the string is anything else, hash it with
sha256(). Take the result as a hex string.
- From that 64-character hex string, take the character at position
32. - Read that character as a base-16 number.
- Add
8to it. The result is theoffset.
For cases not covered above, such as filters without any tag attribute, behavior is not yet defined. This NIP may be modified later to specify those cases. For now no use case justifies HLL there.
Rationale. The offset must be deterministic. That is the only way a relay can cache HLL values instead of counting thousands of database rows on every query. It also lets a relay precompute HLL values for any target <id> or <pubkey> without storing the events themselves. That is handy for reactions, for example.
Common filters
Some relays cache or precompute HLL values only for common canonical queries. They may refuse to count anything else. Each query below uses exactly one tag attribute with a single item, so implementors do not need to check attribute order. This list can grow later if more common useful queries appear:
| Counts | Filter | Kind source |
|---|---|---|
| reactions | {"#e": ["<id>"], "kinds": [7]} | NIP-25 |
| reposts | {"#e": ["<id>"], "kinds": [6]} | NIP-18 |
| quotes | {"#q": ["<id>"], "kinds": [1, 1111]} | NIP-10, NIP-22 |
| replies | {"#e": ["<id>"], "kinds": [1]} | NIP-10 |
| comments | {"#E": ["<id>"], "kinds": [1111]} | NIP-22 |
| followers | {"#p": ["<pubkey>"], "kinds": [3]} | NIP-02 |
The hll encoding
The hll value is the concatenation of the 256 registers. Each register is one uint8 byte. Therefore hll is always a 512-character hex string.
Attack vector: an attacker can mine a pubkey with many zero bits exactly where the HLL algorithm looks. One reaction from that pubkey then "counts more" than others. The attacker needs a fresh mined pubkey per target, which is close to plain sybil spam anyway. The fix is the same for both attacks: do not fetch counts from open relays that accept everything. Base counts on relays that filter, so mostly real humans publish there, not bots or artificially generated pubkeys.
Client-side usage
Clients can combine HLL responses from relays with HLL sketches computed locally from raw events. Keep a local HLL per counter. Add every incoming source to it. An example flow for tracking reactions to an event Z over time:
- The client reads reactions from relays A, B, and C — the NIP-65 "read" relays of Z's author.
- Only B and C support HLL responses. The client fetches both and merges them locally.
- The client fetches all raw reaction events from A. It applies each event to the merged HLL with the same relay-side algorithm.
- The client reads the estimated count from the HLL and displays it to the user.
- Optionally, the client stores the HLL value together with a last-read date for relay A. On the next refresh it fetches only the new reactions from A, applies them, redownloads the HLL values from B and C, and reapplies those.
This procedure lets the client download much less data.
The estimation step is deliberately unspecified. The source spec points to implementation code instead of a formula and allows different estimators "with different quirks". Two conforming clients can therefore show slightly different numbers from identical registers. The zero-bit count in step 3.2 also has no stated cap, though 256 registers with uint8 values implies one.
4 Worked Examples
The session below shows every response shape. Orange lines go to the relay. Blue lines come from the relay. The 512-character hll strings are shortened here; a real one is never truncated.
-- count notes and reactions by one author -- → ["COUNT", "q1", {"kinds": [1, 7], "authors": ["22a12a12…"]}] ← ["COUNT", "q1", {"count": 5}] -- count all notes, relay answers probabilistically -- → ["COUNT", "q2", {"kinds": [1]}] ← ["COUNT", "q2", {"count": 93412452, "approximate": true}] -- follower count with HyperLogLog -- → ["COUNT", "q3", {"kinds": [3], "#p": ["f7234bd4…"]}] ← ["COUNT", "q3", {"count": 16578, "hll": "0607070505060806…0405070708"}] -- reaction count with HyperLogLog -- → ["COUNT", "q4", {"kinds": [7], "#e": ["5c83da77…"]}] ← ["COUNT", "q4", {"count": 2044, "hll": "01ef070505060806…0405070708"}] -- relay refuses to count gift wraps -- → ["COUNT", "q5", {"kinds": [1059], "#p": ["f7234bd4…"]}] ← ["CLOSED", "q5", "auth-required: cannot count other people's DMs"]
In the last exchange the relay protects kind-1059 gift wraps (NIP-59). It demands NIP-42 authentication before it counts private messages.
5 Version History
NIP-45 has lived through a deprecation and a resurrection. Notable changes, newest first:
- 20262026-02Added the HyperLogLog extension:
hllresponses, the register algorithm, the offset formula, canonical queries, and the mined-pubkey attack note. - 20252025-10Renamed the subscription id to a query id. COUNT is a one-shot query, not a subscription.
- 20242024-07Restored. The earlier removal was reverted and COUNT returned to the spec.
- 20232023-12Refusals standardized: a relay that declines a COUNT must reply with
CLOSED. - 20232023-10Deprecated and slated for removal, before the 2024 revert brought it back.
- 20232023-08Added the
approximatekey so relays can flag probabilistic counts. - 20232023-05Clarified multi-filter behavior: filters are OR'd and aggregated into one count.
- 20232023-04Counting by arbitrary filters spelled out; grammar and formatting fixes.
- Origin2023-01Jonathan Staab added NIP-45, defining the COUNT verb.
Original Proposal - Credits
| Author | Jonathan Staab (hodlbod) |
| First commit | 2023-01-03 — “Add NIP-45, which defines a COUNT verb” |
| Status | draft - optional - relay feature |
| Source | 45.md on GitHub - full change history |
6 See Also
- NIP-01 — defines the filters that COUNT reuses, and the
CLOSEDmessage for refusals. - NIP-42 — client authentication; source of the
auth-requiredrefusal prefix. - NIP-02 — kind-3 follow lists, the raw material for follower counts.
- NIP-25 — kind-7 reactions, the most counted event kind.
- NIP-18 — kind-6 reposts and quote conventions.
- NIP-22 — kind-1111 comments counted by the comment and quote queries.
- NIP-65 — relay lists that tell a client where to count from.