fix: replace never-resetting byte counter with sliding window in rate limiter#3881
fix: replace never-resetting byte counter with sliding window in rate limiter#3881berkelmali wants to merge 1 commit intoopenfrontio:mainfrom
Conversation
The totalBytes counter never reset, so legitimate long-running clients would accumulate bytes over the entire game duration and eventually get false-kicked. Replace with a 60-second sliding window that evicts old events, keeping byte tracking accurate for sustained sessions.
WalkthroughClientMsgRateLimiter now enforces byte-rate limits using a 60-second rolling window instead of a static accumulator. Per-client byte events are tracked with timestamps, entries outside the window are evicted, and the total bytes cap is enforced against only the active window. ChangesRolling-Window Byte Limiting
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/server/ClientMsgRateLimiter.ts`:
- Around line 30-33: The loop that evicts old events uses
bucket.byteEvents.shift() which reindexes the array and causes CPU spikes under
high traffic; update the data structure in ClientMsgRateLimiter to avoid
shifting by implementing a head index or ring buffer for bucket.byteEvents (and
the similar occurrences around the code that manipulate bucket.byteEvents at the
other noted sites), removing uses of Array.shift() and instead incrementing a
head pointer and decrementing bucket.totalBytes when evicting, and ensure any
checks (length/emptiness and iteration) account for the head pointer so logic in
the functions/methods that reference bucket.byteEvents continues to work
correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 767180cc-9072-4d6b-a4e0-7858d47d8c51
📒 Files selected for processing (1)
src/server/ClientMsgRateLimiter.ts
| while (bucket.byteEvents.length > 0 && bucket.byteEvents[0].at < cutoff) { | ||
| const evicted = bucket.byteEvents.shift()!; | ||
| bucket.totalBytes -= evicted.bytes; | ||
| } |
There was a problem hiding this comment.
Array.shift() in the hot path can cause CPU spikes under spam traffic.
This loop reindexes the whole array on every eviction, so worst-case cost grows fast with many tiny messages. In a rate-limiter path, that can be used as a DoS vector.
Suggested local refactor (head index queue, no shift)
interface ClientBucket {
perSecond: RateLimiter;
perMinute: RateLimiter;
byteEvents: Array<{ at: number; bytes: number }>;
+ byteEventsHead: number;
totalBytes: number;
}
// in check()
- while (bucket.byteEvents.length > 0 && bucket.byteEvents[0].at < cutoff) {
- const evicted = bucket.byteEvents.shift()!;
+ while (
+ bucket.byteEventsHead < bucket.byteEvents.length &&
+ bucket.byteEvents[bucket.byteEventsHead].at < cutoff
+ ) {
+ const evicted = bucket.byteEvents[bucket.byteEventsHead++]!;
bucket.totalBytes -= evicted.bytes;
}
+ // periodic compact to keep memory bounded
+ if (bucket.byteEventsHead > 1024 && bucket.byteEventsHead * 2 > bucket.byteEvents.length) {
+ bucket.byteEvents = bucket.byteEvents.slice(bucket.byteEventsHead);
+ bucket.byteEventsHead = 0;
+ }
bucket.byteEvents.push({ at: now, bytes });
// in getOrCreate()
const bucket = {
perSecond: new RateLimiter({ tokensPerInterval: INTENTS_PER_SECOND, interval: "second" }),
perMinute: new RateLimiter({ tokensPerInterval: INTENTS_PER_MINUTE, interval: "minute" }),
byteEvents: [],
+ byteEventsHead: 0,
totalBytes: 0,
};Also applies to: 35-35, 74-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/ClientMsgRateLimiter.ts` around lines 30 - 33, The loop that
evicts old events uses bucket.byteEvents.shift() which reindexes the array and
causes CPU spikes under high traffic; update the data structure in
ClientMsgRateLimiter to avoid shifting by implementing a head index or ring
buffer for bucket.byteEvents (and the similar occurrences around the code that
manipulate bucket.byteEvents at the other noted sites), removing uses of
Array.shift() and instead incrementing a head pointer and decrementing
bucket.totalBytes when evicting, and ensure any checks (length/emptiness and
iteration) account for the head pointer so logic in the functions/methods that
reference bucket.byteEvents continues to work correctly.
Description:
The
totalBytescounter inClientMsgRateLimiternever resets. In long-running game sessions, legitimate clients accumulate bytes over the entire game duration and eventually get false-kicked when they exceed the 2MB threshold.This replaces the monotonically increasing counter with a 60-second sliding window. Old byte events are evicted as they age out, so only recent throughput is measured. A running
totalBytescounter is maintained for O(1) per-message checks.Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
barfires