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 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
- Authenticate the customer key against PostgreSQL. Missing, unknown or revoked keys return
401 INVALID_API_KEYbefore KV or api-app access. - Read Cloudflare KV key
usage-exhausted:<userId>:<monthStartUnixSeconds>from the configured namespace. - Absent marker: forward all traffic. Present marker: admit approximately 20%, return
429 REQUEST_THROTTLEDfor the remainder. Safe rejection text: “Request throttled. Please try again.” All keys for that user share the marker; another user’s traffic is independent. - Forward admitted requests normally, replacing the customer key with the upstream shared secret.
- Publish a usage event after the upstream response. Its server-generated transaction UUID deduplicates recording; its occurrence time determines the UTC accounting month.
- 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.
- 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.
Related code
- Usage service: monthly grants, subscription allowance activation, event accounting and exhaustion decisions.
- Usage repository: owner-scoped queries and idempotent ledger writes.
- Gateway throttler: account-level admission and usage-facing marker API.
- Cloudflare adapter: KV REST reads, writes and deletion.
- Gateway service: authenticated routing and completed-call events.
- Schema: PostgreSQL storage and relationships.
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.