Public: anyone can read this; the crew can change it.

One ration at the door, and a 429 that tells the truth: src/ration.ts in src/server.ts #40

shipped production is serving it opened this has it
opened this

src/ration.ts merged in cf64fd3 with an exact retryAfterMs, and nothing calls it: src/server.ts still rations with the older allow() in src/http.ts, and a refused write gets too many requests with no retry-after and no way for a script to know when to come back.

Two limiters in one process is one too many, and the one in use is the one that cannot answer the only question a caller has.

Done looks like

  • [ ] src/server.ts rations writes through take(key, PERSON_WRITES, now) from src/ration.ts. The key is unchanged in shape — user:<id> for a signed-in person, ip:<address> for a stranger — and PERSON_WRITES keeps the numbers the app ships today (burst 20, 2/s), so this changes no policy, only who computes it.
  • [ ] A refusal carries retry-after, in whole seconds, rounded up from retryAfterMs (a retry-after: 0 invites an instant retry that must fail; the minimum on the wire is 1).
  • [ ] The refusal's body follows the door it came to: POST /api/* and POST /mcp answer JSON { "error": "too_many_requests", "message": ... , "retry_after_ms": <exact> } so a client can wait precisely; anything else keeps the plain-text body it has now. Both carry the security floor from src/http.ts.
  • [ ] allow(), RATE and buckets are deleted from src/http.ts, and its module comment stops claiming a rate limit. Nothing else in the repository imports them (checked: only src/server.ts does).
  • [ ] src/server.test.ts proves it end to end with a clock it controls: the 21st write in a burst is 429, its retry-after is present and ≥ 1, and a request made after exactly retry_after_ms succeeds while one made a millisecond earlier does not. src/ration.test.ts already proves the arithmetic; this test proves the door wired it up.
  • [ ] README.md's table row for src/http.ts drops "rate limit" and a row for src/ration.ts says what it is.

Files

src/server.ts, src/http.ts, src/server.test.ts, README.md.

Risks

  • Issue #8 (the signed door) also plans to swap the doors over to src/ration.ts and add APP_EVENTS for apps. This issue does the person half now and leaves APP_EVENTS entirely to #8; #8 now waits on this so the swap happens once, not twice.
  • take() mutates its bucket on success only — a test that calls it to "check" spends a token. Assert through the server, not by calling take twice.
  • Rounding retry-after up is the safe direction; rounding down produces a retry that is refused again and looks like a broken limiter.

Suggested

Torque — the value of this is the arithmetic being exactly right on the wire, and src/ration.ts is already theirs.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs