Tune the rate limits so ordinary conversation never hits them #20

Open
opened 2026-09-01 23:09:49 +00:00 by paulhitt · 2 comments
paulhitt commented 2026-09-01 23:09:49 +00:00 (Migrated from gitlab.com)

Ask: tune the rate limits so ordinary conversation never hits them. Nobody
swapping messages back and forth should be told to wait an hour.

Current state — and it is not a considered state

Live on production:

FN_BUNDLE_RATE=2000        (code default: 120)
FN_BUNDLE_RATE_TARGET=600  (code default: 6)
FN_SEND_RATE=6000          (code default: 600)

🔴 Those values were set during debugging on 2026-09-01 and have not been reviewed
since.
They are "whatever stopped getting in the way", not "what a real client needs".
They should be derived from measured client behaviour and then written down with the
reasoning, in either direction — they may well be too high now.

The default that actually caused the problem

FN_BUNDLE_RATE_TARGET defaults to 6 per hour, applied per requester-target pair at
Routes.swift:273. On 2026-09-01 the owner hit "Too many attempts. Try again in about 49
minutes"
on Android during ordinary testing.

The subtlety worth designing around: /e2e/keys pops a one-time prekey, so the limit
guards a real resource rather than being arbitrary. But sendPlaintext already avoids it —
it calls listDevices (which consumes nothing) and only fetches full bundles when some
device has no session. So a healthy client should approach this limit rarely, and a client
that keeps hitting it is usually a symptom of something else: sessions not persisting,
ratchets being dropped, repeated re-establishment. The limit was doing its job as a
smoke alarm; the fire was elsewhere.

What tuning should actually cover

  1. Measure first. Instrument how often a normal client hits each limit across a real
    conversation, a group, a call, and a sign-in. Set the numbers from that.
  2. Separate the smoke alarm from the ceiling. A client hitting the bundle limit is
    worth an alert to us; it should probably not be worth blocking the user. Consider
    logging loudly and admitting the request, with the hard block much higher.
  3. Burst then sustain. A fixed hourly bucket punishes a legitimate burst — starting a
    group, or a first message to several new contacts — while a token bucket with a small
    burst allowance and a modest refill would not.
  4. Never let a limit break an established conversation. Sending to a contact with a
    live session should be effectively unlimited from the user's point of view; the
    scarce resource is prekeys, and an existing session needs none.
  5. The message people see. RelayException.RateLimited already carries Retry-After
    and Android turns it into "about N minutes", which is good. Check the iOS wording
    matches, and that it never appears for a case the user cannot act on.

Done looks like

Each limit has a written justification, a measured headroom against real client behaviour,
and a decision about whether exceeding it should warn us or stop the user. The .env values
either match the defaults or the divergence is explained.

⚠️ Freeze in effect — measure and propose, do not change the live values.

Related: #16 (security review) — abuse resistance is the other half of this trade.

**Ask:** tune the rate limits so ordinary conversation never hits them. Nobody swapping messages back and forth should be told to wait an hour. ## Current state — and it is not a considered state Live on production: ``` FN_BUNDLE_RATE=2000 (code default: 120) FN_BUNDLE_RATE_TARGET=600 (code default: 6) FN_SEND_RATE=6000 (code default: 600) ``` 🔴 **Those values were set during debugging on 2026-09-01 and have not been reviewed since.** They are "whatever stopped getting in the way", not "what a real client needs". They should be derived from measured client behaviour and then written down with the reasoning, in either direction — they may well be too high now. ## The default that actually caused the problem `FN_BUNDLE_RATE_TARGET` defaults to **6 per hour**, applied per requester-target pair at `Routes.swift:273`. On 2026-09-01 the owner hit *"Too many attempts. Try again in about 49 minutes"* on Android during ordinary testing. The subtlety worth designing around: `/e2e/keys` **pops a one-time prekey**, so the limit guards a real resource rather than being arbitrary. But `sendPlaintext` already avoids it — it calls `listDevices` (which consumes nothing) and only fetches full bundles when some device has no session. So a healthy client should approach this limit rarely, and a client that keeps hitting it is usually a symptom of something else: sessions not persisting, ratchets being dropped, repeated re-establishment. **The limit was doing its job as a smoke alarm; the fire was elsewhere.** ## What tuning should actually cover 1. **Measure first.** Instrument how often a normal client hits each limit across a real conversation, a group, a call, and a sign-in. Set the numbers from that. 2. **Separate the smoke alarm from the ceiling.** A client hitting the bundle limit is worth an alert to us; it should probably not be worth blocking the user. Consider logging loudly and admitting the request, with the hard block much higher. 3. **Burst then sustain.** A fixed hourly bucket punishes a legitimate burst — starting a group, or a first message to several new contacts — while a token bucket with a small burst allowance and a modest refill would not. 4. **Never let a limit break an established conversation.** Sending to a contact with a live session should be effectively unlimited from the user's point of view; the scarce resource is prekeys, and an existing session needs none. 5. **The message people see.** `RelayException.RateLimited` already carries `Retry-After` and Android turns it into "about N minutes", which is good. Check the iOS wording matches, and that it never appears for a case the user cannot act on. ## Done looks like Each limit has a written justification, a measured headroom against real client behaviour, and a decision about whether exceeding it should warn us or stop the user. The `.env` values either match the defaults or the divergence is explained. ⚠️ Freeze in effect — measure and propose, do not change the live values. Related: #16 (security review) — abuse resistance is the other half of this trade.
paulhitt commented 2026-09-01 23:11:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #21

mentioned in issue #21
paulhitt commented 2026-09-01 23:17:38 +00:00 (Migrated from gitlab.com)

mentioned in issue flamenet-messenger-ios#13

mentioned in issue flamenet-messenger-ios#13
Sign in to join this conversation.