Skip to content

Consumer quotas ​

A quota is a budget, not a rate: it caps the total number of requests one consumer may make inside a fixed UTC calendar window -- a day (midnight to midnight UTC) or a month (the first through the last instant of the UTC month). The rate limiter shapes traffic inside seconds or minutes; a budget bounds total volume. The two are separate by design and compose: when both are configured, both apply (rate limiting is covered in Configuration).

A quota never replenishes inside its window. It resets whole at the boundary -- the daily budget is full again at UTC midnight.

Quotas count REQUESTS. AI routes have their own budget family that counts provider-reported TOKENS per minute (and spend per day) per consumer or team -- see Token budgets in the AI gateway guide.

When to use this ​

Use quotas when the unit of control is total volume per billing period rather than bursts per second:

  • Per-tenant request allowances (10k/day on the free tier).
  • Monthly API commitments with automated enforcement.
  • Hard spend ceilings for batch/CI consumers that legitimately burst.

Use rate limits (not quotas) when the concern is protecting upstreams from load spikes.

Configuration ​

Budgets are declared on config consumers:

yaml
consumers:
  - name: acme-prod
    credentials:
      - type: api_key
        key: ${ACME_PROD_KEY}
    quotas:
      daily_requests: 1000000
      monthly_requests: 20000000
FieldTypeDescription
daily_requestsu64Max requests per UTC calendar day (midnight to midnight).
monthly_requestsu64Max requests per UTC calendar month.
dry_runboolWhen true, log would-be rejections but do not deny the request. Default: false. See Dry-run mode below.

At least one budget must be set, and every set budget must be greater than zero -- a budget of 0 would deny the consumer's first request, so "no budget" is expressed by omitting the field (validation enforces both).

The state store is required ​

Enforcement needs the durable state store for its counters: start the gateway with DWARA_STATE_DB pointing at the SQLite file (see environment variables). Without a store, quota config is inert and the request path logs that enforcement is off.

Scope of this implementation: counters are per instance (local SQLite truth), and budgets apply to config-declared consumers -- a fleet-wide shared counter is the enterprise follow-up. If you run several gateway instances behind one load balancer, each instance counts its own share of a consumer's traffic; take that into account or enforce budgets at a single choke point.

Enforcement ​

When a consumer's request would exceed a budget, the gateway answers 429 with the same header family rate-limit 429s carry:

  • Retry-After -- seconds until the window resets (rounded up, minimum 1).
  • X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset -- the binding budget's size, what is left of it, and the Unix epoch second at which it resets.

The gateway is the source of truth for the X-RateLimit-* family: any upstream values are silently stripped, so the client always sees the gateway's own accounting.

Dry-run mode ​

Set dry_run: true on a consumer's quotas block to evaluate the budget and log would-be rejections without denying requests:

yaml
consumers:
  - name: free-tier
    credentials:
      - type: api_key
        key: ${FREE_TIER_KEY}
    quotas:
      daily_requests: 1000
      dry_run: true

In dry-run mode, would-be 429s are:

  • Counted in dwara_policy_dry_run_total{phase="quota",route}.
  • Logged with code = "policy_dry_run" and the consumer and budget that would have been exceeded.
  • NOT returned to the client -- the request proceeds normally.

The state-store counters still increment in dry-run mode (the budget is evaluated against real usage). This lets you measure what the enforcement impact would be before switching to dry_run: false.

Durability and cost ​

Every accepted request of a quota-configured consumer performs one or two synchronous state-store writes (a daily and/or monthly counter increment), committed before the request proceeds -- so a crash can lose at most the request that was in flight, never a committed counter. The store runs SQLite's default synchronous=FULL (an fsync per commit): budgets are billed-grade durable, and the per-request fsync on the single store connection is the documented price. For consumers that do not need second-grade ceilings, prefer rate limits (in-memory) and keep budgets for the volumes where the durability trade is worth it.

Reading usage ​

GET /quotas/usage on the admin API reports every quota-configured consumer's current-window counters (or the one named by the optional ?consumer= parameter):

sh
curl --cert operator.crt --key operator.key \
  https://127.0.0.1:2019/quotas/usage
json
{
  "now_epoch_s": 1770000000,
  "consumers": [
    {
      "consumer": "acme-prod",
      "synced": true,
      "budgets": [
        {
          "budget": "daily",
          "limit": 1000000,
          "used": 482113,
          "remaining": 517887,
          "window_start_epoch_s": 1769961600,
          "reset_epoch_s": 1770048000
        }
      ]
    }
  ]
}

reset_epoch_s is the same instant a budget 429's X-RateLimit-Reset advertises. A consumer whose store row is missing reports synced: false with no budgets -- no counters exist for it yet, and reporting zero usage would be a lie.

Errors: 404 state_store_not_configured when the gateway runs without DWARA_STATE_DB, and 400 quota_bad_consumer when ?consumer= names a consumer that declares no quotas block.

Usage reports ​

Scheduled usage statements carry the quota figures a billing pipeline consumes: the exports' quota_daily_used / quota_daily_limit / quota_monthly_used / quota_monthly_limit columns come from these same state-store counters (absent -- never zero -- when the consumer has no budgets). See Usage reports and exports for the windowing, formats, and the manual trigger.

Runnable demo ​

Run this feature against a live gateway: demos/03-resilience/ (test script: test-13-quotas.sh) in the repository. The category README covers prerequisites and teardown.