Product5 MIN READ126 / 130

Monthly usage and account throttling

Implemented for /vedic/v1/** and /western/v1/**. Usage belongs to the user account and is shared across every API key.

Monthly account usage and throttlingAuthenticated requests read account-month Cloudflare KV markers. Missing markers allow all calls; exhausted accounts receive twenty percent hash admission. Completed responses emit asynchronous usage events; accounting records consumption and sets markers only at exhaustion.Monthly account usage and throttlingProduct backend · account allowance in PostgreSQL · exhaustion markers in Cloudflare KVAuthenticate API keyInvalid → 401; valid → identityRead Cloudflare KVKey: account ID + UTC monthAdmission decisionAbsent: all · present: hash 20%Forward or rejectapi-app response or local 429Account usage serviceAsync event · allocate / consumePostgreSQL ledger3,000 free calls / UTC monthCompleted upstream responsesExcludes local 401 / 429 / 502Set marker only at exhaustion; remove when restoredSynchronous call / state accessAsynchronous signalAll API keys share account usage. Month-specific markers expire; missing markers allow full traffic.

Monthly allowance

Signup allocates 3,000 free calls for the current UTC calendar month. Subsequent months are allocated lazily when the first completed API call is accounted. This also covers accounts created before usage tracking was enabled. Unused calls do not carry forward.

The dedicated monthly_usage table snapshots quota per subscription and UTC month. Subscription activation writes the quota in the same transaction; consumed requests remain in the ledger. Expired subscription quota is excluded from remaining usage, with an active free subscription providing fallback. A future scheduler must create the next month’s paid quota snapshots; no scheduler is implemented yet. The latest active non-free subscription takes priority over the free grant; ties use the newest ID. The validity interval is start inclusive, expiry exclusive. A subscription upgrade changes the monthly ceiling and retains calls already consumed that month. The subscription allocation service accepts the plan, allowance and validity period; payment/redemption activation endpoints are not implemented in this backend yet.

The existing subscription and subscription_tracking tables are reused. Migration migration-0003__monthly_usage_allowance.sql adds the allowance and one-free-grant-per-user/month uniqueness. Existing subscriptions receive 3,000 by default; set existing paid-plan allowances to their actual commercial values before enabling them. Production migrations are applied manually; tests use Flyway.

remaining = max(0, active subscription monthly allowance - account calls in this UTC month)

Each completed upstream response consumes one call, including upstream errors. Local authentication failures, locally throttled requests and unreachable-upstream failures do not consume usage. Requests admitted after exhaustion are still recorded; consumption can exceed the allowance because this is soft throttling.

Request and event flow

  1. Authenticate the customer key against PostgreSQL. Missing, unknown or revoked keys return 401 INVALID_API_KEY before KV or api-app access.
  2. Read Cloudflare KV key usage-exhausted:<userId>:<monthStartUnixSeconds> from the configured namespace.
  3. Absent marker: forward all traffic. Present marker: admit approximately 20%, return 429 REQUEST_THROTTLED for the remainder. Safe rejection text: “Request throttled. Please try again.” All keys for that user share the marker; another user’s traffic is independent.
  4. Forward admitted requests normally, replacing the customer key with the upstream shared secret.
  5. Publish a usage event after the upstream response. Its server-generated transaction UUID deduplicates recording; its occurrence time determines the UTC accounting month.
  6. A dedicated asynchronous usage listener locks that account row, ensures the monthly free grant, selects the active subscription, and inserts the request into the existing ledger once. It calculates remaining usage across all the account’s subscriptions and keys for that month.
  7. At zero remaining, usage tracking calls the throttler’s exhaustion API. The KV adapter writes only when the marker is absent. Ordinary accounts never receive a marker. If an allowance becomes available, reconciliation removes an existing marker.

Usage tracking owns quota decisions. The throttler owns hash admission and exposes exhaust/restore; its KV adapter owns provider calls. There is no API-key validity worker or in-memory flag cache. The DTO file contains the shared UTC month boundaries; obsolete pair identity and cache entry DTOs were removed.

Hash admission

input = "v1:" + authenticatedUserId + ":" + serverGeneratedRequestUuid
bucket = unsigned(first 32 bits of SHA-256(UTF-8(input))) mod 100
admit = marker absent OR bucket < configured admission percentage

The default percentage is 20. Clients cannot choose the UUID. Hashing the account alone would permanently admit or reject it; the nonce provides an independent sample on every request. Twenty percent is a long-run approximation, not exactly one in every five calls.

Cloudflare and failure behavior

KV markers expire at the UTC month boundary. The month is also included in the key, so an old marker cannot throttle the new month even if deletion propagation is delayed. Expired-month events still update PostgreSQL; they do not write an expired KV flag.

KV reads fail open on provider errors, timeout or missing configuration, with a warning. Usage writes remain in PostgreSQL when marker synchronization fails; later delivered calls retry synchronization. KV is eventually consistent and same-key writes are limited, so propagation and write errors can temporarily admit more traffic. These are deliberate soft enforcement semantics, not an exact quota guarantee. See Cloudflare consistency, limits and REST write contract.

Account row locks serialize concurrent accounting and grant decisions across backend replicas. Duplicate event delivery cannot decrement usage twice. The asynchronous queue is bounded; saturation or a process crash can lose an in-memory event. Saturation is logged without replacing a completed upstream response. No durable queue/outbox is introduced. Failed database accounting is logged by Spring’s async exception handler and is not automatically replayed.

Configuration

Property / environment variable Default Purpose
product.throttle.account-id / CLOUDFLARE_ACCOUNT_ID Empty Cloudflare account owning the KV namespace.
product.throttle.namespace-id / CLOUDFLARE_THROTTLE_NAMESPACE_ID Empty Existing KV namespace for exhaustion markers.
product.throttle.api-token / CLOUDFLARE_KV_API_TOKEN Empty Server-side API token with KV read/write access scoped to the namespace.
product.throttle.api-base-url https://api.cloudflare.com/client/v4 REST origin; tests use a local HTTP stub.
product.throttle.request-timeout / THROTTLE_KV_TIMEOUT 3s Connect and operation timeout.
product.throttle.admission-percent / THROTTLE_ADMISSION_PERCENT 20 Percentage admitted after exhaustion.
product.throttle.key-prefix / THROTTLE_KEY_PREFIX usage-exhausted Namespace prefix; separate environments with separate namespaces or prefixes.
product.usage-check.free-monthly-allowance / FREE_MONTHLY_USAGE 3000 Allocation for newly created free monthly grants.
product.usage-check.worker-count / USAGE_WORKER_COUNT 1 Accounting worker count.
product.usage-check.queue-capacity / USAGE_QUEUE_CAPACITY 10000 Maximum queued accounting events.

Configure the Cloudflare identifiers and token to enable shared enforcement. No remote namespace or production schema was changed by this implementation.

Local unlimited mode

The local profile sets product.throttle.enabled: false and product.usage-check.enabled: false. Calls still require a valid API key, but skip KV admission and marker updates. The gateway publishes no usage events, and the usage listener ignores directly delivered events. Signup skips free quota allocation; subscription creation skips quota snapshots. Existing usage history remains readable. Both switches default to true outside the local profile. Restart the backend after changing these properties.