Conversation
Add per-sender message throttling middleware using a sliding-window counter. Configurable max messages, window duration, and behavior (silent drop or reply with custom text). Includes onRateLimit callback for observability and reset/resetSender methods for manual control.
🦋 Changeset detectedLatest commit: 2082e4a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@mvanhorn is attempting to deploy a commit to the XMTP Labs Team on Vercel. A member of the Team first needs to authorize it. |
ApprovabilityVerdict: Needs human review This PR adds a new RateLimiter middleware feature to the Agent SDK with 390 lines of new code. As a new capability introducing new user-facing behavior, and with all files owned by @xmtp/protocol-sdk and @xmtp/documentation (not the author), designated code owners should review these changes. You can customize Macroscope's approvability policy. Learn more. |
|
|
||
| this.#onRateLimit?.(senderId); | ||
|
|
||
| if (this.#behavior === "reply") { |
There was a problem hiding this comment.
I don't think it's a good idea to reply to a sender more than once per period. Otherwise you end up multiplying the DOS, since now you are sending one message for every rate limited message you receive.
| this.#onRateLimit = config.onRateLimit; | ||
| } | ||
|
|
||
| #isAllowed(senderInboxId: string): boolean { |
There was a problem hiding this comment.
If we were willing to give up some precision, and have the rate limit be calculated for every window period, we could store the limits much more efficiently as a simple count rather than an array of timestamps.
Storing the timestamps maybe isn't so bad on its own, but we don't clean up old timestamps unless another message comes in from the same sender. So if you have an agent that is receiving messages from many addresses memory usage will expand forever.
Address review feedback from @neekolas: 1. Reply only once per rate-limit window to avoid DOS amplification. Previously every blocked message triggered a reply, multiplying outbound traffic under attack. 2. Replace per-sender timestamp array with a simple { count, windowStart, replied } struct. Old timestamps were never cleaned unless the same sender sent another message, causing unbounded memory growth for agents receiving messages from many addresses.
|
Fixed in 2082e4a:
|
Summary
Adds a
RateLimitermiddleware to the Agent SDK for per-sender message throttling using a sliding-window counter. No external dependencies.Why this matters
Deployed XMTP agents have no built-in protection against message flooding. The consent system (docs) filters at the conversation level, but there is nothing for per-sender rate control within allowed conversations - particularly in group chats where any member can flood the agent.
Network-level rate limiting has also shown reliability issues (#1641 - 429 errors below documented thresholds), making agent-side throttling a useful defense layer.
HTTP-level rate limiting already exists in the codebase (
apps/xmtp.chat-api-service/src/middleware/rateLimit.tsusingexpress-rate-limit), but nothing equivalent exists for the Agent SDK's message middleware chain.Changes
New files:
sdks/agent-sdk/src/middleware/RateLimiter.ts- The middleware classsdks/agent-sdk/src/middleware/RateLimiter.test.ts- 15 unit testsModified:
sdks/agent-sdk/src/middleware/index.ts- Export the new middleware (also sorted alphabetically to match convention)Usage:
Config options:
maxMessages/windowMs- sliding window parameters (defaults: 10 messages per 60s)behavior: "drop" | "reply"- silently ignore or send a replyreplyText- custom message when rate limitedonRateLimit- callback for logging/metricsreset()/resetSender(id)- manual state controlImplementation notes
.middleware()return pattern asPerformanceMonitorandActionWizardMap<string, number[]>keyed bysenderInboxId, similar toActionWizard's in-memory session storenext()only when the sender is under their limit. In thereduceRightchain (Agent.tsline ~570), placingRateLimiterearly means rate-limited messages never reach downstream handlers.Testing
15 unit tests covering: allow/block thresholds, independent sender tracking, sliding window expiry, drop vs reply behavior, custom reply text, onRateLimit callback, reset/resetSender, and memory cleanup. All tests use
vi.useFakeTimers()for deterministic time control.This contribution was developed with AI assistance (Claude Code).
Note
Add
RateLimitermiddleware to Agent SDK for per-sender message throttlingRateLimitermiddleware class that enforces per-sender message rate limits using a sliding window counter (default: 10 messages per 60s).dropmode) or sends a single reply per window (replymode) with a configurable message.onRateLimitcallback, plusreset()andresetSender()methods to clear window state.RateLimiterfrom themiddlewarebarrel.Macroscope summarized 2082e4a.