A full group can send ~5 messages an hour before being rate limited #54

Closed
opened 2026-09-05 02:01:02 +00:00 by paulhitt · 1 comment
paulhitt commented 2026-09-05 02:01:02 +00:00 (Migrated from gitlab.com)

A full group can send about five messages an hour before the relay 429s it.

Found by an architecture audit, 2026-09-04, and the arithmetic verified by hand against the code.

The mechanism

Routes.swift:599 charges the sender's bucket one unit per envelope, not per message:

let ok = await limits.consume(
    "send:from:\(caller.userID)", amount: input.messages.count,
    limit: config.sendRatePerHour, now: now)

Groups are pairwise fan-out — one envelope per recipient device — and Group.maxMembers = 32 (Models.swift:184). FN_SEND_RATE defaults to 600/hour (Config.swift:332).

So one message to a full group costs 31 members × 2–5 devices = 62 to 155 units. Against 600:

Group size Devices each Envelopes/message Messages before 429
8 3 21 ~28/hour
16 3 45 ~13/hour
32 2 62 ~9/hour
32 5 155 ~4/hour

Why this matters more than it looks

🔴 The limit punishes the feature the product advertises. A 32-member group is supported, documented, and capped deliberately at 32 — and a person in one hits the wall in normal conversation, not abuse. Four messages an hour is not a rate limit; it is the group not working.

⚠️ It will read as a bug in the wrong place. The user sees "Too many messages sent recently" after typing three lines. Nobody will guess the cause is how many phones their friends own — including us, next time it is reported.

⚠️ The 1:1 case is fine, which is exactly why this survived: every manual test of the send path passes.

The fix, and the choice inside it

The limiter is measuring the wrong thing. Fan-out is a property of the recipients, not of how much the sender is talking, so charging per envelope makes the price of speaking depend on other people's hardware.

  1. Charge per logical message on the authenticated path — amount: 1 for a group send. Simplest, and it matches what the limit is for. The per-recipient and per-IP axes on the sealed path (Routes.swift:540) already bound the abuse case separately.
  2. Scale the limit by device count — keep per-envelope accounting but raise the ceiling. Preserves the storage-cost signal; needs a number nobody can derive from first principles.
  3. Charge per envelope but with a separate, larger group budget.

Prefer 1.

Done looks like: a test that sends N messages to a full 32-member group with several devices each and asserts none is refused — the arithmetic above, encoded, so the next change to either number fails loudly.

**A full group can send about five messages an hour before the relay 429s it.** Found by an architecture audit, 2026-09-04, and the arithmetic verified by hand against the code. ## The mechanism `Routes.swift:599` charges the sender's bucket **one unit per envelope**, not per message: ```swift let ok = await limits.consume( "send:from:\(caller.userID)", amount: input.messages.count, limit: config.sendRatePerHour, now: now) ``` Groups are pairwise fan-out — one envelope per recipient *device* — and `Group.maxMembers = 32` (`Models.swift:184`). `FN_SEND_RATE` defaults to 600/hour (`Config.swift:332`). So one message to a full group costs 31 members × 2–5 devices = **62 to 155 units**. Against 600: | Group size | Devices each | Envelopes/message | Messages before 429 | |---|---|---|---| | 8 | 3 | 21 | ~28/hour | | 16 | 3 | 45 | ~13/hour | | 32 | 2 | 62 | ~9/hour | | 32 | 5 | 155 | **~4/hour** | ## Why this matters more than it looks 🔴 **The limit punishes the feature the product advertises.** A 32-member group is supported, documented, and capped deliberately at 32 — and a person in one hits the wall in normal conversation, not abuse. Four messages an hour is not a rate limit; it is the group not working. ⚠️ **It will read as a bug in the wrong place.** The user sees "Too many messages sent recently" after typing three lines. Nobody will guess the cause is how many phones their friends own — including us, next time it is reported. ⚠️ The 1:1 case is fine, which is exactly why this survived: every manual test of the send path passes. ## The fix, and the choice inside it The limiter is measuring the wrong thing. Fan-out is a property of the *recipients*, not of how much the sender is talking, so charging per envelope makes the price of speaking depend on other people's hardware. 1. **Charge per logical message on the authenticated path** — `amount: 1` for a group send. Simplest, and it matches what the limit is for. The per-recipient and per-IP axes on the sealed path (`Routes.swift:540`) already bound the abuse case separately. 2. **Scale the limit by device count** — keep per-envelope accounting but raise the ceiling. Preserves the storage-cost signal; needs a number nobody can derive from first principles. 3. Charge per envelope but with a separate, larger group budget. Prefer 1. **Done looks like:** a test that sends N messages to a full 32-member group with several devices each and asserts none is refused — the arithmetic above, encoded, so the next change to either number fails loudly.
paulhitt commented 2026-09-05 19:40:54 +00:00 (Migrated from gitlab.com)

Fixed in a46f739 and deployed. The rate limit charged one unit per recipient device rather than one per message, so the bigger the group the sooner the sender was cut off — about five messages an hour for a full one. One-to-one chats were never affected.

Shipped in today's relay deploy and written up on /releases:

"A group of any size was quietly limited to about five messages an hour. The limit counted one unit per recipient device rather than one per message, so the bigger your group the sooner you were cut off."

⚠️ Operators who lowered FN_SEND_RATE to compensate for group fan-out can raise it back — that note is in the release's self-hosting section.

Fixed in `a46f739` and **deployed**. The rate limit charged one unit per recipient *device* rather than one per message, so the bigger the group the sooner the sender was cut off — about five messages an hour for a full one. One-to-one chats were never affected. Shipped in today's relay deploy and written up on /releases: > "A group of any size was quietly limited to about five messages an hour. The limit counted one unit per recipient device rather than one per message, so the bigger your group the sooner you were cut off." ⚠️ Operators who lowered `FN_SEND_RATE` to compensate for group fan-out can raise it back — that note is in the release's self-hosting section.
paulhitt (Migrated from gitlab.com) closed this issue 2026-09-05 19:40:54 +00:00
Sign in to join this conversation.