Skip to content

Quota Ledger

Doc Version: 1.0.0 Last Updated: 2026-09-06 Git Commit: - Author: Lincoln

Overview

JAiRouter ships a quota ledger: it tracks request counts and token usage across multiple time windows (minute / hour / day / month) in the request path. When enabled, the ledger also drives limit enforcement against existing API Key limits.

Operator workflow (where limits live)

StepWhereWhat it does
1. Set limitsSecurity → API Keys edit formdailyRequestLimit / dailyTokenLimit / rateLimitPerMinute / quotaAlertThreshold — not on the Quota config page
2. Enable ledger (optional)Traffic → Quota Runtime Config /config/quotaHot-edit enabled / failOpen / windows; ledger defaults to off
3. Observe usageData Records → Quota Usage Monitoring /monitoring/quotaMulti-dimensional usage + limit/progress columns
4. Alerts & resetSecurity → API KeysQuota alerts list, reset daily counters
  • Limit mapping: DAY window → daily request/token caps; MINUTE window → rate limit; 0 means unlimited.
  • With the ledger off, API Key auth still enforces the same limits via in-memory counters + token bucket.

  • Off by default (opt-in): jairouter.quota.enabled defaults to false — zero behavior change. No accumulation, no database access, no Redis access; behavior is identical to v3.0.x

  • Pluggable counter backend: defaults to in-process LocalCounterBackend (LongAdder); only when distributed.enabled=true is RedisCounterBackend assembled as the cross-instance authoritative counter
  • Purely additive capability: when the ledger is disabled, all endpoints return immediately without touching the database or Redis

Quick Start

jairouter:
  quota:
    enabled: true                  # enable the quota ledger (default false)
    fail-open: true                # allow requests when the ledger fails (default true)
    flush-interval-seconds: 60     # in-memory snapshot flush interval in seconds (default 60)
    windows:                       # enabled windows (default: all four)
      - MINUTE
      - HOUR
      - DAY
      - MONTH
    retention:                     # per-window retention periods (defaults shown)
      minute: 1d
      hour: 2d
      day: 35d
      month: 13mo
    distributed:
      enabled: false               # enable Redis distributed counting (default false)
      key-prefix: "jairouter:quota"  # Redis key prefix (default jairouter:quota)
      timeout: 50ms                # per-command Redis timeout (default 50ms, clamped to 1ms~5s)
      degrade-to-local: true       # fall back to local counting when Redis is unavailable (default true)

Configuration Reference

All properties live under the jairouter.quota prefix. Key naming follows Spring relaxed binding (kebab-case in YAML, camelCase in Java).

PropertyDefaultDescription
enabledfalseMaster switch; when off, ledger reads and writes short-circuit with zero overhead
fail-opentrueWhether to allow requests when the ledger encounters an error: true = allow and mark degraded; false = reject
flush-interval-seconds60In-memory snapshot flush interval in seconds (minimum 1)
windows[MINUTE, HOUR, DAY, MONTH]Enabled window list; omitted windows produce no accounting
retention.minute1dMinute window retention period
retention.hour2dHour window retention period
retention.day35dDay window retention period
retention.month13moMonth window retention period
distributed.enabledfalseEnable Redis distributed counting; when false, counting stays entirely in-process
distributed.key-prefixjairouter:quotaDistributed counting Redis key prefix
distributed.timeout50msPer-command Redis timeout (clamped to 1ms~5s)
distributed.degrade-to-localtrueFall back to local counting when Redis is unavailable

Retention duration units: mo (month), d (day), h (hour), m (minute), s (second); values without a unit are interpreted as days, and the minimum amount is 1.

Runtime Configuration (No Restart)

Quota Runtime Configuration

The management console reads quota configuration and applies runtime changes via the following endpoints:

GET  /api/config/quota        # configuration snapshot
PUT  /api/config/quota        # runtime partial update

GET Configuration Snapshot

Returns the currently active quota configuration (including in-memory overrides) and labels which fields are hot-editable:

  • enabled / failOpen / windows — hot-editable fields, currently active runtime values
  • flushIntervalSeconds / retention / distributed — read-only display (restart-required, not modifiable via PUT)
  • backendName — current counter backend name (local or redis)
  • hotEditableFields — list of hot-editable field names (enabled / failOpen / windows)
  • restartRequiredFields — list of fields that require a restart (includes flushIntervalSeconds)

Requires the config:quota:read permission.

PUT Runtime Configuration

Partial update — only non-null fields in the request body are modified; null fields remain unchanged.

Hot-editable fields (take effect immediately, in-memory override):

FieldTypeDescription
enabledBooleanEnable or disable the quota ledger
failOpenBooleanAllow requests when the ledger fails
windowsString[]Enabled window list (e.g. ["MINUTE", "HOUR", "DAY"])

Restart-required fields (if any non-null value is sent, the request is rejected with HTTP 400 + errorCode=RESTART_REQUIRED; the message lists the specific field names):

FieldDescription
distributedEnabledEnable distributed counting
distributedKeyPrefixDistributed Redis key prefix
distributedTimeoutMsDistributed Redis command timeout (milliseconds)
distributedDegradeToLocalFall back to local when Redis is unavailable
retentionPer-window retention periods
flushIntervalSecondsSnapshot flush interval in seconds

An all-null request body results in HTTP 400 + errorCode=INVALID_REQUEST.

On success the response data has the same shape as the GET response. Requires the config:quota:write permission.

Known Limitations

  • Runtime changes are in-memory overrides and reset to the yaml values on restart or configuration refresh (same policy as response cache). For persistent changes, edit the jairouter.quota yaml.
  • flushIntervalSeconds is not hot-editable: this field is classified as restart-required, and PUT requests carrying a non-null value are rejected. The reason: the backend uses @Scheduled(fixedDelayString = "${jairouter.quota.flush-interval-seconds:60}"), which is resolved once when Spring registers the scheduled task; mutating the QuotaProperties bean field has no effect on the already-registered schedule. To change the flush interval, update the yaml and restart.
  • Setting enabled to true via hot-edit causes the ledger to start accumulating for new requests immediately; setting it to false stops accumulation, but any in-memory data will still be flushed on the next flush cycle.

Observability

GET /api/monitoring/quota/status   # runtime status
GET /api/monitoring/quota/usage    # usage query

Runtime Status

GET /api/monitoring/quota/status returns the quota ledger's runtime information:

FieldDescription
enabledWhether the ledger is enabled
backendNameCurrent counter backend name (local or redis)
degradedWhether in a degraded state
degradedReasonDegradation reason (empty string when not degraded)
failOpenCurrent fail-open setting
windowsCurrently enabled window list
distributedDistributed config object (enabled / keyPrefix / timeoutMs / degradeToLocal)
redisProbePresent only when distributed.enabled=true: reachable (bool) / status (healthy / degraded / error) / optional reason
counterMetricsPresent only when distributed.enabled=true: degradationCount (cumulative degradation count)

Requires the monitoring:quota:read permission.

Usage Query

GET /api/monitoring/quota/usage queries quota usage by dimension and window (read-only, no write side effects).

Query parameters (all optional, default to empty string):

ParameterDescription
tenantIdTenant ID
apiKeyIdAPI Key ID
userIdUser ID
serviceTypeService type (e.g. chat)
modelModel name
windowWindow type (MINUTE / HOUR / DAY / MONTH; invalid values return 400)

Response: data is an array where each entry contains:

FieldDescription
dimensionsDimension object (tenantId / apiKeyId / userId / serviceType / model)
windowWindow type
windowStartWindow start time
requestCountRequest count
tokenCountToken count

When the quota ledger is disabled, data=[] and message=配额账本未启用.

Requires the monitoring:quota:read permission.

Permission Codes

The quota feature introduces 3 new permission codes:

Permission codeDescription
config:quota:readQuota configuration status query
config:quota:writeQuota runtime configuration update
monitoring:quota:readQuota runtime status and usage query

The ADMIN role includes all codes by default; OPERATOR includes read/write codes; VIEWER includes read codes.

Limitations and Roadmap

Current version boundaries:

  • Distributed: when distributed.enabled=true, Redis is the authoritative counter and local degrades to a mirror + fallback; when distributed.enabled=false (default), counting is only precise within a single instance
  • Snapshot flushing: in-memory deltas are flushed to the database every 60 seconds by default; the @Scheduled interval is fixed at startup and cannot be changed at runtime (flushIntervalSeconds is a restart-required field)
  • Limit enforcement: the ledger provides the data foundation; actual limit enforcement (QuotaEnforcementService) endpoints and UI will be delivered in a later PR
  • Async path: the hot path uses Mono.block(timeout) to synchronously block Redis calls (default 50ms); full async integration into the Reactor chain is deferred to a later PR