OBP-API/REDIS_RATE_LIMITING_DOCUMENTATION.md
simonredfern efc1868fd4 BREAKING CHANGE: Switch active-rate-limits endpoint to hour-based format
Changed from full timestamp to hour-only format to match implementation.

OLD: /active-rate-limits/2025-12-31T13:34:46Z (YYYY-MM-DDTHH:MM:SSZ)
NEW: /active-rate-limits/2025-12-31-13 (YYYY-MM-DD-HH)

Benefits:
- API now matches actual implementation (hour-level caching)
- Eliminates timezone/minute truncation confusion
- Clearer semantics: 'active during this hour' not 'at this second'
- Direct cache key mapping improves performance
- Simpler date parsing (no timezone handling needed)

Files changed:
- APIMethods600.scala: Updated endpoint and date parsing
- RateLimitsTest.scala: Updated all test cases to new format
- Glossary.scala: Updated API documentation
- introductory_system_documentation.md: Updated user docs

This is a breaking change but necessary to align API with implementation.
Rate limits are cached and queried at hour granularity, so the API
should reflect that reality.
2025-12-30 17:35:38 +01:00

1027 lines
30 KiB
Markdown

# Redis Rate Limiting in OBP-API
## Table of Contents
1. [Overview](#overview)
2. [Architecture](#architecture)
3. [Configuration](#configuration)
4. [Rate Limiting Mechanisms](#rate-limiting-mechanisms)
5. [Redis Data Structure](#redis-data-structure)
6. [Implementation Details](#implementation-details)
7. [API Response Headers](#api-response-headers)
8. [Monitoring and Debugging](#monitoring-and-debugging)
9. [Error Handling](#error-handling)
10. [Performance Considerations](#performance-considerations)
---
## Overview
The OBP-API uses **Redis** as a distributed counter backend for implementing API rate limiting. This system controls the number of API calls that consumers can make within specific time periods to prevent abuse and ensure fair resource allocation.
### Key Features
- **Multi-period rate limiting**: Enforces limits across 6 time periods (per second, minute, hour, day, week, month)
- **Distributed counters**: Uses Redis for atomic, thread-safe counter operations
- **Automatic expiration**: Leverages Redis TTL (Time-To-Live) for automatic counter reset
- **Anonymous access control**: IP-based rate limiting for unauthenticated requests
- **Fail-open design**: Defaults to allowing requests if Redis is unavailable
- **Standard HTTP headers**: Returns X-Rate-Limit-\* headers for client awareness
---
## Architecture
### High-Level Flow
```
┌─────────────────┐
│ API Request │
└────────┬────────┘
┌─────────────────────────────────────────┐
│ Authentication (OAuth/DirectLogin) │
└────────┬────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Rate Limiting Check │
│ (RateLimitingUtil.underCallLimits) │
└────────┬────────────────────────────────┘
├─── Consumer authenticated?
├─── YES → Check 6 time periods
│ │ (second, minute, hour, day, week, month)
│ │
│ ├─── Redis Key: {consumer_id}_{PERIOD}
│ ├─── Check: current_count + 1 <= limit?
│ │
│ ├─── NO → Return 429 (Rate Limit Exceeded)
│ │
│ └─── YES → Increment Redis counters
│ Set X-Rate-Limit-* headers
│ Continue to API endpoint
└─── NO → Anonymous access
│ Check per-hour limit only
├─── Redis Key: {ip_address}_PER_HOUR
├─── Check: current_count + 1 <= limit?
├─── NO → Return 429
└─── YES → Increment counter
Continue to API endpoint
```
### Component Architecture
```
┌──────────────────────────────────────────────────────────┐
│ API Layer │
│ (AfterApiAuth trait - applies rate limiting to all │
│ authenticated endpoints) │
└────────────────────┬─────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ RateLimitingUtil │
│ - underCallLimits() [Main enforcement] │
│ - underConsumerLimits() [Check individual period] │
│ - incrementConsumerCounters()[Increment Redis counters] │
│ - consumerRateLimitState() [Read current state] │
└────────────────────┬─────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Redis Layer │
│ - Redis.use() [Abstraction wrapper] │
│ - JedisPool [Connection pool] │
│ - Atomic operations [GET, SET, INCR, TTL] │
└──────────────────────────────────────────────────────────┘
```
---
## Configuration
### Required Properties
Add these properties to your `default.props` file:
```properties
# Enable consumer-based rate limiting
use_consumer_limits=true
# Redis connection settings
cache.redis.url=127.0.0.1
cache.redis.port=6379
cache.redis.password=your_redis_password
# Optional: SSL configuration for Redis
redis.use.ssl=false
truststore.path.redis=/path/to/truststore.jks
truststore.password.redis=truststore_password
keystore.path.redis=/path/to/keystore.jks
keystore.password.redis=keystore_password
# Anonymous access limit (requests per hour)
user_consumer_limit_anonymous_access=1000
# System-wide default limits (when no RateLimiting records exist)
rate_limiting_per_second=-1
rate_limiting_per_minute=-1
rate_limiting_per_hour=-1
rate_limiting_per_day=-1
rate_limiting_per_week=-1
rate_limiting_per_month=-1
```
### Configuration Parameters Explained
| Parameter | Default | Description |
| -------------------------------------- | ----------- | -------------------------------------------------------- |
| `use_consumer_limits` | `false` | Master switch for rate limiting feature |
| `cache.redis.url` | `127.0.0.1` | Redis server hostname or IP |
| `cache.redis.port` | `6379` | Redis server port |
| `cache.redis.password` | `null` | Redis authentication password |
| `redis.use.ssl` | `false` | Enable SSL/TLS for Redis connection |
| `user_consumer_limit_anonymous_access` | `1000` | Per-hour limit for anonymous API calls |
| `rate_limiting_per_*` | `-1` | Default limits when no DB records exist (-1 = unlimited) |
### Redis Pool Configuration
The system uses JedisPool with the following connection pool settings:
```scala
poolConfig.setMaxTotal(128) // Maximum total connections
poolConfig.setMaxIdle(128) // Maximum idle connections
poolConfig.setMinIdle(16) // Minimum idle connections
poolConfig.setTestOnBorrow(true) // Test connections before use
poolConfig.setTestOnReturn(true) // Test connections on return
poolConfig.setTestWhileIdle(true) // Test idle connections
poolConfig.setMinEvictableIdleTimeMillis(30*60*1000) // 30 minutes
poolConfig.setTimeBetweenEvictionRunsMillis(30*60*1000)
poolConfig.setNumTestsPerEvictionRun(3)
poolConfig.setBlockWhenExhausted(true) // Block when no connections available
```
---
## Rate Limiting Mechanisms
### 1. Authorized Access (Authenticated Consumers)
For authenticated API consumers with valid OAuth tokens or DirectLogin credentials:
#### Six Time Periods
The system enforces limits across **6 independent time periods**:
1. **PER_SECOND** (1 second window)
2. **PER_MINUTE** (60 seconds window)
3. **PER_HOUR** (3,600 seconds window)
4. **PER_DAY** (86,400 seconds window)
5. **PER_WEEK** (604,800 seconds window)
6. **PER_MONTH** (2,592,000 seconds window, ~30 days)
#### Rate Limit Source
Rate limits are retrieved from the **RateLimiting** database table via the `getActiveRateLimitsWithIds()` function:
```scala
// Retrieves active rate limiting records for a consumer
def getActiveRateLimitsWithIds(consumerId: String, date: Date):
Future[(CallLimit, List[String])]
```
This function:
- Queries the database for active RateLimiting records
- Aggregates multiple records (if configured for different APIs/banks)
- Returns a `CallLimit` object with limits for all 6 periods
- Falls back to system property defaults if no records exist
#### Limit Aggregation
When multiple RateLimiting records exist for a consumer:
- **Positive values** (> 0) are **summed** across records
- **Negative values** (-1) indicate "unlimited" for that period
- If all records have -1 for a period, the result is -1 (unlimited)
Example:
```
Record 1: per_minute = 100
Record 2: per_minute = 50
Aggregated: per_minute = 150
```
### 2. Anonymous Access (Unauthenticated Requests)
For requests without consumer credentials:
- **Only per-hour limits** are enforced
- Default limit: **1000 requests per hour** (configurable)
- Rate limiting key: **Client IP address**
- Designed to prevent abuse while allowing reasonable anonymous usage
---
## Redis Data Structure
### Key Format
Rate limiting counters are stored in Redis with keys following this pattern:
```
{consumer_id}_{PERIOD}
```
**Examples:**
```
consumer_abc123_PER_SECOND
consumer_abc123_PER_MINUTE
consumer_abc123_PER_HOUR
consumer_abc123_PER_DAY
consumer_abc123_PER_WEEK
consumer_abc123_PER_MONTH
192.168.1.100_PER_HOUR // Anonymous access (IP-based)
```
### Value Format
Each key stores a **string representation** of the current call count:
```
"42" // 42 calls made in current window
```
### Time-To-Live (TTL)
Redis TTL is set to match the time period:
| Period | TTL (seconds) |
| ---------- | ------------- |
| PER_SECOND | 1 |
| PER_MINUTE | 60 |
| PER_HOUR | 3,600 |
| PER_DAY | 86,400 |
| PER_WEEK | 604,800 |
| PER_MONTH | 2,592,000 |
**Automatic Cleanup:** Redis automatically deletes keys when TTL expires, resetting the counter for the next time window.
### Redis Operations Used
| Operation | Purpose | When Used | Example |
| --------------- | ------------------------------ | ------------------------------------------ | -------------------------------------- |
| **GET** | Read current counter value | During limit check (`underConsumerLimits`) | `GET consumer_123_PER_MINUTE` → "42" |
| **SET** (SETEX) | Initialize counter with TTL | First call in time window | `SETEX consumer_123_PER_MINUTE 60 "1"` |
| **INCR** | Atomically increment counter | Subsequent calls in same window | `INCR consumer_123_PER_MINUTE` → 43 |
| **TTL** | Check remaining time in window | Before incrementing, for response headers | `TTL consumer_123_PER_MINUTE` → 45 |
| **EXISTS** | Check if key exists | During limit check | `EXISTS consumer_123_PER_MINUTE` → 1 |
| **DEL** | Delete counter (when limit=-1) | When limit changes to unlimited | `DEL consumer_123_PER_MINUTE` |
### SET vs INCR: When Each is Used
Understanding when to use SET versus INCR is critical to the rate limiting logic:
#### **SET (SETEX) - First Call in Time Window**
**When:** The counter key does NOT exist in Redis (TTL returns -2)
**Purpose:** Initialize the counter and set its expiration time
**Code Flow:**
```scala
val ttl = Redis.use(JedisMethod.TTL, key).get.toInt
ttl match {
case -2 => // Key doesn't exist - FIRST CALL in this time window
val seconds = RateLimitingPeriod.toSeconds(period).toInt
Redis.use(JedisMethod.SET, key, Some(seconds), Some("1"))
// Returns: (ttl_seconds, 1)
```
**Redis Command Executed:**
```redis
SETEX consumer_123_PER_MINUTE 60 "1"
```
**What This Does:**
1. Creates the key `consumer_123_PER_MINUTE`
2. Sets its value to `"1"` (first call)
3. Sets TTL to `60` seconds (will auto-expire after 60 seconds)
**Example Scenario:**
```
Time: 10:00:00
Action: Consumer makes first API call
Redis: Key doesn't exist (TTL = -2)
Operation: SETEX consumer_123_PER_MINUTE 60 "1"
Result: Counter = 1, TTL = 60 seconds
```
#### **INCR - Subsequent Calls in Same Window**
**When:** The counter key EXISTS in Redis (TTL returns positive number or -1)
**Purpose:** Atomically increment the existing counter
**Code Flow:**
```scala
ttl match {
case _ => // Key exists - SUBSEQUENT CALL in same time window
val cnt = Redis.use(JedisMethod.INCR, key).get.toInt
// Returns: (remaining_ttl, new_count)
```
**Redis Command Executed:**
```redis
INCR consumer_123_PER_MINUTE
```
**What This Does:**
1. Atomically increments the value by 1
2. Returns the new value
3. Does NOT modify the TTL (it continues counting down)
**Example Scenario:**
```
Time: 10:00:15 (15 seconds after first call)
Action: Consumer makes second API call
Redis: Key exists (TTL = 45 seconds remaining)
Operation: INCR consumer_123_PER_MINUTE
Result: Counter = 2, TTL = 45 seconds (unchanged)
```
#### **Why Not Use SET for Every Call?**
**Wrong Approach:**
```redis
SET consumer_123_PER_MINUTE "2" EX 60
SET consumer_123_PER_MINUTE "3" EX 60
```
**Problem:** Each SET resets the TTL to 60 seconds, extending the time window indefinitely!
**Correct Approach:**
```redis
SETEX consumer_123_PER_MINUTE 60 "1" # First call: TTL = 60
INCR consumer_123_PER_MINUTE # Second call: Counter = 2, TTL = 59
INCR consumer_123_PER_MINUTE # Third call: Counter = 3, TTL = 58
```
**Result:** TTL counts down naturally, window expires at correct time
#### **Complete Request Flow Example**
**Scenario:** Consumer with 100 requests/minute limit
```
10:00:00.000 - First request
├─ TTL consumer_123_PER_MINUTE → -2 (key doesn't exist)
├─ SETEX consumer_123_PER_MINUTE 60 "1"
└─ Response: Counter=1, TTL=60, Remaining=99
10:00:00.500 - Second request (0.5 seconds later)
├─ GET consumer_123_PER_MINUTE → "1"
├─ Check: 1 + 1 <= 100? YES (under limit)
├─ TTL consumer_123_PER_MINUTE → 59
├─ INCR consumer_123_PER_MINUTE → 2
└─ Response: Counter=2, TTL=59, Remaining=98
10:00:01.000 - Third request (1 second after first)
├─ GET consumer_123_PER_MINUTE → "2"
├─ Check: 2 + 1 <= 100? YES (under limit)
├─ TTL consumer_123_PER_MINUTE → 59
├─ INCR consumer_123_PER_MINUTE → 3
└─ Response: Counter=3, TTL=59, Remaining=97
... (more requests) ...
10:01:00.000 - Request after 60 seconds
├─ TTL consumer_123_PER_MINUTE → -2 (key expired and deleted)
├─ SETEX consumer_123_PER_MINUTE 60 "1" (New window starts!)
└─ Response: Counter=1, TTL=60, Remaining=99
```
#### **Special Case: Limit Changes to Unlimited**
**When:** Rate limit for a period changes to `-1` (unlimited)
**Code Flow:**
```scala
case -1 => // Limit is not set for the period
val key = createUniqueKey(consumerKey, period)
Redis.use(JedisMethod.DELETE, key)
(-1, -1)
```
**Redis Command:**
```redis
DEL consumer_123_PER_MINUTE
```
**Purpose:** Remove the counter entirely since there's no limit to track
#### **Atomic Operation Guarantee**
**Why INCR is Critical:**
The `INCR` operation is **atomic** in Redis, meaning:
- No race conditions between concurrent requests
- Thread-safe across multiple API instances
- Guaranteed correct count even under high load
**Example of Race Condition (if we used GET/SET):**
```
Thread A: GET counter → "42"
Thread B: GET counter → "42" (reads same value!)
Thread A: SET counter "43"
Thread B: SET counter "43" (overwrites A's increment!)
Result: Counter should be 44, but it's 43 (lost update!)
```
**With INCR (atomic):**
```
Thread A: INCR counter → 43
Thread B: INCR counter → 44 (atomic, no race condition)
Result: Counter is correctly 44
```
#### **Summary: Decision Tree**
```
Is this request within a rate limit period?
├─ Check TTL of Redis key
│ │
│ ├─ TTL = -2 (key doesn't exist)
│ │ └─ Use: SETEX key <period_seconds> "1"
│ │ Purpose: Start new time window
│ │
│ └─ TTL > 0 or TTL = -1 (key exists)
│ └─ Use: INCR key
│ Purpose: Increment counter in existing window
└─ After <period_seconds> pass
└─ Redis automatically deletes key (TTL expires)
Next request will use SETEX again
```
---
## Implementation Details
### Core Functions
#### 1. `underCallLimits()`
**Location:** `RateLimitingUtil.scala`
**Purpose:** Main rate limiting enforcement function called for every API request
**Flow:**
```scala
def underCallLimits(userAndCallContext: (Box[User], Option[CallContext])):
(Box[User], Option[CallContext])
```
**Logic:**
1. Check if CallContext exists
2. Determine if consumer is authenticated (authorized) or anonymous
3. **Authorized path:**
- Retrieve rate limits from CallContext.rateLimiting
- Check all 6 time periods using `underConsumerLimits()`
- If any limit exceeded → Return 429 error with appropriate message
- If all checks pass → Increment all counters using `incrementConsumerCounters()`
- Set X-Rate-Limit-\* headers
4. **Anonymous path:**
- Check only PER_HOUR limit
- Use IP address as rate limiting key
- If limit exceeded → Return 429 error
- Otherwise increment counter and continue
**Error Precedence:** Shorter periods take precedence in error messages:
```
PER_SECOND > PER_MINUTE > PER_HOUR > PER_DAY > PER_WEEK > PER_MONTH
```
#### 2. `underConsumerLimits()`
**Purpose:** Check if consumer is under limit for a specific time period
```scala
private def underConsumerLimits(consumerKey: String,
period: LimitCallPeriod,
limit: Long): Boolean
```
**Logic:**
1. If `use_consumer_limits=false` → Return `true` (allow)
2. If `limit <= 0` → Return `true` (unlimited)
3. If `limit > 0`:
- Build Redis key: `{consumerKey}_{period}`
- Check if key EXISTS in Redis
- If exists: GET current count, check if `count + 1 <= limit`
- If not exists: Return `true` (first call in window)
4. Return result (true = under limit, false = exceeded)
**Exception Handling:** Catches all Redis exceptions and returns `true` (fail-open)
#### 3. `incrementConsumerCounters()`
**Purpose:** Increment Redis counter for a specific time period
```scala
private def incrementConsumerCounters(consumerKey: String,
period: LimitCallPeriod,
limit: Long): (Long, Long)
```
**Logic:**
1. If `limit == -1` → DELETE the Redis key, return `(-1, -1)`
2. If `limit > 0`:
- Build Redis key
- Check TTL of key
- If `TTL == -2` (key doesn't exist):
- Initialize with `SETEX key ttl "1"`
- Return `(ttl_seconds, 1)`
- If key exists:
- Atomically increment with `INCR key`
- Return `(remaining_ttl, new_count)`
3. Return tuple: `(TTL_remaining, call_count)`
**Return Values:**
- `(-1, -1)`: Unlimited or error
- `(ttl, count)`: Active limit with remaining time and current count
#### 4. `consumerRateLimitState()`
**Purpose:** Read current state of all rate limit counters (for reporting/debugging)
```scala
def consumerRateLimitState(consumerKey: String):
immutable.Seq[((Option[Long], Option[Long]), LimitCallPeriod)]
```
**Returns:** Sequence of tuples containing:
- `Option[Long]`: Current call count
- `Option[Long]`: Remaining TTL
- `LimitCallPeriod`: The time period
**Used by:** API endpoints that report rate limit status to consumers
---
## API Response Headers
### Standard Rate Limit Headers
The system sets three standard HTTP headers on successful responses:
```http
X-Rate-Limit-Limit: 1000
X-Rate-Limit-Remaining: 732
X-Rate-Limit-Reset: 2847
```
| Header | Description | Example |
| ------------------------ | ------------------------------------ | ------- |
| `X-Rate-Limit-Limit` | Maximum requests allowed in period | `1000` |
| `X-Rate-Limit-Remaining` | Requests remaining in current window | `732` |
| `X-Rate-Limit-Reset` | Seconds until limit resets (TTL) | `2847` |
### Header Selection Priority
When multiple periods are active, headers reflect the **most restrictive active period**:
```scala
// Priority order (first active period wins)
if (PER_SECOND has TTL > 0) Use PER_SECOND values
else if (PER_MINUTE has TTL > 0) Use PER_MINUTE values
else if (PER_HOUR has TTL > 0) Use PER_HOUR values
else if (PER_DAY has TTL > 0) Use PER_DAY values
else if (PER_WEEK has TTL > 0) Use PER_WEEK values
else if (PER_MONTH has TTL > 0) Use PER_MONTH values
```
### Error Response (429 Too Many Requests)
When rate limit is exceeded:
```http
HTTP/1.1 429 Too Many Requests
X-Rate-Limit-Limit: 1000
X-Rate-Limit-Remaining: 0
X-Rate-Limit-Reset: 2847
Content-Type: application/json
```
**Message Format:**
- Authorized: `"Too Many Requests. We only allow {limit} requests {period} for this Consumer."`
- Anonymous: `"Too Many Requests. We only allow {limit} requests {period} for anonymous access."`
---
## Monitoring and Debugging
### Redis CLI Commands
Useful Redis commands for monitoring rate limiting:
```bash
# Connect to Redis
redis-cli -h 127.0.0.1 -p 6379
# View all rate limit keys
KEYS *_PER_*
# Check specific consumer's counters
KEYS consumer_abc123_*
# Get current count
GET consumer_abc123_PER_MINUTE
# Check remaining time
TTL consumer_abc123_PER_MINUTE
# View all counters for a consumer
MGET consumer_abc123_PER_SECOND \
consumer_abc123_PER_MINUTE \
consumer_abc123_PER_HOUR \
consumer_abc123_PER_DAY \
consumer_abc123_PER_WEEK \
consumer_abc123_PER_MONTH
# Delete a specific counter (reset limit)
DEL consumer_abc123_PER_MINUTE
# Delete all counters for a consumer (full reset)
DEL consumer_abc123_PER_SECOND \
consumer_abc123_PER_MINUTE \
consumer_abc123_PER_HOUR \
consumer_abc123_PER_DAY \
consumer_abc123_PER_WEEK \
consumer_abc123_PER_MONTH
# Monitor Redis operations in real-time
MONITOR
# Check Redis memory usage
INFO memory
# Count rate limiting keys
KEYS *_PER_* | wc -l
```
### Application Logs
Enable debug logging in `logback.xml`:
```xml
<logger name="code.api.util.RateLimitingUtil" level="DEBUG"/>
<logger name="code.api.cache.Redis" level="DEBUG"/>
```
**Log Examples:**
```
DEBUG RateLimitingUtil - getCallCounterForPeriod: period=PER_MINUTE, key=consumer_123_PER_MINUTE, raw ttlOpt=Some(45)
DEBUG RateLimitingUtil - getCallCounterForPeriod: period=PER_MINUTE, key=consumer_123_PER_MINUTE, raw valueOpt=Some(42)
DEBUG Redis - KryoInjection started
DEBUG Redis - KryoInjection finished
ERROR RateLimitingUtil - Redis issue: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
```
### Health Check Endpoint
Check Redis connectivity:
```scala
Redis.isRedisReady // Returns Boolean
```
**Usage:**
```bash
# Via API (if exposed)
curl https://api.example.com/health/redis
# Returns:
{
"redis_ready": true,
"url": "127.0.0.1",
"port": 6379
}
```
---
## Error Handling
### Fail-Open Design
The system uses a **fail-open** approach for resilience:
```scala
try {
// Redis operation
} catch {
case e: Throwable =>
logger.error(s"Redis issue: $e")
true // Allow request to proceed
}
```
**Rationale:** If Redis is unavailable, the API remains functional rather than blocking all requests.
### Redis Connection Failures
**Symptoms:**
- Logs show: `Redis issue: redis.clients.jedis.exceptions.JedisConnectionException`
- All rate limit checks return `true` (allow)
- Rate limiting is effectively disabled
**Resolution:**
1. Check Redis server is running: `redis-cli ping`
2. Verify network connectivity
3. Check Redis credentials and SSL configuration
4. Review connection pool settings
5. Monitor connection pool exhaustion
### Common Issues
#### 1. Rate Limits Not Enforced
**Check:**
```bash
# Is rate limiting enabled?
grep "use_consumer_limits" default.props
# Is Redis reachable?
redis-cli -h 127.0.0.1 -p 6379 ping
# Are there active RateLimiting records?
SELECT * FROM ratelimiting WHERE consumer_id = 'your_consumer_id';
```
#### 2. Inconsistent Rate Limiting
**Cause:** Multiple API instances with separate Redis instances
**Solution:** Ensure all API instances connect to the **same Redis instance**
#### 3. Counters Not Resetting
**Check TTL:**
```bash
# Should return positive number (seconds remaining)
TTL consumer_123_PER_MINUTE
# -1 means no expiry (bug)
# -2 means key doesn't exist
```
**Fix:**
```bash
# Manually reset if TTL is -1
DEL consumer_123_PER_MINUTE
```
#### 4. Memory Leak (Growing Redis Memory)
**Check:**
```bash
INFO memory
KEYS *_PER_* | wc -l
```
**Cause:** Keys created without TTL
**Prevention:** Always use `SETEX` (not `SET`) for rate limit counters
---
## Performance Considerations
### Redis Operations Cost
| Operation | Time Complexity | Performance Impact |
| --------- | --------------- | ------------------ |
| GET | O(1) | Negligible |
| SET | O(1) | Negligible |
| SETEX | O(1) | Negligible |
| INCR | O(1) | Negligible |
| TTL | O(1) | Negligible |
| EXISTS | O(1) | Negligible |
| DEL | O(1) | Negligible |
**Per Request Cost:**
- Authorized: ~12-18 Redis operations (6 checks + 6 increments)
- Anonymous: ~2-3 Redis operations (1 check + 1 increment)
### Network Latency
**Typical Redis RTT:** 0.1-1ms (same datacenter)
**Per Request Latency:**
- Authorized: 1.2-18ms
- Anonymous: 0.2-3ms
### Optimization Tips
#### 1. Co-locate Redis with API
Deploy Redis on the same network/datacenter as OBP-API instances to minimize network latency.
#### 2. Connection Pooling
The default pool configuration is optimized for high throughput:
- 128 max connections supports 128 concurrent requests
- Adjust based on your load profile
#### 3. Redis Memory Management
**Estimate memory usage:**
```
Memory per key = ~100 bytes (key + value + metadata)
Active consumers = 1000
Periods = 6
Total memory = 1000 * 6 * 100 = 600 KB
```
**Monitor:**
```bash
INFO memory
CONFIG GET maxmemory
```
#### 4. Batch Operations
The current implementation checks all 6 periods sequentially. Future optimization could use Redis pipelining:
```scala
// Current: 6 round trips
underConsumerLimits(..., PER_SECOND, ...)
underConsumerLimits(..., PER_MINUTE, ...)
// ... 4 more
// Optimized: 1 round trip with pipeline
jedis.pipelined {
get(key_per_second)
get(key_per_minute)
// ... etc
}
```
### Scalability
**Horizontal Scaling:**
- Multiple OBP-API instances → **Same Redis instance**
- Redis becomes a potential bottleneck at very high scale
**Redis Scaling Options:**
1. **Redis Sentinel**: High availability with automatic failover
2. **Redis Cluster**: Horizontal sharding for massive scale
3. **Redis Enterprise**: Commercial solution with advanced features
**Capacity Planning:**
- Single Redis instance: 50,000-100,000 ops/sec
- With 6 ops per authorized request: ~8,000-16,000 requests/sec
- With 2 ops per anonymous request: ~25,000-50,000 requests/sec
---
## API Endpoints for Rate Limit Management
### Get Rate Limiting Info
```http
GET /obp/v3.1.0/management/rate-limiting
```
**Response:**
```json
{
"enabled": true,
"technology": "REDIS",
"service_available": true,
"currently_active": true
}
```
### Get Consumer's Call Limits
```http
GET /obp/v6.0.0/management/consumers/{CONSUMER_ID}/consumer/call-limits
```
**Response:**
```json
{
"per_second_call_limit": "10",
"per_minute_call_limit": "100",
"per_hour_call_limit": "1000",
"per_day_call_limit": "10000",
"per_week_call_limit": "50000",
"per_month_call_limit": "200000",
"redis_call_limit": {
"per_second": {
"calls_made": 5,
"reset_in_seconds": 0
},
"per_minute": {
"calls_made": 42,
"reset_in_seconds": 37
},
"per_hour": {
"calls_made": 732,
"reset_in_seconds": 2847
}
}
}
```
---
## Summary
The Redis-based rate limiting system in OBP-API provides:
**Distributed rate limiting** across multiple API instances
**Multi-period enforcement** (second, minute, hour, day, week, month)
**Automatic expiration** via Redis TTL
**Atomic operations** for thread-safety
**Fail-open reliability** when Redis is unavailable
**Standard HTTP headers** for client awareness
**Flexible configuration** via properties and database records
**Anonymous access control** based on IP address
**Key Files:**
- `code/api/util/RateLimitingUtil.scala` - Main rate limiting logic
- `code/api/cache/Redis.scala` - Redis connection abstraction
- `code/api/AfterApiAuth.scala` - Integration point in request flow
**Configuration:**
- `use_consumer_limits=true` - Enable rate limiting
- `cache.redis.url` / `cache.redis.port` - Redis connection
- `user_consumer_limit_anonymous_access` - Anonymous limits
**Monitoring:**
- Redis CLI: `KEYS *_PER_*`, `GET`, `TTL`
- Application logs: Enable DEBUG on `RateLimitingUtil`
- API headers: `X-Rate-Limit-*`