ADR-0039: Robust Unified Rate Limiting Architecture¶
| Field | Value |
|---|---|
| Status | Accepted (Implemented) |
| Date | 2026-08-16 |
| Authors | Spector Maintainers & Architecture Working Group |
| Deciders | Spector Technical Steering Committee (TSC) |
| Supersedes | None |
| Superseded By | None |
| Last Verified | 2026-09-16 (Verified against main) |
1. Context¶
Spector has evolved into an enterprise-grade cognitive memory and autonomous agent runtime. As Spector exposes public and internal surfaces (REST APIs, Model Context Protocol endpoints, Server-Sent Events, 20+ Camel connectors, 10 messaging channel adapters, and outbound LLM generation/embedding pipelines), it faces severe stability, cost, and availability risks without comprehensive traffic shaping and rate limiting:
2. Problem Statement¶
- Inbound API Overload & DoS: Unauthenticated or rogue clients can flood
/api/**or/mcpendpoints, consuming CPU/memory and starving legitimate users. - LLM Cost & Quota Exhaustion: Outbound calls to cloud LLMs (OpenAI, Anthropic, Gemini, Groq) have strict Requests-Per-Minute (RPM) and Tokens-Per-Minute (TPM) limits. Unthrottled agent loops cause HTTP 429 bans, budget blowouts, and cascading failure across cognitive pipelines.
- Connector & Third-Party API Banning: Unregulated polling or webhooks in Apache Camel connectors risk hitting rate limits on external SaaS providers (Salesforce, GitHub, Jira, Confluence, Slack, Google Drive) or overwhelming downstream ingestion pipelines.
- Messaging Flood: Chat channel webhooks (Discord, Telegram, WhatsApp, Slack) can be flooded by spam bots or multi-user chatter, overloading cognition threads.
- Cloud Deployment Scalability: Standalone in-memory rate limiting is insufficient for multi-replica Kubernetes clusters unless backed by a pluggable distributed state store (e.g., Redis).
GitHub Issue #120 originally proposed basic API rate limiting with Bucket4j. This ADR establishes a Unified 4-Pillar Rate Limiting & Resilience Architecture covering all layers of Spector, designed for zero-downtime dynamic configuration and seamless transition from standalone to distributed cloud environments.
3. Decision Drivers¶
- Comprehensive Traffic Shaping: Protect inbound APIs, outbound LLM tokens, Camel connectors, and chat webhooks from saturation.
- Bucket4j & Resilience4j Core: Standardize on high-throughput token-bucket algorithms with atomic concurrency.
- Hierarchical Key Resolution: Isolate limits per API key, tenant, IP address, and model provider.
- Pluggable Distributed Storage: In-memory Caffeine storage for standalone deployments with zero-code switch to Redis for multi-replica clusters.
4. Considered Options¶
Option 1: Ad-Hoc In-Memory Guava Rate Limiters¶
- Description: Add
RateLimiter.create()directly inside individual controllers and provider clients. - Advantages: Fast to prototype.
- Disadvantages: Fragile, unconfigurable, no cluster synchronization, missing token/RPM differentiation.
Option 2: Infrastructure / API Gateway Exclusively (Kong / Envoy)¶
- Description: Delegate rate limiting entirely to external ingress proxies.
- Advantages: Offloads JVM compute.
- Disadvantages: Fails to protect outbound LLM API budgets, Camel ingestion polling, or internal agent execution loops.
Option 3: Unified 5-Pillar Architecture with Bucket4j & Resilience4j (Selected)¶
- Description: Standardize on a cohesive 5-pillar architecture covering inbound HTTP/SSE/MCP, outbound LLMs (RPM + TPM), Camel connectors, messaging webhooks, and pluggable Redis/Caffeine storage.
- Advantages: Complete protection across all surfaces; configurable via YAML; cluster-ready; Actuator metrics integration.
- Disadvantages: Requires maintaining multi-surface filter infrastructure.
5. Decision Outcome¶
graph TD
subgraph "Layer 1: Inbound API Surface"
Client["External Clients / MCP Agents / Web UI"] -->|HTTP / MCP / SSE| RLF["RateLimitFilter (Bucket4j + Security)"]
RLF -->|Allowed| Endpoints["REST Controllers / MCP / SSE / Actuator"]
RLF -->|Rejected| Resp429["HTTP 429 Too Many Requests + Retry-After"]
end
subgraph "Layer 2: Messaging Channels"
ChatUser["Chat Users / Webhooks"] --> CR["ChannelRouter"]
CR --> CRL["Inbound Message RateLimiter"]
CRL -->|Allowed| ChatService["ChatService / Cognition"]
CRL -->|Rejected| ChatThrottle["Polite Throttling Notice"]
CamelChannelAdapter -->|Outbound Pacing| OutboundThrottle["Outbound Channel Throttler"]
end
subgraph "Layer 3: Apache Camel Ingestion & Connectors"
Poller["S3 / DB / Jira / RSS / Mongo"] --> RouteThrottle["Camel Throttler EIP / Resilience4j"]
RouteThrottle --> IngestSink["SpectorIngestionSink"]
IngestSink --> CoreMem["Spector Memory Kernel"]
end
subgraph "Layer 4: Outbound LLM Providers"
ChatService & CoreMem --> ResilientLLM["ResilientRateLimitedLlmProvider"]
ResilientLLM --> RPMBucket["RPM Token Bucket"]
ResilientLLM --> TPMBucket["TPM Token Bucket (Pre-reserve & Post-settle)"]
ResilientLLM --> Bulkhead["Concurrency Semaphore"]
ResilientLLM -->|Provider 429 / Exhaustion| Failover["Exponential Backoff + Provider Fallback"]
ResilientLLM --> UpstreamLLM["OpenAI / Anthropic / Gemini / Ollama"]
end
subgraph "Pluggable Storage & Cloud Management"
BackendStore["RateLimitBackend SPI"]
BackendStore --> Caffeine["In-Memory Caffeine (Default)"]
BackendStore --> RedisStore["Distributed Redis (Cloud / K8s)"]
ConfigMgmt["Dynamic Config & Actuator /actuator/ratelimits"] --> BackendStore
end
RLF -.-> BackendStore
CRL -.-> BackendStore
ResilientLLM -.-> BackendStore 3. Pillar 1: Inbound API Rate Limiting¶
3.1 Key Resolution Hierarchy¶
A polymorphic key resolution strategy identifies callers with fallback tiers:
flowchart TD
Req["Incoming HTTP Request"] --> HasAPIKey{"Has X-API-Key or Bearer API Key?"}
HasAPIKey -->|Yes| KeyBucket["Key: 'apikey:' + Hash(Key)"]
HasAPIKey -->|No| HasAuth{"Is JWT User Authenticated?"}
HasAuth -->|Yes| UserBucket["Key: 'user:' + SubjectID"]
HasAuth -->|No| HasTenant{"Has X-Tenant-ID Header?"}
HasTenant -->|Yes| TenantBucket["Key: 'tenant:' + TenantID"]
HasTenant -->|No| HasIP{"Extract Client IP"}
HasIP -->|Yes| IPBucket["Key: 'ip:' + ValidatedIP"]
KeyBucket --> TierSelect["Select Tier Policy: System / Premium / Standard / Anonymous"]
UserBucket --> TierSelect
TenantBucket --> TierSelect
IPBucket --> AnonymousTier["Anonymous Tier Policy"] 3.2 Endpoint Categorization & Custom Policies¶
Different routes require different rate-limiting envelopes:
- Public / Static / Probes:
/actuator/health,/actuator/info,/index.html,/assets/**→ Bypassed / Excluded. - Auth Endpoints:
/api/v1/auth/login,/api/v1/auth/token→ Strict (10 req/min, burst 20) to prevent credential brute forcing. - Standard REST & MCP:
/api/v1/memories/**,/mcp,/api/v1/query→ Standard Tier (100 req/s, burst 200). - Heavy Operations:
/api/v1/admin/consolidate, batch embeddings → Throttled (5 req/min, burst 10).
3.3 HTTP 429 Response Protocol¶
When a limit is violated:
- HTTP Status:
429 Too Many Requests - Response Headers:
Retry-After: <seconds>(calculated fromConsumptionProbe.getNanosToWaitForRefill())X-RateLimit-Limit: <capacity>X-RateLimit-Remaining: 0X-RateLimit-Reset: <epoch-seconds>
- Response Body: RFC 7807 Problem Details
4. Pillar 2: Outbound LLM Provider Rate Limiting & Resilience¶
4.1 Dual-Dimension Token Bucket (RPM + TPM)¶
Cloud LLMs enforce both Request rates and Token consumption rates:
- RPM Bucket: Consumes 1 unit per request.
- TPM Bucket (Tokens Per Minute):
- Pre-flight Estimation: Heuristic tokenizer calculates
estimatedTokens = (prompt.length() / 4) + options.maxTokens(). - Token Reservation: Probe bucket for
estimatedTokens. If insufficient tokens remain, queue request up toqueueTimeoutMs(e.g. 5000ms). - Post-call Reconciliation: Upon receiving
LlmResponse, extract actualpromptTokens + completionTokens. Credit over-reserved tokens or debit under-reserved tokens against the bucket.
- Pre-flight Estimation: Heuristic tokenizer calculates
4.2 Concurrency Bulkhead & Backoff Failover¶
- Bulkhead:
Semaphoreper provider limiting concurrent in-flight HTTP connections (e.g., max 10 concurrent requests to Claude 3.5 Sonnet). - Retry with Jitter: On upstream HTTP 429 or transient 503, parse upstream
Retry-Afterheader and execute full-jitter exponential backoff (up to 3 retries). - Dynamic Provider Failover: If primary provider quota is exhausted, fail over to registered secondary provider (e.g.,
openai→anthropic→ localollama).
5. Pillar 3: Apache Camel Connector & Ingestion Throttling¶
5.1 Route Template Parameters¶
All Camel route templates in synapse/spector-connector are upgraded with declarative throttling parameters:
- routeTemplate:
id: "rest-api-poll"
parameters:
- name: throttleRequests
defaultValue: "60"
- name: throttlePeriodMs
defaultValue: "60000"
- name: throttleAsyncDelayed
defaultValue: "true"
from:
uri: "timer:{{routeId}}?period={{pollIntervalMs}}"
steps:
- throttle:
expression:
simple: "{{throttleRequests}}"
timePeriodMillis: "{{throttlePeriodMs}}"
asyncDelayed: "{{throttleAsyncDelayed}}"
- to: "{{url}}"
- process:
ref: "spectorIngestionSink"
5.2 Ingestion Backpressure & Circuit Breakers¶
- Camel
Resilience4jConfigurationapplied to routes with error thresholds. - When
SpectorIngestionSinkdetects kernel indexing queue pressure, it signals route controllers to temporarily suspend polling rather than risking out-of-memory errors.
6. Pillar 4: Multi-Channel Messaging Throttling¶
6.1 Inbound User Anti-Flood¶
- Implemented in
ChannelRouter:- Token bucket keyed by
(channelId + ":" + senderId)(e.g.,slack:U123456). - Limits message processing to 30 msgs/min per user with burst of 5.
- Exceeding limit triggers an in-channel notice: "You are sending messages faster than I can process. Please wait a moment." without invoking LLM cognition.
- Token bucket keyed by
6.2 Outbound Platform Pacing¶
- Platform rate limits enforced in
CamelChannelAdapter:- Telegram: 30 msgs/s global, 1 msg/s per specific chat.
- Slack: 1 msg/s per webhook / bot token.
- Discord: 50 msgs/s global, 5 msgs/s per channel.
7. Pillar 5: Cloud-Readiness & Pluggable Storage¶
7.1 Multi-Backend Storage SPI¶
public interface RateLimitStateStore {
Bucket resolveBucket(String key, RateLimitPolicy policy);
void resetBucket(String key);
Map<String, BucketStats> activeBuckets();
}
Implementations:
-
CaffeineRateLimitStateStore(In-Memory Default):- Uses
Bucket4jwith localCaffeinecache with TTL eviction (e.g. expire buckets after 10 minutes of inactivity). - Zero external dependency, sub-microsecond latency, ideal for single-node deployments.
- Uses
-
RedisRateLimitStateStore(Distributed Cloud):- Uses
bucket4j-redis(Lettuce / Redisson) with atomic Redis EVAL scripts. - Enables multiple Spector Synapse nodes behind AWS ALB / Kubernetes Ingress to share atomic rate limits.
- Uses
8. Configuration Specification¶
spector:
resilience:
rate-limiting:
enabled: true
backend: in-memory # [in-memory, redis, hazelcast]
redis:
uri: redis://localhost:6379/0
default-tier: standard
tiers:
anonymous:
requests-per-second: 10
burst-capacity: 20
standard:
requests-per-second: 100
burst-capacity: 200
premium:
requests-per-second: 500
burst-capacity: 1000
system:
requests-per-second: 2000
burst-capacity: 5000
endpoints:
- path-pattern: "/api/v1/auth/**"
requests-per-minute: 30
burst-capacity: 10
- path-pattern: "/api/v1/memory/consolidate"
requests-per-minute: 20
burst-capacity: 5
llm:
enabled: true
queue-timeout-ms: 5000
max-retries: 3
default-policy:
requests-per-minute: 60
tokens-per-minute: 100000
max-concurrent-calls: 10
providers:
openai:
requests-per-minute: 500
tokens-per-minute: 200000
max-concurrent-calls: 20
anthropic:
requests-per-minute: 300
tokens-per-minute: 150000
max-concurrent-calls: 15
ollama:
requests-per-minute: 60
tokens-per-minute: 50000
max-concurrent-calls: 2
channels:
inbound-user-rpm: 30
inbound-user-burst: 5
connectors:
default-throttle-requests: 100
default-throttle-period-ms: 60000
Consequences¶
- Positive: Complete defense against DoS, LLM quota exhaustion, upstream SaaS banning, and webhook floods; transparent Actuator metrics.
- Negative / Trade-offs: Minor latency overhead (~10–20µs per HTTP request) for atomic token bucket deduction.
6. Pros and Cons of the Options¶
| Alternative | Pros | Cons |
|---|---|---|
| Option 1: Ad-hoc Guava | Quick prototype | Uncoordinated, non-clusterable, lacks TPM limits |
| Option 2: Gateway-Only | Offloads JVM | Blind to outbound LLM costs, connector polling, internal loops |
| Option 3: Unified 5-Pillars (Selected) | Complete 360-degree protection, cloud-ready, Actuator metrics | Small filter overhead on inbound requests |
7. Implementation Plan¶
| Phase | Description | Key Deliverables |
|---|---|---|
| Phase 1 | Core API Rate Limiting & Bucket4j | RateLimitFilter, RateLimitProperties, Caffeine bucket store, 429 handler, SecurityConfig wiring |
| Phase 2 | Outbound LLM Dual-Dimension Rate Limiting | ResilientRateLimitedLlmProvider, RPM/TPM token reservation, concurrency bulkhead, backoff |
| Phase 3 | Camel Connector & Messaging Channel Throttling | Route template throttling EIPs, ChannelRouter anti-flood, outbound pacing |
| Phase 4 | Cloud-Ready Redis Store & Actuator Management | Redis state store adapter, /actuator/ratelimits endpoint, Micrometer metrics |
8. Code Reference & Verification¶
9.1 Micrometer Metrics¶
spector.ratelimit.requests.total{tier="...", key_type="...", status="allowed|rejected"}spector.ratelimit.tokens.remaining{tier="..."}spector.ratelimit.llm.tpm.usage{provider="..."}spector.ratelimit.llm.rpm.usage{provider="..."}spector.ratelimit.wait_time.seconds
9.2 Management Endpoints¶
GET /actuator/ratelimits: Returns status of all rate limiting policies, backend type, and summary statistics.POST /actuator/ratelimits/reset?key=<key>: Allows admins to clear a throttled IP, user, or API key immediately.
Code Reference & Verification Gate¶
- Primary Module(s):
synapse/spector-synapse,synapse/spector-connector,memory/spector-providers - Key Packages:
com.spectrayan.spector.synapse.ratelimit,com.spectrayan.spector.connector.core - Classes:
RateLimitFilter.java,RateLimitProperties.java,ConnectorExecutionAuditNotifier.java - Verification Tests:
RateLimitFilterTest.java,ConnectorExecutionAuditNotifierTest.java