The relay advertises max_limit: 10_000 in its NIP-11 document but enforces an effective websocket page limit of 1,000 — a 10x discrepancy. NIP-11 defines the field as "the relay server will clamp each filter's limit value to this number… with this number, you can know that there are additional results" (11.md:129), so a client that trusts it can silently lose data.
The three layers
| Layer |
Value |
Source |
NIP-11 advertised max_limit |
10,000 |
crates/buzz-relay/src/nip11.rs:106 |
| WS REQ clamp |
2,000 |
crates/buzz-relay/src/handlers/req.rs:25 (MAX_HISTORICAL_LIMIT), applied at :881-882 |
| DB clamp |
1,000 |
crates/buzz-db/src/event.rs:347 — let clamp = q.max_limit.unwrap_or(1000) |
EventQuery::max_limit is left None on the websocket REQ path (it is set only by the COUNT fallback, req.rs:755), so the DB default of 1,000 applies. Effective page limit is min(requested, 2000, 1000) = 1,000.
The HTTP bridge POST /query path is the same: it builds its query through crate::handlers::req::build_event_query_from_filter (crates/buzz-relay/src/api/bridge.rs:1215) and likewise leaves max_limit unset.
Why it matters
A paginating client that reads NIP-11 and asks for limit: 10000 receives 1,000 events. Seeing a page an order of magnitude shorter than the limit it requested and than the cap the relay advertised, it concludes the query is exhausted — and silently drops everything older. There is no error and no truncation signal.
This is the exact failure mode that NIP-MP's pagination contract (#3163) names as condition 2: "The relay exposes the exact effective page limit it enforces… If the advertised cap differs from the enforced one, 'shorter than the effective limit' is undecidable by the client." Writing that condition down is what surfaced this.
Suggested fix
Advertise what is enforced. Either set NIP-11 max_limit to the true effective value, or derive it from the same constant the enforcing paths use so the two cannot drift again. The second is preferable — a hardcoded 10_000 next to a MAX_HISTORICAL_LIMIT of 2_000 and a DB default of 1_000 is three sources of truth for one number.
Note the two enforced clamps are themselves redundant: the WS clamp of 2,000 can never bind, because the DB clamp of 1,000 is always tighter on that path.
Found while reviewing #3163 (NIP-MP). Not a blocker for that PR — its spec text is scoped correctly and does not claim Buzz satisfies the contract — but it is a live silent-data-loss path for any client that trusts the advertised limit.
The relay advertises
max_limit: 10_000in its NIP-11 document but enforces an effective websocket page limit of 1,000 — a 10x discrepancy. NIP-11 defines the field as "the relay server will clamp each filter'slimitvalue to this number… with this number, you can know that there are additional results" (11.md:129), so a client that trusts it can silently lose data.The three layers
max_limitcrates/buzz-relay/src/nip11.rs:106crates/buzz-relay/src/handlers/req.rs:25(MAX_HISTORICAL_LIMIT), applied at:881-882crates/buzz-db/src/event.rs:347—let clamp = q.max_limit.unwrap_or(1000)EventQuery::max_limitis leftNoneon the websocket REQ path (it is set only by the COUNT fallback,req.rs:755), so the DB default of 1,000 applies. Effective page limit ismin(requested, 2000, 1000)= 1,000.The HTTP bridge
POST /querypath is the same: it builds its query throughcrate::handlers::req::build_event_query_from_filter(crates/buzz-relay/src/api/bridge.rs:1215) and likewise leavesmax_limitunset.Why it matters
A paginating client that reads NIP-11 and asks for
limit: 10000receives 1,000 events. Seeing a page an order of magnitude shorter than the limit it requested and than the cap the relay advertised, it concludes the query is exhausted — and silently drops everything older. There is no error and no truncation signal.This is the exact failure mode that NIP-MP's pagination contract (#3163) names as condition 2: "The relay exposes the exact effective page limit it enforces… If the advertised cap differs from the enforced one, 'shorter than the effective limit' is undecidable by the client." Writing that condition down is what surfaced this.
Suggested fix
Advertise what is enforced. Either set NIP-11
max_limitto the true effective value, or derive it from the same constant the enforcing paths use so the two cannot drift again. The second is preferable — a hardcoded10_000next to aMAX_HISTORICAL_LIMITof2_000and a DB default of1_000is three sources of truth for one number.Note the two enforced clamps are themselves redundant: the WS clamp of 2,000 can never bind, because the DB clamp of 1,000 is always tighter on that path.
Found while reviewing #3163 (NIP-MP). Not a blocker for that PR — its spec text is scoped correctly and does not claim Buzz satisfies the contract — but it is a live silent-data-loss path for any client that trusts the advertised limit.