API Rate Limiting and Abuse Prevention
Whistl's API handles sensitive financial data and must remain available even under attack. This technical guide explains rate limiting algorithms, abuse detection, DDoS protection, and how Whistl balances security with legitimate user access.
Why Rate Limiting Matters
APIs face multiple threats that rate limiting addresses:
- Resource exhaustion: Prevent single users from overwhelming servers
- Brute force attacks: Limit authentication attempts
- Scraping: Prevent data harvesting
- DDoS mitigation: Absorb volumetric attacks
- Cost control: Third-party API calls (Plaid, Oura) have costs
Rate Limiting Architecture
Whistl uses multi-layer rate limiting:
Rate Limit Layers
┌─────────────────────────────────────────────────┐ │ Edge Layer (Cloudflare) │ │ - Global rate limiting │ │ - DDoS protection │ │ - WAF rules │ ├─────────────────────────────────────────────────┤ │ API Gateway (Kong) │ │ - Per-user rate limits │ │ - Per-endpoint limits │ │ - Quota management │ ├─────────────────────────────────────────────────┤ │ Application Layer │ │ - Business logic limits │ │ - Abuse detection │ │ - Adaptive throttling │ └─────────────────────────────────────────────────┘
Token Bucket Algorithm
Whistl uses token bucket for flexible rate limiting:
How Token Bucket Works
class TokenBucket {
private var tokens: Double
private let capacity: Double
private let refillRate: Double // tokens per second
private var lastRefill: Date
init(capacity: Double, refillRate: Double) {
self.capacity = capacity
self.refillRate = refillRate
self.tokens = capacity
self.lastRefill = Date()
}
func consume(tokens: Double = 1) -> Bool {
refill()
if self.tokens >= tokens {
self.tokens -= tokens
return true // Request allowed
}
return false // Rate limited
}
private func refill() {
let now = Date()
let elapsed = now.timeIntervalSince(lastRefill)
tokens = min(capacity, tokens + elapsed * refillRate)
lastRefill = now
}
}
// Example: 100 requests/minute
let bucket = TokenBucket(capacity: 100, refillRate: 100/60)
Rate Limit Tiers
| Tier | Requests/Minute | Requests/Day | Use Case |
|---|---|---|---|
| Anonymous | 10 | 100 | Unauthenticated endpoints |
| Free User | 60 | 5,000 | Standard app usage |
| Premium User | 300 | 50,000 | High-frequency features |
| Internal Service | 1,000 | Unlimited | Server-to-server |
Sliding Window Log
For precise rate limiting, Whistl uses sliding window logs:
Implementation
class SlidingWindowLimiter {
private var timestamps: [Date] = []
private let windowSize: TimeInterval
private let maxRequests: Int
func allowRequest() -> Bool {
let now = Date()
let windowStart = now.addingTimeInterval(-windowSize)
// Remove old timestamps
timestamps = timestamps.filter { $0 > windowStart }
// Check if under limit
if timestamps.count < maxRequests {
timestamps.append(now)
return true
}
return false
}
}
// Example: 100 requests per 60-second window
let limiter = SlidingWindowLimiter(windowSize: 60, maxRequests: 100)
Redis-Based Distributed Rate Limiting
For multi-server deployments, rate limits are stored in Redis:
Redis Implementation
import Redis
class DistributedRateLimiter {
private let redis: RedisClient
private let keyPrefix = "ratelimit:"
func isAllowed(user: String, endpoint: String, limit: Int, window: Int) async -> Bool {
let key = "\(keyPrefix)\(user):\(endpoint)"
let now = Date().timeIntervalSince1970
let windowStart = now - Double(window)
// Use Redis sorted set for sliding window
try await redis.zremrangebyscore(key, min: "-inf", max: windowStart)
let count = try await redis.zcard(key)
if count < limit {
try await redis.zadd(key, score: now, member: "\(now)-\(UUID())")
try await redis.expire(key, window)
return true
}
return false
}
func getRemaining(user: String, endpoint: String, limit: Int, window: Int) async -> Int {
let key = "\(keyPrefix)\(user):\(endpoint)"
let now = Date().timeIntervalSince1970
let windowStart = now - Double(window)
try await redis.zremrangebyscore(key, min: "-inf", max: windowStart)
let count = try await redis.zcard(key)
return max(0, limit - count)
}
}
Rate Limit Headers
API responses include rate limit information:
Response Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1709625600
Retry-After: 30
{
"data": {...}
}
Rate Limit Exceeded Response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1709625600
Retry-After: 30
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please retry after 30 seconds.",
"retry_after": 30
}
}
Abuse Detection
Beyond simple rate limiting, Whistl detects abuse patterns:
Abuse Signals
| Signal | Threshold | Action |
|---|---|---|
| Rapid auth failures | 5 in 5 minutes | Temporary lockout |
| Sequential ID enumeration | 20 sequential IDs | Block + alert |
| Geographic anomalies | 2 countries in 1 hour | Require re-auth |
| Unusual request patterns | ML anomaly score >0.8 | Throttle + review |
| Bot-like behaviour | Regular intervals | CAPTCHA challenge |
Anomaly Detection
class AbuseDetector {
private let anomalyModel: MLModel
func detectAbuse(request: APIRequest, userHistory: UserHistory) -> AbuseScore {
let features = extractFeatures(request, userHistory)
let score = anomalyModel.predict(features)
return AbuseScore(
value: score,
riskLevel: getRiskLevel(score),
recommendedAction: getAction(score)
)
}
private func extractFeatures(_ request: APIRequest, _ history: UserHistory) -> [Double] {
return [
request.timeSinceLastRequest,
request.endpointRiskScore,
history.failureRate,
history.averageRequestsPerDay,
request.deviationFromNormal
]
}
}
DDoS Protection
Whistl uses Cloudflare for DDoS mitigation:
DDoS Protection Layers
- Layer 3/4 (Network): SYN flood, UDP flood protection
- Layer 7 (Application): HTTP flood, slowloris protection
- Rate limiting: Global and per-IP limits
- WAF rules: Block known attack patterns
- Challenge mode: JavaScript/CAPTCHA challenges
Cloudflare Configuration
# Cloudflare Rate Limiting Rules
{
"rules": [
{
"name": "Global API Rate Limit",
"threshold": 1000,
"period": 60,
"action": "block",
"match": {
"url": "api.whistl.app/*"
}
},
{
"name": "Auth Endpoint Protection",
"threshold": 10,
"period": 60,
"action": "challenge",
"match": {
"url": "api.whistl.app/auth/*"
}
},
{
"name": "Data Export Protection",
"threshold": 5,
"period": 300,
"action": "block",
"match": {
"url": "api.whistl.app/export/*"
}
}
]
}
Adaptive Throttling
Rate limits adapt based on system load:
Load-Based Throttling
class AdaptiveThrottler {
private let metricsClient: MetricsClient
private var currentMultiplier: Double = 1.0
func updateThrottleMultiplier() async {
let cpuUsage = await metricsClient.getCPUUsage()
let memoryUsage = await metricsClient.getMemoryUsage()
let requestLatency = await metricsClient.getP99Latency()
// Calculate throttle factor
var factor = 1.0
if cpuUsage > 0.8 { factor *= 0.7 }
if memoryUsage > 0.85 { factor *= 0.8 }
if requestLatency > 1000 { factor *= 0.6 }
currentMultiplier = max(0.3, factor) // Never go below 30%
}
func getEffectiveLimit(baseLimit: Int) -> Int {
return Int(Double(baseLimit) * currentMultiplier)
}
}
Graceful Degradation
When rate limited, Whistl degrades gracefully:
Degradation Strategy
| Load Level | Enabled Features | Disabled Features |
|---|---|---|
| Normal | All features | None |
| Elevated | Core features | Analytics, non-essential sync |
| High | Blocking, intervention | Background sync, ML updates |
| Critical | Emergency features only | Everything non-critical |
Client-Side Handling
Whistl apps handle rate limits gracefully:
Retry Logic with Backoff
class APIClient {
func requestWithRetry(_ request: APIRequest) async throws -> T {
var attempts = 0
let maxAttempts = 5
while attempts < maxAttempts {
do {
return try await apiClient.send(request)
} catch APIError.rateLimited(let retryAfter) {
attempts += 1
if attempts >= maxAttempts {
throw APIError.tooManyRetries
}
// Exponential backoff with jitter
let delay = Double(retryAfter) * pow(2.0, Double(attempts - 1))
let jitter = Double.random(in: 0...1) * 0.5 * delay
try await Task.sleep(nanoseconds: UInt64((delay + jitter) * 1_000_000_000))
}
}
throw APIError.tooManyRetries
}
}
Monitoring and Alerting
Rate limiting effectiveness is continuously monitored:
Key Metrics
- Rate limit hit rate: Percentage of requests rate limited
- False positive rate: Legitimate users rate limited
- Abuse detection accuracy: True positive rate for abuse
- Response time impact: Latency added by rate limiting
Alert Thresholds
| Metric | Warning | Critical |
|---|---|---|
| Rate limit hit rate | >5% | >20% |
| DDoS attack detected | Any | Any |
| Auth failure spike | 2x normal | 10x normal |
| Abuse detection rate | >1% | >5% |
Conclusion
Rate limiting and abuse prevention protect Whistl's infrastructure while ensuring legitimate users have reliable access. Through token bucket algorithms, distributed rate limiting with Redis, and adaptive throttling, the API remains responsive even under attack.
Graceful degradation ensures critical features (blocking, intervention) remain available even when non-essential features are throttled.
Experience Reliable Protection
Whistl's infrastructure is designed for reliability and security. Download free and experience always-available protection.
Download Whistl FreeRelated: DNS Filtering Implementation | Push Notification Delivery | Crash Reporting