17 KiB
Cache-All Feasibility Test Program
Goal
Build a standalone C test program that connects to all outbox relays of followed pubkeys and attempts to subscribe to all events (no author filter) to determine if relays will tolerate this level of data flow. The program logs all relay interactions (disconnections, rate limiting, CLOSED messages, NOTICE messages) so we can assess feasibility before modifying the caching daemon.
Background
The current caching daemon subscribes only to events from followed pubkeys
using authors=[followed_set] filters. The proposed "cache everything" approach
would subscribe to all events on those relays and prune old non-followed events
later. The key unknown is whether relays will allow this — they may disconnect
or rate-limit clients that request too much data.
Preliminary Probe Results (60s on 3 major relays)
Before designing the test program, we ran 60-second probes using nak req --stream
with no filter (all events) against 3 major relays to measure real-world data volume
and kind distribution.
Relay Connectivity
| Relay | Events/60s | Avg Bytes | Max Event | Unique Pubkeys | Notes |
|---|---|---|---|---|---|
| nos.lol | 2,634 | 1,713 | 44,372 (kind 30089) | 1,228 | Connected OK |
| relay.primal.net | 1,547 | 1,920 | 207,377 (kind 3) | 613 | Connected OK |
| relay.damus.io | 0 | — | — | — | HTTP 503 (unavailable) |
Kind Distribution (nos.lol + primal combined, ~4,181 events)
| Kind | Description | Count | % | Avg Bytes | Notes |
|---|---|---|---|---|---|
| 21059 | Gift wrap (NIP-59) | 1,063 | 25.4% | 2,574 | Encrypted DMs — large, noisy |
| 5 | Deletion requests | 592 | 14.2% | 447 | Very common |
| 30078 | App-specific data | 173 | 4.1% | 3,670 | Variable size, can be large |
| 20001 | Ephemeral (key exchange?) | 253 | 6.0% | 849 | Short-lived |
| 22580 | WebRTC signaling | 193 | 4.6% | 795 | Noise |
| 1059 | Gift wrap seal (NIP-59) | 154 | 3.7% | 3,023 | Encrypted wrapper |
| 22734 | WebRTC signaling | 168 | 4.0% | 427 | Noise |
| 1 | Text notes | 58 | 1.4% | 812 | Primary content |
| 7 | Reactions | 59 | 1.4% | 534 | Likes |
| 0 | Profile metadata | 33 | 0.8% | 985 | Replaceable |
| 3 | Follow lists | 10 | 0.2% | 43,077 | Replaceable, can be huge |
| 10002 | Relay lists | 55 | 1.3% | 418 | Small |
| 9735 | Zap receipts | 22 | 0.5% | 2,110 | Payment confirmations |
| 6 | Reposts | 11 | 0.3% | 1,876 | |
| 30023 | Long-form articles | 3 | 0.1% | 6,119 | |
| 30089 | Chunked data | 16 | 0.4% | 43,907 | Very large (avg 44KB!) |
| 30001 | Lists (encrypted) | 4 | 0.1% | 30,538 | Large encrypted content |
| 30815 | Ephemeral large | 4 | 0.1% | 37,534 | Very large (avg 37KB!) |
| 25555 | App data | 31 | 0.7% | 5,115 | |
| 13194 | NWC info | 15 | 0.4% | 514 | Wallet connect |
| 445 | Encrypted group msg | 22 | 0.5% | 1,452 | |
| Other | 90+ other kinds | ~600 | ~14% | varies | Long tail of app-specific kinds |
Key Findings
-
Gift wrap (kind 21059) dominates: 25% of all events are NIP-59 gift wraps (encrypted DMs). These are large (~2.5KB avg) and mostly noise for a caching relay. They have
expirationtags and are ephemeral by nature. -
Deletion requests (kind 5) are extremely common: 14% of all events. This is surprising — relays are very busy processing deletes.
-
WebRTC signaling (kinds 22580, 22734, etc.) is ~10% of traffic: These are ephemeral signaling events for video/voice calls. Pure noise for caching.
-
"Social" content (kinds 1, 7, 6, 0, 3) is only ~4% of total events: The vast majority of relay traffic is NOT the content users actually see.
-
Some kinds are extremely large: Kind 30089 averages 44KB, kind 30001 averages 30KB, kind 30815 averages 37KB. These would consume significant storage.
-
Data volume is manageable: ~2,600-4,200 events/min across 2 relays. At this rate, a 24h test would collect ~3.7-6M events. Storage would be ~6-10 GB of raw JSON.
-
Damus.io is unavailable: Returns HTTP 503. This may be temporary or may indicate they block unfiltered subscriptions.
Implications for the Test Program
- Subscribe to ALL kinds for the test — we need to measure which kinds relays actually send and whether they tolerate the volume.
- Do NOT save full event JSON during the test — at ~2MB/min, a 24h run would produce ~3GB of raw data per relay. Instead, log kind/pubkey/size summaries.
- Track kind 21059 (gift wrap) separately — it's the dominant kind and may need special handling in the real implementation.
- Monitor for CLOSED messages — if relays start closing subscriptions due to volume, we'll see it in the raw relay log.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ cache_all_feasibility_test │
│ │
│ 1. Load root npubs from config.jsonc │
│ 2. Resolve follow graph (kind-3) → followed set │
│ 3. Discover outbox relays (kind-10002) for each follow │
│ 4. Compute minimum covering set of relays │
│ 5. Connect to all covering relays │
│ 6. Subscribe to ALL events (no authors filter) on each │
│ 7. Log every event received (counts + stats, not full JSON)│
│ 8. Log every relay message: CLOSED, NOTICE, EOSE, error │
│ 9. Run for configurable duration (default 24h) │
│ 10. Save summary report + per-relay stats to files │
└─────────────────────────────────────────────────────────────┘
File Structure
All new files go in caching/ directory, alongside the existing daemon:
caching/
├── Makefile # Modified: add test target
├── src/
│ ├── main.c # Existing daemon (unchanged)
│ ├── cache_all_test.c # NEW: test program entry point
│ ├── cache_all_test.h # NEW: test program header
│ ├── debug.c / debug.h # Existing (reused)
│ ├── config.c / config.h # Existing (reused for config loading)
│ ├── state.c / state.h # Existing (reused for pubkey set)
│ ├── follow_graph.c / follow_graph.h # Existing (reused)
│ ├── relay_discovery.c / relay_discovery.h # Existing (reused)
│ └── ... # Other existing files unchanged
Detailed Design
1. Config Loading (reuse existing)
Reuse caching/src/config.c and
caching/src/config.h to load root npubs, upstream
relays, kinds, and follow graph settings from the same .jsonc config file
the daemon uses.
2. Follow Graph Resolution (reuse existing)
Reuse caching/src/follow_graph.c to:
- Decode root npubs to hex
- Query each root's most recent kind-3 contact list from bootstrap relays
- Build the
cr_pubkey_set_tof followed pubkeys
3. Relay Discovery (reuse existing)
Reuse caching/src/relay_discovery.c to:
- Query kind-10002 for each followed pubkey
- Parse "r" tags to build outbox relay map
- Compute minimum covering set of relays
4. Subscription Strategy
For each relay in the covering set, open a subscription with:
// Filter: ALL events (no authors, no kinds, no limit)
cJSON *filter = cJSON_CreateObject();
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber((double)time(NULL)));
This requests every new event from now onward on that relay.
Key difference from existing daemon: No authors filter. This means we
receive events from everyone on that relay, not just followed pubkeys.
5. Event Handling & Logging
The on_event callback should:
- Count events per relay (maintain a per-relay counter)
- Count events per kind (maintain a kind distribution map)
- Count events per pubkey (track which pubkeys are most active)
- Log at INFO level every N events (e.g., every 1000) with summary stats
- Do NOT save full event JSON to disk during the run (would be too much data)
- Periodically checkpoint (every 5 min) a summary to a file
6. Relay Interaction Logging — Raw Relay Responses
Critical requirement: Log the actual raw response text from the relay, not an interpreted summary. The relay's own words are what matter.
The on_event, on_eose, and subscription status callbacks from
nostr_core_lib provide status strings and message
content. These must be logged verbatim.
What to log and how:
| Trigger | Log Level | What to Log (verbatim relay text) |
|---|---|---|
| EOSE | INFO | [RELAY] <url> EOSE |
| CLOSED | WARN | [RELAY] <url> CLOSED: <relay's exact reason text> |
| NOTICE | WARN | [RELAY] <url> NOTICE: <relay's exact notice text> |
| Error | ERROR | [RELAY] <url> ERROR: <relay's exact error message> |
| Disconnect | WARN | [RELAY] <url> DISCONNECTED: <transport-level error if any> |
| Reconnect | INFO | [RELAY] <url> RECONNECTED |
| OK (publish) | INFO | [RELAY] <url> OK: <event_id> <relay's exact message> |
Do NOT categorize or summarize. If a relay says:
"rate-limited: please wait 60 seconds before sending new requests"
log that exact string. Do NOT log just "rate-limited".
Raw Relay Log File
In addition to the normal debug log output, write a raw relay log file:
cache_all_test_raw_relay_<timestamp>.log
This file contains only relay responses, one per line, in this format:
[TIMESTAMP] [RELAY] <relay_url> <RAW_RESPONSE>
Example:
[2026-08-02 13:01:00] [RELAY] wss://relay.damus.io CLOSED: "rate-limited: please wait 60 seconds before sending new requests"
[2026-08-02 13:01:05] [RELAY] wss://relay.damus.io RECONNECTED
[2026-08-02 13:01:10] [RELAY] wss://relay.damus.io NOTICE: "too many subscriptions, closing oldest"
[2026-08-02 13:02:00] [RELAY] wss://relay.primal.net EOSE
[2026-08-02 13:02:01] [RELAY] wss://relay.primal.net ERROR: "connection closed unexpectedly"
This file is append-only and can be tail -f'd during the test run.
Event Logging
For events received, log at TRACE level (not to the raw relay log):
[TRACE] [EVENT] relay=<url> kind=<N> pubkey=<first8chars>... id=<first8chars>...
This gives enough to correlate without flooding the log with full JSON. Every 1000 events, log a summary at INFO level:
[INFO] [STATS] 5000 events received total | relayA: 3200 relayB: 1800 | kinds: 1=4500 7=500
7. Summary Report
At the end of the run (or on SIGINT/SIGTERM), write a report file:
cache_all_test_report_<timestamp>.txt
Contents:
=== Cache-All Feasibility Test Report ===
Duration: 24h 3m 12s
Config: ./caching_relay_config.jsonc
=== Relay Summary ===
Relay Events EOSE CLOSED NOTICE Errors Status
wss://relay.example.com 124532 12 0 2 0 OK
wss://relay2.example.com 0 0 3 5 2 BLOCKED
=== Kind Distribution ===
Kind Count %
0 1,234 0.5%
1 234,567 94.2%
3 567 0.2%
7 12,345 5.0%
9734 234 0.1%
=== Top 10 Pubkeys by Event Count ===
pubkey_hex_here... 12,345 events
pubkey_hex_here... 8,901 events
...
=== Raw Relay Response Log ===
[2026-08-02 13:01:00] wss://relay.damus.io CLOSED: "rate-limited: please wait 60 seconds before sending new requests"
[2026-08-02 13:01:05] wss://relay.damus.io RECONNECTED
[2026-08-02 13:01:10] wss://relay.damus.io NOTICE: "too many subscriptions, closing oldest"
[2026-08-02 13:02:00] wss://relay.primal.net EOSE
[2026-08-02 13:02:01] wss://relay.primal.net ERROR: "connection closed unexpectedly"
...
The Raw Relay Response Log section is a copy of the raw relay log file
(cache_all_test_raw_relay_<timestamp>.log).
It contains the verbatim text from each relay response, not interpreted
or summarized.
8. Per-Relay Stats File
Additionally, write a JSON file with per-relay detailed stats:
cache_all_test_stats_<timestamp>.json
This can be used for programmatic analysis. It includes the raw relay response text for each interaction, not just counts.
Implementation Steps
Step 1: Create caching/src/cache_all_test.h
Header file declaring the test program's public interface:
#ifndef CACHE_ALL_TEST_H
#define CACHE_ALL_TEST_H
/* Run the cache-all feasibility test.
* config_path: path to .jsonc config file
* duration_seconds: how long to run (0 = run until SIGINT)
* log_level: debug level 0-5
* Returns 0 on success, -1 on error.
*/
int run_cache_all_test(const char *config_path,
long duration_seconds,
int log_level);
#endif
Step 2: Create caching/src/cache_all_test.c
Main implementation file with these sections:
- Includes and forward declarations
- Per-relay stats tracking structure
- Global stats accumulator
- Callback implementations (on_event, on_eose, on_status)
- Report generation (write summary + JSON stats)
- Main entry point (
run_cache_all_test)
Step 3: Modify caching/Makefile
Add a new target cache_all_test that compiles the test program:
TEST_SRC = src/cache_all_test.c src/debug.c src/jsonc_strip.c src/config.c \
src/state.c src/follow_graph.c src/relay_discovery.c
cache_all_test: $(TEST_SRC) $(NOSTR_CORE_LIB)
# ... compile to ../build/cache_all_test
Step 4: Build and Run
cd caching && make cache_all_test
./build/cache_all_test -c caching_relay_config.jsonc -d 3 -t 86400
Output Files
| File | Contents |
|---|---|
cache_all_test_raw_relay_<timestamp>.log |
Primary output. One line per relay response, verbatim text. Can be tail -f'd live. |
cache_all_test_report_<timestamp>.txt |
Summary report with counts, kind distribution, top pubkeys, and raw relay log section. |
cache_all_test_stats_<timestamp>.json |
Machine-readable JSON with per-relay stats including raw response texts. |
What We're Measuring
- Relay tolerance: Do relays CLOSE our subscription or disconnect us?
- Rate limiting: How often do we get rate-limited? What are the cooldown periods?
- Data volume: How many events per hour per relay? What's the kind distribution?
- Connection stability: How often do relays drop us? Do they allow reconnection?
- Pubkey diversity: How many unique pubkeys are posting? What's the ratio of followed vs non-followed events?
Success Criteria
The test is considered a success (feasible) if:
- At least 80% of relays maintain the subscription for the full duration
- Rate limiting events are infrequent (< 5 per relay per day)
- No relay permanently bans or blacklists the connection
- Data volume is manageable (under ~1M events/day total)
The test is considered a failure (not feasible) if:
- Most relays CLOSE the subscription within minutes
- Rate limiting is constant (every few minutes)
- Multiple relays permanently disconnect
Non-Goals
- This test does NOT save events to PostgreSQL
- This test does NOT modify the existing caching daemon
- This test does NOT implement pruning logic
- This test does NOT need to be efficient for production use
Mermaid Diagram
flowchart TD
A[Start] --> B[Load config from .jsonc]
B --> C[Resolve follow graph kind-3]
C --> D[Discover outbox relays kind-10002]
D --> E[Compute min covering set]
E --> F[Connect to all covering relays]
F --> G[Subscribe to ALL events no authors filter]
G --> H{Test duration reached or SIGINT?}
H -->|No| I[Pump relay pool]
I --> J[Count events per relay/kind/pubkey]
J --> K[Log relay interactions CLOSED/NOTICE/errors]
K --> L[Periodic checkpoint every 5 min]
L --> H
H -->|Yes| M[Write summary report]
M --> N[Write per-relay JSON stats]
N --> O[Cleanup and exit]
Files to Create
| File | Purpose |
|---|---|
caching/src/cache_all_test.h |
Header with public API |
caching/src/cache_all_test.c |
Main implementation (~400-500 lines) |
Files to Modify
| File | Change |
|---|---|
caching/Makefile |
Add cache_all_test target |
Files NOT Modified
The existing caching daemon files are not touched:
caching/src/main.c— unchangedcaching/src/backfill.c— unchangedcaching/src/live_subscriber.c— unchangedcaching/src/pg_inbox.c— unchangedcaching/src/forward_catchup.c— unchangedcaching/src/pg_config.c— unchanged