You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 | +--------+--------+ | V +-----------------------------------------+ | Authentication (OAuth/DirectLogin) | +--------+--------------------------------+ | V +-----------------------------------------+ | 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
# 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:
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:
PER_SECOND (1 second window)
PER_MINUTE (60 seconds window)
PER_HOUR (3,600 seconds window)
PER_DAY (86,400 seconds window)
PER_WEEK (604,800 seconds window)
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:
// Retrieves active rate limiting records for a consumer defgetActiveRateLimitsWithIds(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:
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:
valttl=Redis.use(JedisMethod.TTL, key).get.toInt ttl match { case-2=>// Key doesn't exist - FIRST CALL in this time window valseconds=RateLimitingPeriod.toSeconds(period).toInt Redis.use(JedisMethod.SET, key, Some(seconds), Some("1")) // Returns: (ttl_seconds, 1)
Redis Command Executed:
SETEX consumer_123_PER_MINUTE 60 "1"
What This Does:
Creates the key consumer_123_PER_MINUTE
Sets its value to "1" (first call)
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:
ttl match { case _ =>// Key exists - SUBSEQUENT CALL in same time window valcnt=Redis.use(JedisMethod.INCR, key).get.toInt // Returns: (remaining_ttl, new_count)
Redis Command Executed:
INCR consumer_123_PER_MINUTE
What This Does:
Atomically increments the value by 1
Returns the new value
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:
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!
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:
case-1=>// Limit is not set for the period valkey= createUniqueKey(consumerKey, period) Redis.use(JedisMethod.DELETE, key) (-1, -1)
Redis Command:
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!)
When multiple periods are active, headers reflect the most restrictive active period:
// Priority order (first active period wins) if (PER_SECOND has TTL>0) -UsePER_SECOND values elseif (PER_MINUTE has TTL>0) -UsePER_MINUTE values elseif (PER_HOUR has TTL>0) -UsePER_HOUR values elseif (PER_DAY has TTL>0) -UsePER_DAY values elseif (PER_WEEK has TTL>0) -UsePER_WEEK values elseif (PER_MONTH has TTL>0) -UsePER_MONTH values
Error Response (429 Too Many Requests)
When rate limit is exceeded:
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
{ "error": "OBP-10006: Too Many Requests. We only allow 1000 requests per hour for this Consumer." }
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:
# 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
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:
Redis.isRedisReady // Returns Boolean
Usage:
# Via API (if exposed) curl https://api.example.com/health/redis
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