Rate Limiting System โ Complete
Status: Production Ready | Performance: <1ms per check | Tests: 8/8 passing
1. Overview
Multi-tier token bucket rate limiting with Redis backend. Protects API from abuse with graceful degradation and automatic retry.
Key Features
- โ
Global, tenant, user, and method-specific limits
- โ
Token bucket algorithm with Redis Lua scripts (atomic)
- โ
Automatic retry with exponential backoff (frontend)
- โ
User-friendly toast notifications
- โ
Fails open on Redis errors (graceful degradation)
- โ
Performance: ~187ยตs per check
2. Architecture
โโโโโโโโโโโโโโโ
โ Client โ
โโโโโโโโฌโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend Layer โ
โ โข Rate limit handler utilities โ
โ โข Toast notifications โ
โ โข Automatic retry (3x, backoff) โ
โโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ HTTP/gRPC
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Backend gRPC Server โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Rate Limit Interceptor โ โ
โ โ (runs before auth) โ โ
โ โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Multi-Tier Check โ โ
โ โ 1. Global (10k/min) โ โ
โ โ 2. Tenant (1k/min) โ โ
โ โ 3. User (200/min) โ โ
โ โ 4. Method (varies) โ โ
โ โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Auth Interceptor โ โ
โ โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Business Logic Handler โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโ
โ Redis โ
โ (Lua script)
โโโโโโโโโโโโโโโ
3. Backend Implementation
Core Files
| File |
Purpose |
Lines |
backend/internal/middleware/ratelimit.go |
Token bucket algorithm, Redis operations |
281 |
backend/internal/middleware/ratelimit_test.go |
8 tests + benchmark |
450 |
backend/cmd/server/main.go |
gRPC server integration |
Modified |
Token Bucket Algorithm
// Redis Lua script (atomic operation)
tokens = redis.get(key)
elapsed = now - last_update
tokens = min(capacity, tokens + elapsed * rate)
if tokens >= 1:
tokens -= 1
redis.set(key, tokens, last_update=now)
return ALLOW
else:
return DENY
Backend Code Example
// Go - Initialize rate limiter
rateLimiter := middleware.NewRateLimiter(redisClient, &middleware.RateLimitConfig{
GlobalRequestsPerMinute: 10000,
GlobalBurstSize: 100,
TenantRequestsPerMinute: 1000,
TenantBurstSize: 50,
UserRequestsPerMinute: 200,
UserBurstSize: 20,
})
// Add to gRPC interceptor chain
grpcSrv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
rateLimiter.Unary(), // Rate limit FIRST
authInterceptor.Unary(), // Auth second
),
)
4. Frontend Integration
Core Files
| File |
Purpose |
frontend/src/lib/rate-limit-handler.ts |
Detection, retry, error parsing utilities |
frontend/src/lib/connect-rpc-browser.ts |
gRPC interceptor with rate limit logging |
frontend/src/lib/api/imports.ts |
REST API wrapper with automatic retry |
frontend/src/components/ui/rate-limit-toast.tsx |
Toast notification with countdown |
frontend/src/hooks/use-imports.ts |
React hooks with rate limit state |
Automatic Retry
// TypeScript - Wrap API call with retry
import { withRateLimitRetry } from '@/lib/rate-limit-handler'
const result = await withRateLimitRetry(async () => {
return await fetch('/api/some-endpoint', { method: 'POST' })
})
// Retries: 1s โ 2s โ 4s (exponential backoff)
Error Detection
// TypeScript - Check if error is rate limit
import { isRateLimitError, parseRateLimitError } from '@/lib/rate-limit-handler'
try {
await apiCall()
} catch (error) {
if (isRateLimitError(error)) {
const parsed = parseRateLimitError(error)
console.log(parsed.message) // "Global rate limit exceeded..."
console.log(parsed.retryAfter) // 5 (seconds)
}
}
5. Rate Limits & Configuration
Default Limits
| Tier |
Requests/Min |
Burst |
Redis Key |
| Global |
10,000 |
100 |
ratelimit:global |
| Tenant |
1,000 |
50 |
ratelimit:tenant:{uuid} |
| User |
200 |
20 |
ratelimit:user:{uuid} |
Method-Specific Limits
| Method |
Requests/Min |
Burst |
Reason |
| AI Chat |
100 |
10 |
Expensive LLM calls |
| File Upload/Extract |
200 |
20 |
Large file processing |
| Bulk Operations |
50 |
5 |
Heavy database writes |
๐ก Enforcement Order: Global โ Tenant โ User โ Method-specific. All must pass.
6. Usage Examples
Example 1: React Hook with Toast
import { useImportUpload } from '@/hooks/use-imports'
import { RateLimitToast, useRateLimitToast } from '@/components/ui/rate-limit-toast'
import { getRateLimitMessage } from '@/lib/rate-limit-handler'
function UploadPage() {
const { uploadFile, isUploading, isRateLimited, retryAfter } = useImportUpload()
const { toast, showToast, hideToast } = useRateLimitToast()
const handleUpload = async (file: File) => {
try {
await uploadFile({ tenantId, file })
// Success!
} catch (err) {
if (isRateLimited) {
showToast(getRateLimitMessage(err), retryAfter)
}
}
}
return (
<>
<button onClick={handleUpload}>Upload</button>
{toast.isVisible && <RateLimitToast {...toast} onClose={hideToast} />}
</>
)
}
Example 2: Direct API Call
import { ImportsAPI } from '@/lib/api/imports'
// Automatic retry built-in
try {
await ImportsAPI.executeImport(tenantId, sessionId)
} catch (error) {
// Already retried 3x, still failed
console.error('Import failed after retries:', error)
}
Example 3: gRPC Call with Connect-RPC
import { accountingClientBrowser } from '@/lib/connect-rpc-browser'
try {
const response = await accountingClientBrowser.getDashboardStats({ tenantId })
} catch (error) {
// Rate limit logged by interceptor automatically
// Handle error or show toast
}
7. Testing
Run Backend Tests
# All tests (requires Redis running)
cd backend
go test ./internal/middleware -v
# Specific test
go test ./internal/middleware -v -run TestRateLimiter_GlobalLimit
# Benchmark
go test ./internal/middleware -bench=. -benchtime=3s
# Expected output:
# BenchmarkRateLimiter_CheckLimit-10 18302 186925 ns/op
# PASS
Manual Testing - Trigger Rate Limit
# Make rapid requests to exceed burst
for i in {1..15}; do
curl -X POST http://localhost:3000/api/imports/upload \
-H "Authorization: Bearer $TOKEN"
done
# Expected:
# Requests 1-10: โ
200 OK (burst allowed)
# Requests 11-15: โ ๏ธ 429 Too Many Requests
# Frontend: Shows toast "Rate limit reached, retrying in 3s..."
Test Results
| Test |
Status |
| TestRateLimiter_GlobalLimit | โ
PASS |
| TestRateLimiter_TenantLimit | โ
PASS |
| TestRateLimiter_TenantIsolation | โ
PASS |
| TestRateLimiter_MethodSpecificLimit | โ
PASS |
| TestRateLimiter_UserLimit | โ
PASS |
| TestRateLimiter_ResetLimit | โ
PASS |
| TestRateLimiter_GetRateLimitInfo | โ
PASS |
| TestRateLimiter_TokenRefill | โ
PASS |
8. Monitoring & Admin
Admin Functions
// Go - Reset specific limit
err := rateLimiter.ResetLimit(ctx, "ratelimit:global")
// Reset tenant limit
err := rateLimiter.ResetTenantLimit(ctx, tenantID)
// Reset user limit
err := rateLimiter.ResetUserLimit(ctx, userID)
// Check remaining tokens
tokens, err := rateLimiter.GetRateLimitInfo(ctx, "ratelimit:tenant:uuid")
fmt.Printf("Remaining tokens: %.2f\n", tokens)
Redis Commands
# Check all rate limit keys
redis-cli --scan --pattern "ratelimit:*"
# View specific key
redis-cli HGETALL ratelimit:global
# Delete all rate limit keys (reset all)
redis-cli EVAL "return redis.call('del', unpack(redis.call('keys', 'ratelimit:*')))" 0
Monitoring Metrics
| Metric |
How to Monitor |
| Rate limit hits |
Count gRPC errors with code ResourceExhausted (8) |
| Redis performance |
Monitor Lua script execution time (<1ms expected) |
| Retry attempts |
Frontend console logs: [Rate Limit] Attempt X/3 |
| Token bucket state |
Use GetRateLimitInfo() to check tokens |
Performance Characteristics
- Latency: ~187ยตs per rate limit check
- Throughput: 100k+ checks/sec per Redis instance
- Memory: ~200 bytes per unique key (scales to millions)
- Redis Load: 1 EVALSHA + 1 EXPIRE per request
๐ฏ Production Notes:
- Rate limiter runs before authentication (protects auth endpoints)
- Fails open on Redis errors (availability over strict limits)
- Uses Redis pipelining for efficiency
- Keys auto-expire after 2 minutes (cleanup)
Rate Limiting System | Complete Implementation | Nov 2025