Relay REQ budget (20 subscriptions) exhausted mid-session — late subscriptions silently die until reload #18

Closed
opened 2026-07-08 11:30:36 +00:00 by laoc · 3 comments
Owner

Found while investigating #4. A single app session quickly produces dozens of relay NOTICEs ERROR: too many concurrent REQs on relay.edufeed.org / relay-rpi.edufeed.org / nos.lol (strfry maxSubsPerConnection = 20).

Once the per-connection budget is exhausted, every later REQ on that socket silently fails — share-state detection, kind 10002 relay-list fetches, NIP-46 signer subscriptions. A page reload gives a fresh socket and everything works again, which users experience as "random breakage that a reload fixes" (see #4).

Needs: an audit of subscription lifecycles (leaked/never-closed REQs), possibly multiplexing or closing one-shot REQs eagerly, and surfacing relay NOTICEs in debug mode.

Found while investigating #4. A single app session quickly produces dozens of relay NOTICEs `ERROR: too many concurrent REQs` on relay.edufeed.org / relay-rpi.edufeed.org / nos.lol (strfry `maxSubsPerConnection = 20`). Once the per-connection budget is exhausted, every later REQ on that socket silently fails — share-state detection, kind 10002 relay-list fetches, NIP-46 signer subscriptions. A page reload gives a fresh socket and everything works again, which users experience as "random breakage that a reload fixes" (see #4). Needs: an audit of subscription lifecycles (leaked/never-closed REQs), possibly multiplexing or closing one-shot REQs eagerly, and surfacing relay NOTICEs in debug mode.
Author
Owner

Full audit done (read-only). Root cause mechanics:

  • applesauce opens one shared WebSocket per relay URL, and relay.edufeed.org serves ALL five app-relay categories plus fallback — nearly every REQ in the app lands on that one socket with strfry's maxSubsPerConnection = 20.
  • applesauce has no per-connection cap or queue, and strfry rejects excess REQs with a NOTICE (not CLOSED) — the offending REQ neither errors nor completes; pool.subscription() streams hang forever. That's the "silently broken until reload".

Ranked hotspots:

  1. CommunityShare mounts eagerly inside CLOSED dialogs (EventContextMenu, CalendarEventDetailsModal, AddToCalendarDropdown, ResourceFormWizard): every rendered card fires 2–4 share-detection REQs even though the dialog was never opened. 10 cards = 20–40 REQs → blows the budget alone.
  2. Discover page: burst of category/supplemental/curated loaders + one pagination loader per relay.
  3. Calendar loaders: persistent pool.subscription() (never EOSE-completes) + per-author deletion loaders.
  4. inbox-service: fixed ~5 long-lived REQs per session baseline.
  5. Per-item comment/RSVP loaders on list views.

Existing good pattern: the reactions loader batches per-card queries (bufferTime + lazy IntersectionObserver) — share detection should do the same.

Remediation plan (priority order):

  1. Don't render/subscribe CommunityShare until its dialog is actually open — biggest single win, likely resolves the symptom.
  2. Collapse share detection from 2–4 loaders to ONE merged filter (kinds:[6,16,30222], #e+#a).
  3. Client-side per-connection REQ semaphore (cap ~15, queue overflow) around the pool — the durable fix; applesauce offers nothing built-in.
  4. React to the too many concurrent REQs NOTICE (back off + re-dispatch) instead of hanging.
  5. Polish: dedupe getAllLookupRelays(), lazy ReactionBar in AMB grid cards.

Implementing (1) and (2) now; (3)/(4) are a bigger infrastructure change worth its own PR.

Full audit done (read-only). Root cause mechanics: - applesauce opens **one shared WebSocket per relay URL**, and relay.edufeed.org serves ALL five app-relay categories plus fallback — nearly every REQ in the app lands on that one socket with strfry's `maxSubsPerConnection = 20`. - applesauce has **no per-connection cap or queue**, and strfry rejects excess REQs with a NOTICE (not CLOSED) — the offending REQ neither errors nor completes; `pool.subscription()` streams hang forever. That's the "silently broken until reload". **Ranked hotspots:** 1. **`CommunityShare` mounts eagerly inside CLOSED dialogs** (`EventContextMenu`, `CalendarEventDetailsModal`, `AddToCalendarDropdown`, `ResourceFormWizard`): every rendered card fires 2–4 share-detection REQs even though the dialog was never opened. 10 cards = 20–40 REQs → blows the budget alone. 2. Discover page: burst of category/supplemental/curated loaders + one pagination loader per relay. 3. Calendar loaders: persistent `pool.subscription()` (never EOSE-completes) + per-author deletion loaders. 4. inbox-service: fixed ~5 long-lived REQs per session baseline. 5. Per-item comment/RSVP loaders on list views. **Existing good pattern:** the reactions loader batches per-card queries (`bufferTime` + lazy IntersectionObserver) — share detection should do the same. **Remediation plan (priority order):** 1. Don't render/subscribe `CommunityShare` until its dialog is actually open — biggest single win, likely resolves the symptom. 2. Collapse share detection from 2–4 loaders to ONE merged filter (`kinds:[6,16,30222]`, `#e`+`#a`). 3. Client-side per-connection REQ semaphore (cap ~15, queue overflow) around the pool — the durable fix; applesauce offers nothing built-in. 4. React to the `too many concurrent REQs` NOTICE (back off + re-dispatch) instead of hanging. 5. Polish: dedupe `getAllLookupRelays()`, `lazy` ReactionBar in AMB grid cards. Implementing (1) and (2) now; (3)/(4) are a bigger infrastructure change worth its own PR.
Author
Owner

Quick wins 1+2 from the plan are implemented on branch fix/issue-18-share-detection-reqs (with tests, verified at runtime): CommunityShare no longer mounts inside closed dialogs/dropdowns — detection REQs fire only when the share UI actually opens — and detection itself collapsed from up to 4 loaders to ONE REQ with merged filters over a de-duplicated relay set. Remaining (bigger, separate PR): per-connection REQ semaphore + reacting to the too-many-REQs NOTICE, plus one-shot lifetimes for persistent pool.subscription() streams.

Quick wins 1+2 from the plan are implemented on branch `fix/issue-18-share-detection-reqs` (with tests, verified at runtime): CommunityShare no longer mounts inside closed dialogs/dropdowns — detection REQs fire only when the share UI actually opens — and detection itself collapsed from up to 4 loaders to ONE REQ with merged filters over a de-duplicated relay set. Remaining (bigger, separate PR): per-connection REQ semaphore + reacting to the too-many-REQs NOTICE, plus one-shot lifetimes for persistent pool.subscription() streams.
Author
Owner

Confirmed by Steffen; quick wins merged to dev. The systemic follow-up (per-connection REQ semaphore + NOTICE handling) stays documented in the audit above — reopen or file fresh if the symptom recurs.

Confirmed by Steffen; quick wins merged to dev. The systemic follow-up (per-connection REQ semaphore + NOTICE handling) stays documented in the audit above — reopen or file fresh if the symptom recurs.
laoc closed this issue 2026-07-09 10:29:44 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
edufeed/edufeed-app#18
No description provided.