Skip to content

Replace unbounded ConcurrentHashMap with bounded Caffeine cache in RateLimitingFilter#54

Open
vaquarkhan wants to merge 1 commit intospring-ai-community:mainfrom
vaquarkhan:fix/ratelimit-memory-leak
Open

Replace unbounded ConcurrentHashMap with bounded Caffeine cache in RateLimitingFilter#54
vaquarkhan wants to merge 1 commit intospring-ai-community:mainfrom
vaquarkhan:fix/ratelimit-memory-leak

Conversation

@vaquarkhan
Copy link
Copy Markdown

Summary

RateLimitingFilter stores per-client rate limit buckets in a ConcurrentHashMap that never evicts entries. Every unique client IP permanently allocates a Bucket object (~200-500 bytes) that is never removed. Behind a load balancer, CDN, or under attack, this causes unbounded heap growth until OutOfMemoryError.

Changes

  • spring-ai-agentcore-runtime-starter/pom.xml — Added explicit caffeine dependency (already a transitive dependency via Spring Boot)
  • RateLimitingFilter.java — Replaced ConcurrentHashMap<String, Bucket> with a bounded Caffeine Cache<String, Bucket> (default: 100,000 max entries, 5-minute expiry after last access)

Before vs After

Scenario (500k unique IPs) Buckets in memory Heap used
Before (ConcurrentHashMap) 500,000 (never freed) ~165 MB and growing
After (Caffeine cache) 100,000 (capped) ~40 MB (stays flat)

Backward Compatibility

  • The existing 2-arg constructor delegates to the new 4-arg constructor with sensible defaults, so no changes needed in ThrottleConfiguration or existing tests
  • All 80 existing tests pass

Testing

  • mvn test -pl spring-ai-agentcore-runtime-starter — 80 tests, 0 failures
  • mvn spring-javaformat:apply — clean

Fixes #51

…in RateLimitingFilter

RateLimitingFilter stored per-client rate limit buckets in a ConcurrentHashMap
that never evicted entries. Every unique client IP permanently allocated a Bucket
object that was never removed, causing unbounded heap growth behind load balancers,
CDNs, or under attack until OutOfMemoryError.

Replace ConcurrentHashMap with a bounded Caffeine cache (default: 100,000 max
entries, 5-minute expiry after last access). Caffeine is already a transitive
dependency via Spring Boot; added as explicit dependency in runtime-starter pom.xml.

Fixes: RateLimitingFilter unbounded memory leak
@vaquarkhan vaquarkhan changed the title x: Replace unbounded ConcurrentHashMap with bounded Caffeine cache in RateLimitingFilter Replace unbounded ConcurrentHashMap with bounded Caffeine cache in RateLimitingFilter Apr 15, 2026
Copy link
Copy Markdown
Collaborator

@maschnetwork maschnetwork left a comment

Choose a reason for hiding this comment

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

Thanks for spotting this. A few minor comments. Can you also please add a test for bounded buckets and eviction?

private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
private static final long DEFAULT_MAX_BUCKETS = 100_000;

private static final Duration DEFAULT_BUCKET_EXPIRY = Duration.ofMinutes(5);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why 5 Minutes?

private static final String UTF_8 = "UTF-8";

private final ConcurrentHashMap<String, Bucket> buckets = new ConcurrentHashMap<>();
private static final long DEFAULT_MAX_BUCKETS = 100_000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we have a lower default here? We assume to have buckets per Agentcore Runtime session.

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.

[BUG] RateLimitingFilter: Unbounded memory leak ConcurrentHashMap never evicts per-client buckets

2 participants