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

TierRequests/MinuteRequests/DayUse Case
Anonymous10100Unauthenticated endpoints
Free User605,000Standard app usage
Premium User30050,000High-frequency features
Internal Service1,000UnlimitedServer-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

SignalThresholdAction
Rapid auth failures5 in 5 minutesTemporary lockout
Sequential ID enumeration20 sequential IDsBlock + alert
Geographic anomalies2 countries in 1 hourRequire re-auth
Unusual request patternsML anomaly score >0.8Throttle + review
Bot-like behaviourRegular intervalsCAPTCHA 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 LevelEnabled FeaturesDisabled Features
NormalAll featuresNone
ElevatedCore featuresAnalytics, non-essential sync
HighBlocking, interventionBackground sync, ML updates
CriticalEmergency features onlyEverything 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

MetricWarningCritical
Rate limit hit rate>5%>20%
DDoS attack detectedAnyAny
Auth failure spike2x normal10x 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 Free

Related: DNS Filtering Implementation | Push Notification Delivery | Crash Reporting