Files
ngit-grasp/docs/reference/relay-limits.md
T
DanConwayDev a6b5e3a9f6 fix(relay): preserve bursty client sessions
rust-nostr 0.45 added a connection-wide 300-frame-per-minute bucket and
closes a WebSocket when it is exhausted. Production recorded 5,020 such
disconnects before this change; Caddy correlation showed both a rapid source
sending more than 300 frames in roughly 1.5 seconds and legitimate
gitworkshop.dev, gittr.space, armada.buzz, and localhost browser sessions
crossing the same ceiling. This predates and is independent of the
subscription-budget ledger.

Select a fixed 6,000-message-per-minute allowance for ngit-grasp. An initial
1,200/minute production candidate reduced closures to one in 29 minutes, but
that remaining localhost development client legitimately sustained about
44-45 frames/second. A 100-frame-per-second token rate gives that observed
traffic useful headroom while retaining a finite catch-all for malformed and
non-operation traffic.

The tighter independent limits for EVENT writes, queries, and authentication
events remain unchanged, so this does not expand those operation budgets. No
new configuration option is added because clients cannot discover or adapt
to a non-standard frame quota.

Add scenario coverage proving a 1,201-frame burst remains connected and
completes a subsequent REQ/EOSE exchange, while a rapid 6,001-frame burst is
still closed. Update the changelog and relay hardening/scaling references in
the same commit.

Per-IP admission fairness and upstream rust-nostr policy remain deliberately
out of scope.

Validation:
- nix develop -c cargo test --test relay_message_rate (46 passed)
- nix develop -c cargo test --lib (643 passed on the initial candidate)
- nix build .#ngit-grasp (initial candidate; final package rebuilt by deploy)
2026-08-07 09:36:21 +00:00

2.6 KiB

Embedded relay limits

ngit-grasp embeds rust-nostr LocalRelay 0.45.0. The application selects every effective limit explicitly so future dependency defaults cannot silently alter production admission policy.

Effective limits

Limit ngit-grasp default Operator configuration NIP-11
Total inbound connections Unbounded NGIT_MAX_CONNECTIONS No standard field
Active REQs per connection 500 NGIT_RELAY_MAX_SUBSCRIPTIONS max_subscriptions
Results per filter 500 NGIT_RELAY_FILTER_LIMIT max_limit, default_limit
Serialized event size 192 KiB NGIT_RELAY_MAX_EVENT_SIZE_BYTES No equivalent field
Event writes per minute 60 Fixed No standard field
Queries per minute 120 Fixed No standard field
Authentication events per minute 30 Fixed No standard field
WebSocket messages per minute 6,000 Fixed No standard field
WebSocket message size 5 MiB Fixed max_message_length
Handshake deadline 10 seconds Fixed No standard field
Subscription-ID length 250 bytes Fixed max_subid_length
Filters per REQ 20 Fixed No standard field
Subscription state per connection 1 MiB Fixed No standard field
Active negentropy sessions per connection 10 Fixed No standard field
Negentropy items per connection 50,000 Fixed No standard field
Negentropy frame 60,000 bytes Fixed upstream No standard field

The filter setting is applied consistently to rust-nostr's explicit filter cap, per-query result cap, and omitted-limit default. Limits are per filter; results from multiple filters in one REQ are merged without an aggregate truncation.

The 192 KiB event default is three times rust-nostr's new 64 KiB default. Production history contains a valid NIP-34 patch event of about 149 KiB, so 64 KiB is incompatible with ngit-grasp's purpose. The raised limit remains bounded and below the 5 MiB WebSocket message ceiling.

Client adaptation

ngit-grasp refetches NIP-11 per connection session. Its outbound sync ledger uses max_subscriptions, falling back conservatively when absent. Adaptive historic pagination uses default_limit with a verification page and learns from raw delivered page sizes. max_limit describes explicit filter limits; historic sync currently omits limit, so it does not treat max_limit as an omitted-filter page-size promise.

NIP-11 has no standard max_filters field in the current schema. Filter-count and serialized-message budgets therefore remain conservative client-side constants rather than falsely negotiated capabilities.