Skip to content

Rate-limit responses: EWMA backoff is a half-life, an outage is reported as rate-limited, and the message bucket ignores message type #1006

Closed

Description

@chappie-daemon

Follow-up from the spike in #1004 (findings). Three wire-correctness defects in one path; each is small.

1. The EWMA backoff is a half-life, and is safe only by accident

rateLimiter.strategy: ewma is the default (resources/default-settings.yaml:221). Under EWMA period is a half-life (src/utils/ewma-rate-limiter.ts:18), so the true time until an event is accepted again is period · log2(R_new / rate). We send period (src/handlers/event-message-handler.ts:453). With rate = 6, period = 60000, R_new = 24 needs 120 s and we say 60 s.

It is not exploitable today because the hash is given a TTL of exactly period ms (src/utils/ewma-rate-limiter.ts:22), so a quiet client's state disappears anyway. That coupling is unstated and would break the moment someone lengthens the EXPIRE to keep state warm.

Fix: return R_new from the Lua script (it is already computed) instead of only 0/1, and compute Math.ceil(period * Math.log2(R_new / rate)) in the handler. Where R_new is unavailable, send no parameter — absence is explicitly allowed and MUST NOT be read as retry-immediately.

Tests that pin today's behaviour: test/unit/utils/ewma-rate-limiter.spec.ts (script returns 0/1), test/unit/handlers/event-message-handler.spec.ts:138 and :153.

2. A limiter outage is reported as rate-limited

src/handlers/event-message-handler.ts:445-447 catches a backend error and return true, so :97 yields no parameter and the client gets rate-limited: slow down without a value. Every connected client then backs off at once when the cache returns, and the spec's own example for this condition is error: could not connect to the database.

Fix: error: rate limiter unavailable on that branch. Fail-closed behaviour is unchanged — the event is still rejected.

Test to update: the fail-closed case in test/unit/handlers/event-message-handler.spec.ts.

3. The message bucket is not per message type

src/adapters/web-socket-adapter.ts:291 keys on ${client}:message:${period} and is consulted for every inbound message before parsing (:221), so a REQ can be refused because EVENTs filled the same bucket. The spec says the client should back off the same message type that triggered the limit, and honouring that can still be rejected.

Fix: add the message type to the key. It is built before the message is validated, so it needs the raw buffer or a reordering — that is why this is the maybe of the three. Bucket layout changes, so note it for anyone switching strategies mid-flight.

Tests: test/unit/adapters/web-socket-adapter.spec.ts:596, :641.

Activity

  1. added a commit that references this issue on Nov 2, 2026
    aa7564c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions