Email Rate Limiter Plugin - Phase 6
Distributed rate limiting for email operations using the token bucket algorithm with Redis backend support.
Overview
The rate limiter enforces API quotas for email operations to prevent abuse and ensure fair usage:
- Sync operations: 100 requests per hour
- Send operations: 50 requests per hour
- Search operations: 500 requests per hour
Rate limits are tracked per account per operation type with automatic hourly resets and support for multi-tenant environments.
Features
Core Capabilities
- Token Bucket Algorithm: Smooth rate limiting with token refill mechanism
- Distributed Backend: Redis support for multi-instance deployments (with in-memory fallback)
- Per-Account Tracking: Separate quotas for each email account
- Per-Operation Quotas: Isolated limits for sync/send/search operations
- Multi-Tenant Support: Tenant-scoped rate limit isolation
- HTTP Headers: Standard rate limit headers in responses
- Graceful Degradation: Continues operating if Redis unavailable
Request Flow
1. Check token bucket state (from Redis or memory)
2. Refill tokens based on elapsed time since last refill
3. Check if bucket expired (hourly reset)
4. Attempt to consume tokens
5. Return remaining quota and reset time
6. If quota exceeded: Return retry-after header
Configuration
Required Parameters
{
operationType: 'sync' | 'send' | 'search', // Operation type
accountId: string, // UUID of email account
tenantId: string // UUID of tenant
}
Optional Parameters
{
tokensToConsume?: number, // Tokens to consume per request (default: 1)
customLimit?: number, // Override default quota per hour
resetWindowMs?: number, // Reset window in ms (default: 3600000)
redisUrl?: string // Redis connection URL
}
Usage Examples
Basic Sync Rate Limit Check
const node = {
nodeType: 'rate-limiter',
parameters: {
operationType: 'sync',
accountId: 'acc-123e4567-e89b-12d3-a456-426614174000',
tenantId: 'tenant-acme'
}
};
const result = await rateLimiterExecutor.execute(node, context, state);
if (result.status === 'success') {
const rateLimit = result.output.data;
if (rateLimit.allowed) {
// Proceed with sync operation
console.log(`Remaining quota: ${rateLimit.remainingTokens}/${rateLimit.bucketCapacity}`);
} else {
// Return HTTP 429 with retry-after header
return {
status: 429,
headers: rateLimit.headers,
body: { error: rateLimit.error }
};
}
}
Send Operation with Custom Token Cost
{
operationType: 'send',
accountId: 'acc-456',
tenantId: 'tenant-acme',
tokensToConsume: 5 // Large batch send = 5 tokens
}
Custom Quota Override
{
operationType: 'search',
accountId: 'acc-789',
tenantId: 'tenant-acme',
customLimit: 1000 // Override 500 default with 1000/hour
}
Custom Reset Window
{
operationType: 'sync',
accountId: 'acc-abc',
tenantId: 'tenant-acme',
resetWindowMs: 86400000 // 24-hour reset window instead of 1 hour
}
Response Format
Success Response (Allowed)
{
"allowed": true,
"tokensConsumed": 1,
"remainingTokens": 99,
"bucketCapacity": 100,
"refillRate": 100,
"resetAt": 1706179200000,
"resetIn": 3599,
"headers": {
"X-RateLimit-Limit": "100",
"X-RateLimit-Remaining": "99",
"X-RateLimit-Reset": "1706179200000",
"X-RateLimit-Reset-In": "3599"
}
}
Blocked Response (Quota Exceeded)
{
"allowed": false,
"tokensConsumed": 0,
"remainingTokens": 0,
"bucketCapacity": 50,
"refillRate": 50,
"resetAt": 1706179200000,
"resetIn": 1800,
"retryAfter": 1800,
"error": "Rate limit exceeded for send. Quota: 50 per 1 hour(s). Retry after 1800 seconds.",
"headers": {
"X-RateLimit-Limit": "50",
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": "1706179200000",
"X-RateLimit-Reset-In": "1800",
"Retry-After": "1800"
}
}
HTTP Response Headers
All rate limit responses include standard headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Total quota capacity |
X-RateLimit-Remaining |
Tokens remaining in current window |
X-RateLimit-Reset |
Unix timestamp when quota resets |
X-RateLimit-Reset-In |
Seconds until quota reset |
Retry-After |
Seconds to wait before retry (only if quota exceeded) |
Token Bucket Algorithm
The token bucket mechanism works as follows:
- Initialization: Bucket starts with full capacity (100 for sync, 50 for send, 500 for search)
- Refill: Tokens are added continuously based on refill rate
- Rate = Capacity / Reset Window
- Example: 100 tokens / 3600 seconds = 0.0278 tokens/second
- Consumption: Each request consumes 1+ tokens
- Blocking: If insufficient tokens, request is blocked with retry-after
- Reset: Bucket resets hourly (or custom interval)
Token Refill Example
For sync operation (100/hour):
- Refill rate: 100 tokens per 3600 seconds = 0.0278 tokens/second
- After 10 seconds: +0.278 tokens refilled
- After 60 seconds: +1.67 tokens refilled
- After 600 seconds: +16.7 tokens refilled
- After 3600 seconds: +100 tokens refilled (full reset)
Multi-Tenant Isolation
Rate limits are scoped by tenant to prevent one tenant from affecting others:
Key format: ratelimit:{tenantId}:{accountId}:{operationType}
Example:
ratelimit:tenant-acme:acc-123:sync
ratelimit:tenant-beta:acc-123:sync // Different quota, same account ID
Admin Operations
Reset Account Quota
Force reset of all quotas for an account:
await executor.resetQuota('acc-123', 'tenant-acme', 'sync');
Get Bucket Statistics
Retrieve current quota status for all operations:
const stats = await executor.getBucketStats('acc-123', 'tenant-acme');
// Returns:
{
sync: {
remaining: 75,
capacity: 100,
resetAt: 1706179200000,
quotaPercentage: 75
},
send: {
remaining: 45,
capacity: 50,
resetAt: 1706179200000,
quotaPercentage: 90
},
search: {
remaining: 500,
capacity: 500,
resetAt: 1706179200000,
quotaPercentage: 100
}
}
Redis Backend (Production)
In production, the plugin supports distributed rate limiting via Redis:
const config = {
operationType: 'sync',
accountId: 'acc-123',
tenantId: 'tenant-acme',
redisUrl: 'redis://redis.internal:6379'
};
Redis Operations
The plugin uses Redis commands:
# Get bucket state (TTL = reset window)
GET ratelimit:tenant:account:operation
# Set bucket state with expiration
SETEX ratelimit:tenant:account:operation {resetWindowMs} {state}
# Atomic token consumption (Lua script in production)
EVALSHA script_sha {resetWindowMs} {tokens}
Fallback Behavior
If Redis is unavailable:
- In-memory storage is used instead
- Rate limits still enforced per instance
- Does not provide cross-instance coordination
- Automatic recovery when Redis available
Testing
Comprehensive test suite covers:
Validation
- Required vs optional parameters
- Parameter type checking
- Valid operation types
- Quota and window constraints
Success Cases
- Requests within quota allowed
- Multiple operation type isolation
- Per-account isolation
- Per-tenant isolation
- HTTP headers populated correctly
Quota Exceeded
- Blocking when quota exhausted
- Retry-after header provided
- Partial quota consumption
Custom Configuration
- Custom quota limits
- Custom reset windows
- Custom token consumption
Token Refill
- Tokens refill over time
- Bucket reset on window expiration
Admin Operations
- Quota reset functionality
- Bucket statistics retrieval
Performance
- Concurrent request handling
- Execution duration tracking
Error Handling
- Invalid parameter detection
- Redis unavailable fallback
Run tests:
npm run test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
Quota Defaults
| Operation | Limit | Window |
|---|---|---|
| sync | 100 | 1 hour |
| send | 50 | 1 hour |
| search | 500 | 1 hour |
Integration with Workflow Engine
The rate limiter integrates as a workflow node:
{
"id": "node-rate-check",
"type": "rate-limiter",
"parameters": {
"operationType": "send",
"accountId": "{{ $json.accountId }}",
"tenantId": "{{ $json.tenantId }}"
},
"on": {
"success": ["node-send-email"],
"blocked": ["node-send-429-error"]
}
}
Workflow Integration Pattern
1. Email Send Request
↓
2. Rate Limiter Check
├─→ [Allowed] → Execute Send
└─→ [Blocked] → Return 429 + Retry-After
Performance Characteristics
- Single Request: <1ms (in-memory), 5-10ms (Redis)
- Concurrent Requests: Linear scaling with instance count
- Memory: ~100 bytes per tracked account per operation
- Redis: O(1) operations via atomic scripts
Security Considerations
- Tenant Isolation: Rate limits scoped by tenant ID
- Account Isolation: Separate quotas per account ID
- No Information Leakage: Same rate limit response for all operation types
- Distributed Safety: Redis-backed for multi-instance coordination
- Reset Window Randomization: Optional jitter to prevent thundering herd
Error Handling
The executor returns proper error codes:
RATE_LIMIT_ERROR: Configuration or execution errorblocked: Quota exceeded (HTTP 429)success: Request allowed
Monitoring and Observability
Metrics to Track
// Per account per operation
rate_limit_remaining_tokens
rate_limit_quota_percentage
rate_limit_requests_blocked
rate_limit_reset_window_duration
Log Entries
INFO: Rate limit check - operation=sync, account=acc-123, remaining=99/100
WARN: Rate limit quota low - operation=send, account=acc-456, remaining=2/50
ERROR: Rate limit exceeded - operation=search, account=acc-789, retry_after=1800
Future Enhancements
- Redis Cluster support
- Token burst allowance
- Per-IP rate limiting
- Quota sharing across accounts
- Adaptive quota adjustment
- Rate limit analytics dashboard
- Webhook notifications on quota warnings
License
Apache 2.0