Problem
After moving /routing-get, DHT refresh, and /ws/ipns to the Go sidecar (PR #18 + upcoming PR), the Python pinner still leaks ~160 MB/min from the ipns-intercept write path:
- ~28 ipns-intercept stores/min trigger chain validation
- Each chain validation call fetches CID content from kubo via
/api/v0/cat
- The HTTP response parsing (
httpx response objects, CBOR/JSON decode) creates short-lived Python objects that fragment CPython's pymalloc arenas
malloc_trim() cannot reclaim pymalloc-internal fragmentation
Current Memory Profile
| Component |
RSS |
Growth |
Status |
| Go sidecar |
27 MB |
flat |
routing-get, DHT refresh, ws/ipns |
| Python pinner |
~160 MB/min |
leaking |
ipns-intercept, chain validation, Nostr |
The Python pinner self-restarts at 4 GB (~25 min cycle). This is stable but wastes resources.
Root Cause
The validate_version_chain() function in nostr_pinner.py fetches CID content from kubo:
response = await client.post(f"{IPFS_API_URL}/api/v0/cat", params={"arg": cid})
content = response.content # bytes object allocated in pymalloc arena
data = cbor2.loads(content) # dict/list objects allocated in pymalloc arena
Even at ~25 req/min, the accumulated allocations fragment pymalloc arenas faster than they can be reclaimed.
Proposed Fix
Option A: Move chain validation to Go sidecar (recommended)
Move the validate_version_chain logic to Go:
- Go sidecar adds
/internal/validate-chain endpoint
- Accepts
ipns_name, new_cid, current_cid, new_sequence, existing_sequence
- Fetches CID content from kubo (
/api/v0/cat)
- Parses CBOR to extract
_meta.lastCid and _meta.version
- Validates chain link (lastCid must equal current CID)
- Returns validation result as JSON
Python ipns-intercept calls Go for validation instead of doing it in-process. This eliminates all HTTP/CBOR allocations from Python.
Estimated impact: Reduces Python to Nostr relay connections + SQLite writes only. Expected steady-state: ~100-200 MB, flat.
Option B: Move entire ipns-intercept to Go
More aggressive: move the full ipns-intercept handler to Go, including:
- Rate limiting (per-IP, per-name, global)
- Signature verification
- Chain validation
- Record storage with optimistic locking
- Security audit logging
Python would only handle Nostr relay subscriptions and reannouncement.
Estimated impact: Python ~50-100 MB, flat. But significantly more Go code.
Option C: Accept current state
The 25-min self-restart cycle is stable and the Go sidecar handles all high-throughput paths. The remaining leak is bounded by MAX_RSS_MB (4 GB default).
This is acceptable if the operational overhead of the restart cycle is tolerable.
Context
Problem
After moving
/routing-get, DHT refresh, and/ws/ipnsto the Go sidecar (PR #18 + upcoming PR), the Python pinner still leaks ~160 MB/min from the ipns-intercept write path:/api/v0/cathttpxresponse objects, CBOR/JSON decode) creates short-lived Python objects that fragment CPython's pymalloc arenasmalloc_trim()cannot reclaim pymalloc-internal fragmentationCurrent Memory Profile
The Python pinner self-restarts at 4 GB (~25 min cycle). This is stable but wastes resources.
Root Cause
The
validate_version_chain()function innostr_pinner.pyfetches CID content from kubo:Even at ~25 req/min, the accumulated allocations fragment pymalloc arenas faster than they can be reclaimed.
Proposed Fix
Option A: Move chain validation to Go sidecar (recommended)
Move the
validate_version_chainlogic to Go:/internal/validate-chainendpointipns_name,new_cid,current_cid,new_sequence,existing_sequence/api/v0/cat)_meta.lastCidand_meta.versionPython ipns-intercept calls Go for validation instead of doing it in-process. This eliminates all HTTP/CBOR allocations from Python.
Estimated impact: Reduces Python to Nostr relay connections + SQLite writes only. Expected steady-state: ~100-200 MB, flat.
Option B: Move entire ipns-intercept to Go
More aggressive: move the full ipns-intercept handler to Go, including:
Python would only handle Nostr relay subscriptions and reannouncement.
Estimated impact: Python ~50-100 MB, flat. But significantly more Go code.
Option C: Accept current state
The 25-min self-restart cycle is stable and the Go sidecar handles all high-throughput paths. The remaining leak is bounded by
MAX_RSS_MB(4 GB default).This is acceptable if the operational overhead of the restart cycle is tolerable.
Context