Skip to content

fix(mobile): actually throttle unread catch-up at startup - #4444

Open
CryptoJones wants to merge 1 commit into
block:mainfrom
CryptoJones:fix/mobile-unread-catchup-burst
Open

fix(mobile): actually throttle unread catch-up at startup#4444
CryptoJones wants to merge 1 commit into
block:mainfrom
CryptoJones:fix/mobile-unread-catchup-burst

Conversation

@CryptoJones

Copy link
Copy Markdown

Problem

_catchUpUnreadEvents built a List<Future> before awaiting it:

for (final channel in channels) {
  futures.add(_catchUpUnreadEventsForChannel(...));   // starts immediately
}

const batchSize = 5;
for (var i = 0; i < futures.length; i += batchSize) {
  await Future.wait(futures.sublist(i, min(i + batchSize, futures.length)));
}

Invoking an async function in Dart starts it right away, so every per-channel history REQ was dispatched during the loop that builds the list. The batch loop below only awaited work that was already in flight, which made batchSize = 5 a no-op for concurrency.

A member of N channels therefore issued N concurrent history requests at startup, on top of the per-channel live subscriptions, presence, and the section sync.

Impact

On a relay using the default WS admission budget (BUZZ_RATE_LIMIT_HUMAN_WS_EVENTS_PER_SEC = 10 over the fixed 5s WS_BURST_WINDOW_SECS), the overflow is rejected:

[ChannelsNotifier] unread catch-up failed for <id>: Exception: rate-limited: quota exceeded; retry in 1s
[ChannelSectionsManager] startup sync incomplete; retrying in 2000ms (attempt 1)

The failed catch-ups then retry, re-spending the same fixed window, so a single overflow becomes a sustained failure loop. On a 14-channel account the app launches, fails to populate, and retries indefinitely — which presents to the user as the app hanging or crashing on open.

The relay-side comment on WS_BURST_WINDOW_SECS notes the window was sized for desktop startup, which establishes fewer simultaneous subscriptions than mobile.

Fix

Collect thunks rather than futures and invoke each batch as it is awaited, so at most batchSize requests are ever in flight. No behavioural change beyond concurrency.

Testing

  • dart format — clean
  • flutter analyze — no issues
  • flutter test — 905 pass. One pre-existing failure in channel_detail_page_test.dart ("keeps follow mode off while a tall newest message stays visible") which reproduces identically on a clean origin/main checkout and is unrelated to this change.

Notes

This reduces the startup burst but does not eliminate it — the per-channel live subscriptions still fan out from a separate path. Filed #4440 for the relay side, where the WS admission budget always uses the human per-second limit even for agents and has no agent tier.

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

Invoking an async function in Dart starts it immediately, so building a
List<Future> up front dispatched every per-channel history REQ at once and
left the batch loop only awaiting work already in flight. The batchSize of 5
never limited concurrency.

A member of N channels therefore issued N concurrent history REQs during
startup, on top of the per-channel live subscriptions, presence, and the
section sync. On a relay with the default fixed-window WS admission budget
(10 events/sec over a 5s window) the overflow was rejected with
"rate-limited: quota exceeded", and the failed catch-ups then retried,
re-spending the same budget.

Collect thunks instead and invoke each batch as it is awaited, so at most
batchSize requests are ever in flight.

Signed-off-by: Aaron K. Clark <cryptojones@owasp.org>
@CryptoJones
CryptoJones requested a review from a team as a code owner August 3, 2026 00:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant