fix(status): status changes apply at once instead of waiting out the rate limit #469

Merged
jared merged 1 commits from fix/status-save-speed into lotus 2026-10-25 21:21:10 -04:00
Owner

Reported by Jared: changing your status (picking a quick status, then Save) takes far too long.

Cause: Synapse's default rc_presence allows one presence write per 10 s per user, shared by all of your devices. Production uses that default.

  • The presence heartbeat wrote at startup ({"presence":"online"}) and on every tab switch (unavailable → online).
  • So a status save shortly afterwards got a 429 and waited out the window: 7 s in my reproduction, up to 25 s with the retry budget.
  • During that wait the whole status section was locked: the quick-status chips, the text box, "Save current" and Clear, with the Save spinner going.
  • Picking a quick status already saves it, but Save compared with the server's echo, so it stayed lit. Clicking it sent a second write, which was bound to hit the limit.

Fix

  • A shared presence queue (queuePresenceWrite) used by the heartbeat and the status save. The newest write wins: changes made while a write waits out a 429 replace it, and only the latest is sent, as soon as the server allows.
  • The status UI doesn't wait on the server any more:
    • the status applies locally straight away;
    • Save switches off (it now compares with what this device applied);
    • the controls stay usable;
    • a note ("Status set. Sharing it with others, this can take a few seconds.") shows until the write lands;
    • errors still show as before.
  • The heartbeat skips writes the server already reflects, e.g. "online" at startup while /sync keeps you online. That leaves the budget free for a status change.

Measured with the production limit emulated in the browser (one PUT per 10 s, otherwise 429 with retry_after_ms):

Before After
Startup presence write yes (uses the budget) skipped (server already online)
Quick status right after opening Settings 429, then 7 s locked UI 200 at once
A second change right after Locked until the first finishes Applies on screen at once; sent when the server allows
Three quick statuses in a row Each waits for the last (≈30 s) Only the last is sent

Tested

  • New unit tests for the queue: newest wins after a 429 (the in-flight write isn't retried once a newer one is waiting, and the middle one is never sent); failures reject the callers. 1,445 unit tests pass.
  • New Playwright test, with the 10 s limit emulated:
    • Save switches off as soon as you save;
    • three quick statuses in a row: the field shows the last one, the note appears, and the server ends on the last one;
    • Lunch and Watching are never sent.
  • The existing "#226 status save survives the presence rate limit" test passes.
  • The full local-homeserver e2e file passes. The #218 member-drawer test failed once under load and passes 3/3 on its own.
  • Lint is at 29, unchanged.

Server-side option (not in this PR): raising rc_presence on matrix.lotusguild.org (e.g. 1/s, burst 5) would also make status changes reach other people faster. That's an infra change, so it's left for your call.

🤖 Generated with Claude Code

https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5

**Reported by Jared:** changing your status (picking a quick status, then Save) takes far too long. **Cause:** Synapse's default `rc_presence` allows **one presence write per 10 s per user**, shared by all of your devices. Production uses that default. - The presence heartbeat wrote at startup (`{"presence":"online"}`) and on every tab switch (unavailable → online). - So a status save shortly afterwards got a 429 and waited out the window: **7 s** in my reproduction, up to **25 s** with the retry budget. - During that wait the whole status section was locked: the quick-status chips, the text box, "Save current" and Clear, with the Save spinner going. - Picking a quick status already saves it, but Save compared with the server's echo, so it stayed lit. Clicking it sent a second write, which was bound to hit the limit. **Fix** - **A shared presence queue** (`queuePresenceWrite`) used by the heartbeat and the status save. The newest write wins: changes made while a write waits out a 429 replace it, and only the latest is sent, as soon as the server allows. - **The status UI doesn't wait on the server any more:** - the status applies locally straight away; - Save switches off (it now compares with what this device applied); - the controls stay usable; - a note ("Status set. Sharing it with others, this can take a few seconds.") shows until the write lands; - errors still show as before. - **The heartbeat skips writes the server already reflects**, e.g. "online" at startup while /sync keeps you online. That leaves the budget free for a status change. **Measured** with the production limit emulated in the browser (one PUT per 10 s, otherwise 429 with `retry_after_ms`): | | Before | After | |---|---|---| | Startup presence write | yes (uses the budget) | skipped (server already online) | | Quick status right after opening Settings | 429, then 7 s locked UI | 200 at once | | A second change right after | Locked until the first finishes | Applies on screen at once; sent when the server allows | | Three quick statuses in a row | Each waits for the last (≈30 s) | Only the last is sent | **Tested** - New unit tests for the queue: newest wins after a 429 (the in-flight write isn't retried once a newer one is waiting, and the middle one is never sent); failures reject the callers. 1,445 unit tests pass. - New Playwright test, with the 10 s limit emulated: - Save switches off as soon as you save; - three quick statuses in a row: the field shows the last one, the note appears, and the server ends on the last one; - Lunch and Watching are never sent. - The existing "#226 status save survives the presence rate limit" test passes. - The full local-homeserver e2e file passes. The #218 member-drawer test failed once under load and passes 3/3 on its own. - Lint is at 29, unchanged. **Server-side option (not in this PR):** raising `rc_presence` on matrix.lotusguild.org (e.g. 1/s, burst 5) would also make status changes reach other people faster. That's an infra change, so it's left for your call. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared added 1 commit 2026-10-25 20:14:48 -04:00
fix(status): status changes apply at once instead of waiting out the rate limit
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m55s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 27s
CI / Playwright smoke (e2e) (pull_request) Successful in 33m26s
8cdf192b56
Synapse allows one presence write per 10 s per user (shared by every
device). The heartbeat wrote presence at startup and on every tab
switch, so a status save right after usually got a 429 and waited 7-25 s,
with the whole status section (quick statuses, input, Clear) locked and
the Save spinner going.

- One shared presence queue (heartbeat + status save), newest write wins:
  changes made while a write waits out a 429 replace it, so only the latest
  is sent, as soon as the server allows.
- The status UI no longer waits on the server: the status applies locally,
  Save switches off (it compared with the server echo, so it stayed lit
  and invited a second, guaranteed-429 write), the controls stay usable,
  and a short note shows until the write lands.
- The heartbeat skips writes the server already reflects (e.g. "online"
  at startup), so it stops spending the budget for nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared scheduled this pull request to auto merge when all checks succeed 2026-10-25 20:14:56 -04:00
jared merged commit e4591f669b into lotus 2026-10-25 21:21:10 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: LotusGuild/cinny#469