Skip to content

Add RateLimiter middleware to Agent SDK#1766

Open
mvanhorn wants to merge 2 commits intoxmtp:mainfrom
mvanhorn:feat/agent-sdk-ratelimiter-middleware
Open

Add RateLimiter middleware to Agent SDK#1766
mvanhorn wants to merge 2 commits intoxmtp:mainfrom
mvanhorn:feat/agent-sdk-ratelimiter-middleware

Conversation

@mvanhorn
Copy link
Copy Markdown

@mvanhorn mvanhorn commented Mar 19, 2026

Summary

Adds a RateLimiter middleware 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.ts using express-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 class
  • sdks/agent-sdk/src/middleware/RateLimiter.test.ts - 15 unit tests

Modified:

  • sdks/agent-sdk/src/middleware/index.ts - Export the new middleware (also sorted alphabetically to match convention)

Usage:

import { Agent, RateLimiter } from "@xmtp/agent-sdk";

const limiter = new RateLimiter({
  maxMessages: 5,
  windowMs: 30_000,
  behavior: "reply",
  onRateLimit: (senderId) => console.log(`Rate limited: ${senderId}`),
});

const agent = await Agent.create(/* ... */);
agent.use(limiter.middleware());

Config options:

  • maxMessages / windowMs - sliding window parameters (defaults: 10 messages per 60s)
  • behavior: "drop" | "reply" - silently ignore or send a reply
  • replyText - custom message when rate limited
  • onRateLimit - callback for logging/metrics
  • reset() / resetSender(id) - manual state control

Implementation notes

  • Follows the same .middleware() return pattern as PerformanceMonitor and ActionWizard
  • Uses Map<string, number[]> keyed by senderInboxId, similar to ActionWizard's in-memory session store
  • Expired timestamps are cleaned on each check, so memory stays bounded
  • The middleware calls next() only when the sender is under their limit. In the reduceRight chain (Agent.ts line ~570), placing RateLimiter early 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.

✓ src/middleware/RateLimiter.test.ts (15 tests) 5ms

This contribution was developed with AI assistance (Claude Code).

Note

Add RateLimiter middleware to Agent SDK for per-sender message throttling

  • Adds a new RateLimiter middleware class that enforces per-sender message rate limits using a sliding window counter (default: 10 messages per 60s).
  • When a sender exceeds the limit, the middleware either drops the message silently (drop mode) or sends a single reply per window (reply mode) with a configurable message.
  • Supports an optional onRateLimit callback, plus reset() and resetSender() methods to clear window state.
  • Exports RateLimiter from the middleware barrel.

Macroscope summarized 2082e4a.

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.
@mvanhorn mvanhorn requested review from a team as code owners March 19, 2026 19:36
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 2082e4a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@xmtp/agent-sdk Minor

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

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 19, 2026

@mvanhorn is attempting to deploy a commit to the XMTP Labs Team on Vercel.

A member of the Team first needs to authorize it.

@macroscopeapp
Copy link
Copy Markdown

macroscopeapp Bot commented Mar 19, 2026

Approvability

Verdict: 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") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@mvanhorn
Copy link
Copy Markdown
Author

Fixed in 2082e4a:

  1. Reply is now sent once per window, not per blocked message. Subsequent blocked messages are silently dropped.
  2. Replaced the timestamp array with a { count, windowStart, replied } struct per sender. No more unbounded growth from inactive senders - the window resets cleanly on expiry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants