Skip to content

relay: NIP-11 advertises max_limit 10,000 but enforces 1,000 — silent truncation for clients that trust it #3757

Closed

Description

@tlongwell-block

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.

Activity

  1. dpetrakov commented on Jul 30, 2026

    @dpetrakov

    Heads-up: #3635 (fix(relay): align NIP-11 max_limit with REQ ceiling, open since 2026-07-29) already implements this – it landed in the queue about 17 hours before this issue was filed, which is probably why neither of you found the other.

    It does what the "Suggested fix" section here asks for: it introduces buzz_db::DEFAULT_MAX_PAGE_LIMIT and derives NIP-11 limitation.max_limit from it, so the advertised and enforced caps can't drift. It also covers the two things this issue notes in passing – the redundant MAX_HISTORICAL_LIMIT path in handlers/req.rs, and the POST /query bridge.

    It doesn't carry a Fixes #3757 line, so this issue won't auto-close when that PR merges.

    For what it's worth, I reproduced the discrepancy independently on main @ bf139e8 before finding that PR – the three values are still nip11.rs:106 = 10_000, handlers/req.rs:25 = 2_000, and buzz-db/src/event.rs:358 = 1_000 – so the report holds and the fix in #3635 looks like the right shape.

  2. added a commit that references this issue on Jul 30, 2026
    23f0c26
  3. added a commit that references this issue on Aug 3, 2026
    2ef8926
  4. added a commit that references this issue on Aug 23, 2026
    deec496
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions