API Explorer

Rate Limiting

Rate Limiting controls the number of API requests a Consumer can make within specific time periods. This prevents abuse and ensures fair resource allocation across all API consumers.

Architecture - Single Source of Truth

┌─────────────────────────────────────────────────────────────────────────┐
│                      RateLimitingUtil.scala                             │
│                                                                         │
│  ┌───────────────────────────────────────────────────────────────────┐ │
│  │                                                                   │ │
│  │  getActiveRateLimitsWithIds(consumerId, date):                   │ │
│  │  Future[(CallLimit, List[String])]                               │ │
│  │                                                                   │ │
│  │  ═══════════════════════════════════════════════════════         │ │
│  │  Single Source of Truth                              │ │
│  │  ═══════════════════════════════════════════════════════         │ │
│  │                                                                   │ │
│  │  This function calculates active rate limits                │ │
│  │                                                                   │ │
│  │  Logic:                                                           │ │
│  │  1. Query RateLimiting table for active records                  │ │
│  │  2. If found, per period:                                         │ │
│  │     • Ignore -1 values (unlimited rows add nothing)               │ │
│  │     • Sum the rest; a sum of 0 -> blocked (429 on every call)     │ │
│  │     • Nothing to sum (all -1) -> -1 (unlimited)                   │ │
│  │     • Extract rate_limiting_ids                                  │ │
│  │  3. If not found:                                                 │ │
│  │     • Return system defaults from props                          │ │
│  │     • Empty ID list                                              │ │
│  │  4. Return: (CallLimit, List[rate_limiting_ids])                 │ │
│  │                                                                   │ │
│  └───────────────────────────────────────────────────────────────────┘ │
│                              ▲                                          │
│                              │                                          │
└──────────────────────────────┼──────────────────────────────────────────┘
                               │
                               │ Both callers use
                               │ the same function
                               │
               ┌───────────────┴───────────────┐
               │                               │
               │                               │
    ┌──────────▼──────────┐         ┌──────────▼──────────┐
    │                     │         │                     │
    │  AfterApiAuth.scala │         │ Http4s600.scala     │
    │                     │         │                     │
    │  checkRateLimiting()│         │ getActiveCallLimits │
    │                     │         │ AtDate              │
    │  ─────────────────  │         │  ────────────────   │
    │                     │         │                     │
    │  Called: Every      │         │  Endpoint:          │
    │  API request        │         │  GET /management/   │
    │                     │         │  consumers/ID/      │
    │  Uses:              │         │  consumer/active-   │
    │  (rateLimit, _)     │         │  rate-limits/DATE   │
    │                     │         │                     │
    │  Ignores IDs,       │         │  Uses:              │
    │  just needs the     │         │  (rateLimit, ids)   │
    │  CallLimit for      │         │                     │
    │  enforcement        │         │  Returns both in    │
    │                     │         │  JSON response      │
    │                     │         │                     │
    └─────────────────────┘         └─────────────────────┘

Key Point: There is one function that calculates active rate limits. Both enforcement and API reporting call this one function.

How It Works

  1. Rate Limit Records: Stored in the RateLimiting table with date ranges (from_date, to_date)
  2. Multiple Records: A consumer can have multiple active rate limit records that overlap
  3. Aggregation: When multiple records are active, per period: -1 values are ignored and the rest (0 or positive) are summed; a sum of 0 blocks the period; nothing to sum (all -1) means unlimited
  4. Enforcement: On every API request, the system checks Redis counters against the aggregated limits
  5. Counting: Every served request is counted in the Redis counter of every period, whether or not that period has a limit, so the call-counter endpoints show a Consumer's activity even when nothing limits it. A blocked period (sum 0) serves nothing, so nothing is counted under it.

Time Periods

Rate limits can be set for six time periods:

  • per_second_rate_limit: Maximum requests per second
  • per_minute_rate_limit: Maximum requests per minute
  • per_hour_rate_limit: Maximum requests per hour
  • per_day_rate_limit: Maximum requests per day
  • per_week_rate_limit: Maximum requests per week
  • per_month_rate_limit: Maximum requests per month

Each value means:

  • 0: this record grants no calls for that period. Records are summed, so a 0 only blocks the Consumer when the sum over all of its records is 0 (for example when it is the Consumer's only record). A blocked period refuses every call with 429. This is how a suspended API Product Subscription stops a Consumer whose access came from that subscription alone.
  • -1: unlimited for that period. Once a record exists, -1 is literal: the system default for that period does not apply. -1 records add nothing to the sum.
  • a positive number: the maximum number of calls in that period. Overlapping records are summed.

A Consumer with no records at all gets the system defaults (see below).

HTTP Headers

When rate limiting is active, responses include:

  • X-Rate-Limit-Limit: Maximum allowed requests for the period
  • X-Rate-Limit-Remaining: Remaining requests in current period
  • X-Rate-Limit-Reset: Seconds until the limit resets

The three headers describe the shortest period that has a positive limit (per second before per minute, and so on). When no period is limited they read -1.

HTTP Status Codes

  • 200 OK: Request allowed, headers show current limit status
  • 429 Too Many Requests: Rate limit exceeded for a time period

Querying Active Rate Limits

Use the endpoint:

GET /obp/v6.0.0/management/consumers/{CONSUMER_ID}/active-rate-limits/{DATE_WITH_HOUR}

Where DATE_WITH_HOUR is in format YYYY-MM-DD-HH in UTC timezone (e.g., 2025-12-31-13 for hour 13:00-13:59 UTC on Dec 31, 2025).

Returns the aggregated active rate limits for the specified hour, including which rate limit records contributed to the totals.

Rate limits are cached and queried at hour-level granularity for performance. All hours are interpreted in UTC for consistency across all servers.

System Defaults

If no rate limit records exist for a consumer, system-wide defaults are used from properties:

  • rate_limiting_per_second
  • rate_limiting_per_minute
  • rate_limiting_per_hour
  • rate_limiting_per_day
  • rate_limiting_per_week
  • rate_limiting_per_month

Default value: -1 (unlimited). These defaults apply only to Consumers with no active records; a default of 0 would block every such Consumer.

Example

A consumer with two overlapping rate limit records:

  • Record 1: 10 requests/second, 100 requests/minute
  • Record 2: 5 requests/second, 50 requests/minute

Aggregated limits: 15 requests/second, 150 requests/minute

The same consumer with a third record of 0 requests/second (for example a suspended API Product Subscription) is unchanged, because the 0 adds nothing to the sum:

Aggregated limits: 15 requests/second, 150 requests/minute

A consumer whose only record is 0 requests/second:

Aggregated limits: 0 requests/second (blocked, 429 on every call)

Configuration

Enable rate limiting by setting:

use_consumer_limits=true

For anonymous access, configure:

user_consumer_limit_anonymous_access=1000

(Default: 1000 requests per hour. 0 blocks all anonymous access, -1 removes the limit.)

Three rate limiters

OBP runs three independent rate limiters. They are checked in this order, and each answers 429 with its own error code so a client can tell which counter it hit:

  1. Self-service limiter (self_service.rate_limit.*) runs first, before routing and before any authentication, keyed by the client IP address. It covers the endpoints anyone can call before the bank has granted them anything. Trip code: OBP-10060.
  2. Authentication limiter (auth.rate_limit.*) runs inside the credential check of Direct Login, DAuth, Gateway Login and SIWE, before the password or token is verified, keyed by IP address and by account. It defends against brute force, credential stuffing and lockout attacks. Trip code: OBP-10061.
  3. Consumer quota (the limits described above) runs after authentication, keyed by Consumer, or by IP address with a single hourly ceiling for anonymous calls. It is the commercial and fair-use quota. Trip code: OBP-10018.

Before all three, an operator can put a single IP address under a temporary IP penalty: a per-minute limit on every endpoint, for a set time, for example during a scan or denial-of-service attempt (POST /obp/v7.0.0/management/ip-penalties, Role CanCreateIpPenalty). A per-minute limit of 0 refuses every request. Penalties are always enforced, shared by every instance, and disappear when they expire; the penalty endpoints themselves are never refused, so a mistake can be undone. Trip code: OBP-10062.

A login attempt is counted by the authentication limiter only; it is not a self-service scope, so no attempt is counted twice. Every limiter counts in Redis and fails open: a Redis outage never blocks a call.

Self-service rate limiting (per IP address, before any credential)

The limits above are keyed by Consumer, so they cannot protect the calls a client makes before it has one. Those endpoints are covered by the self-service limiter, keyed by the client IP address, grouped in scopes:

  • signup — Create User (self-registration), Validate User Email, Get User Invitation Information
  • password_reset — Request Password Reset Email, Complete Password Reset
  • consent_request — Create Consent Request, Create Consent Request VRP
  • consumer_registration — Create a Consumer (Dynamic Registration)
  • lookup — Validate and check IBAN
  • signal_channel_create — Publish Signal Message, counted only when it creates a new channel, over REST and gRPC alike (gRPC uses the socket peer address; if none is available it falls back to the Consumer)

Each scope has per-minute, per-hour and per-day limits per IP, with built-in defaults chosen so that a person or a well-behaved agent never reaches them, plus an optional global per-hour cap across all addresses that acts as a circuit breaker. Every request is counted, whether or not it succeeds. Counters live in Redis and fail open.

Shadow mode (the default). The limiter is on out of the box but does not block. A request over a limit is logged once per window (event=self_service_rate_limit_shadow_trip) and the response carries:

X-Rate-Limit-Warning: OBP-10059: Could conflict with a Future Rate Limit: This request might exceed the rate limit for signup (5 per hour) in the future.

No enforcement date is claimed unless the operator sets self_service.rate_limit.enforce_announced_from, in which case ", from <date>" is appended. Every self-service response also carries X-Rate-Limit-Limit, X-Rate-Limit-Remaining and X-Rate-Limit-Reset for the window the caller is closest to exhausting, so a client can back off before enforcement starts.

Enforce mode. Set self_service.rate_limit.mode = enforce and a trip answers 429 with OBP-10060, a Retry-After header and the same X-Rate-Limit-* headers, without running the endpoint.

Limits are set with self_service.rate_limit.<scope>.per_ip.per_minute|per_hour|per_day, self_service.rate_limit.<scope>.global.per_hour, or the generic self_service.rate_limit.per_ip.*; -1 switches a window off and 0 blocks it. See the props template for the built-in numbers. Behind a proxy, configure trust.proxy.enabled and trust.proxy.header so the client address is the real one; otherwise every caller shares the proxy's counters.

Related Concepts

  • Consumer: The API client subject to rate limiting
  • Redis: Storage system for tracking request counts
  • Single Source of Truth: RateLimitingUtil.getActiveRateLimitsWithIds() function calculates all active rate limits