Production gitnostr.com repeatedly received CLOSED responses from relay.ngit.dev after reconnect: its 34-filter live set for 924 full repositories, 89 state-only repositories, and 3,748 roots crossed rust-nostr 0.45's newly selected 1 MiB cumulative subscription-state limit. Fourteen representative REQs were accepted and the remainder were refused, silently leaving persistent live coverage incomplete; cooldown recovery only recreated the same impossible set. Raise the explicitly selected per-connection retained subscription-state allowance to 5 MiB. This remains a finite boundary, matches the largest individual WebSocket message already admitted, and leaves roughly four times the observed working-set headroom without promising the theoretical 500 x 96 KiB maximum. Document the exact serving policy and its lack of NIP-11 negotiation. A scenario opens 17 persistent sub-96 KiB REQs carrying 34 filters. It failed unchanged 2.1.1 at live-14 with the production CLOSED reason and passes with the new bound. Correctness assumes this allowance is enforced per connection by rust-nostr; client-side adaptation to unknown third-party cumulative byte limits and multi-connection sharding remain excluded because NIP-11 exposes no such capability. Validation: focused scenario passed; 658 library tests passed; git diff --check passed; nix build .#ngit-grasp passed.
3.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 | 1,200 | Fixed temporary override | 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 | 5 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.
The 5 MiB subscription-state allowance raises rust-nostr's 1 MiB default. Repository sync keeps multiple byte-budgeted filters live: production's 34-filter coverage reached roughly 1.2 MiB and the smaller bound silently left part of that coverage closed. The new allowance remains finite per connection, matches the maximum admitted WebSocket message, and provides about four times the observed working-set headroom. NIP-11 has no field for this cumulative byte limit, so clients cannot negotiate it.
The 1,200-query allowance temporarily overrides rust-nostr 0.45's newly
introduced 120/minute default. A finite per-connection bound remains one useful
DoS layer, but the upstream default is unusually restrictive for sync clients
and currently counts every SDK-managed NIP-77 NEG-MSG continuation as another
query. Review this 10× override after upstream changes that accounting; a small
per-connection quota is not a substitute for per-IP admission or global
resource bounds because clients can multiply connections.
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.