4.7 KiB
nostr-relay-builder Limits
This document describes the rate limiting, throttling, and query limits in nostr-relay-builder version 0.44. These are the limits that apply to ngit-grasp and any relay built with this crate.
Note: Other relay implementations (strfry, nostream, etc.) have different limits. This document focuses on nostr-relay-builder specifically.
Hard Limits (Cannot Be Changed)
These limits are enforced and cannot be overridden by configuration:
WebSocket Message Limits (from tungstenite)
| Limit | Default Value | Source |
|---|---|---|
max_message_size |
64 MB (67,108,864 bytes) | tungstenite default |
max_frame_size |
16 MB (16,777,216 bytes) | tungstenite default |
nostr-relay-builder does not override these tungstenite defaults.
Practical impact: A single REQ message or EVENT with extremely large content could hit these limits. A filter with ~1,000,000 32-byte event IDs (~32MB in JSON) would fit, but 2 million would not.
Negentropy Frame Limit
| Limit | Value | Source |
|---|---|---|
frame_size_limit |
60,000 bytes (60KB) | Hardcoded in inner.rs |
let mut negentropy = Negentropy::owned(storage, 60_000)?;
If reconciliation needs more data, negentropy splits across multiple NEG-MSG round-trips automatically.
No Hard Limits On
nostr-relay-builder does NOT enforce hard limits on:
| Item | Hard Limit? | Notes |
|---|---|---|
Tag values per filter (#e, #p, etc.) |
❌ None | Only limited by message size |
| Filters per REQ array | ❌ None | Only limited by message size |
| Filter JSON size | ❌ None | Only limited by WebSocket message |
| Authors per filter | ❌ None | Only limited by message size |
| Kinds per filter | ❌ None | Only limited by message size |
| IDs per filter | ❌ None | Only limited by message size |
Configurable Limits (Server-Side)
These limits have defaults but can be configured via RelayBuilder:
Query/Response Limits
| Setting | Default | Builder Method |
|---|---|---|
default_filter_limit |
500 | .default_filter_limit(n) |
max_filter_limit |
None (no cap) |
.max_filter_limit(n) |
Behavior:
- If filter has no
limitfield → server appliesdefault_filter_limit(500) - If filter has
limit > max_filter_limit→ clamped tomax_filter_limit - If filter has specific
ids→ usesids.len()as limit
Rate Limiting
| Setting | Default | Description |
|---|---|---|
max_reqs |
500 | Max active subscriptions per session |
notes_per_minute |
60 | Token bucket rate for EVENT writes |
Rate limiting uses a token bucket: tokens regenerate proportionally over time, each EVENT consumes 1 token.
Connection/Session Limits
| Setting | Default | Description |
|---|---|---|
max_connections |
None |
Max concurrent WebSocket connections |
max_subid_length |
250 | Max characters in subscription ID |
REQ vs Negentropy Limits
| Aspect | REQ (NIP-01) | Negentropy (NIP-77) |
|---|---|---|
| Events returned | Limited (default: 500) | Unlimited (all IDs returned) |
| Filter limit applies? | ✅ Yes | ❌ No |
| Returns full events? | ✅ Yes | ❌ No (only EventId + Timestamp) |
| Message size limit | 64MB (WebSocket) | 60KB per frame |
Why negentropy returns all: It only returns ~40 bytes per event (ID + timestamp) for set reconciliation. Full events are fetched separately after identifying what's missing.
Quick Reference
| Limit | Value | Type |
|---|---|---|
| WebSocket message | 64 MB | Hard (tungstenite) |
| WebSocket frame | 16 MB | Hard (tungstenite) |
| Negentropy frame | 60 KB | Hard (hardcoded) |
| Tags per filter | None | Soft (message size only) |
| Filters per REQ | None | Soft (message size only) |
| Events per REQ | 500 | Configurable default |
| Max subscriptions | 500 | Configurable default |
| Write rate | 60/min | Configurable default |
ngit-grasp Configuration
ngit-grasp uses defaults (no custom limits configured):
let builder = RelayBuilder::default()
.database(database.clone())
.write_policy(write_policy.clone());
Additionally, ngit-grasp's memory database limits to 100,000 events (LMDB has no such limit).