Guides

Interpreting Quota Decisions

Intermediate~6 min

How to read QuotaDecision responses, the headers attached by rate-limited endpoints, and the retry contract your integration should implement.

Prerequisites

  • A Meteorack workspace with at least one active subscription.
  • Familiarity with calling `/api/v1/*` endpoints with an API key.

What You Will Finish With

  • Map each `action` value to a concrete client response.
  • Distinguish `reject` from `throttle` from `degrade` and act accordingly.
  • Honor `retry_after_seconds` without hammering the rate limiter.
  • Cache decisions for at most `cache_ttl_seconds` before re-evaluating.

Every entitlement-gated endpoint on api.meteorack.com returns a QuotaDecision — either as the JSON body of the response (for /api/v1/entitlements/:productKey style probes) or via response headers stamped by apps/api's rate-limit middleware. This guide covers both surfaces.

Decision shape

{
  "workspace_id": "11111111-1111-1111-1111-111111111111",
  "meter_key": "api.requests_per_second",
  "action": "allow",
  "reason_code": 1000,
  "reason_name": "ALLOWED",
  "effective_limit": 100,
  "current_usage": 42,
  "included_quantity": 100,
  "unit": "req/s",
  "entitlement_version": "ev_1234567890abcdef",
  "evaluated_at": "2026-04-22T20:00:00.000Z",
  "cache_ttl_seconds": 1
}

Fields you must read on every decision:

fieldwhy
actionprimary signal — see the table below
reason_codestable numeric ID; route on this, not reason_name
entitlement_versionopaque cache key; invalidate if it changes
cache_ttl_secondsupper bound on how long to trust this decision client-side

Fields you may display to end users:

fieldwhy
effective_limit"5 of 10 seats used" UI
current_usagesame
included_quantityplan-bundled baseline vs. overrides
unithuman label (req/s, bytes, seats, …)

Action matrix

The five possible action values map to distinct client behaviors. This is the only dispatch table you should read — never branch on reason_code alone, because ranges are extensible (see below).

actionyour behavior
allowProceed. No retry, no delay.
warnProceed, log the decision. The response carries DEGRADED_MODE_APPLIED (9002) or ALLOWED_WITH_WARNING (1001) to signal operator-visible degradation. Consider showing a "you're close to the limit" UI.
throttleProceed later. Honor retry_after_seconds. Typical reason: THROTTLED_PLAN_RATE (2000) or THROTTLED_BURST (2001).
degradeProceed with reduced feature set listed in degraded_features (optional). Typical reason: DEGRADED_PRESENCE_OVER_TIER (3000) or DEGRADED_FEATURE_DISABLED (3001).
rejectStop. Surface the error to the user. Typical reasons: REJECTED_PLAN_LIMIT (4001), REJECTED_SEAT_LIMIT (4002), REJECTED_SPENDING_CAP (4003), METER_UNKNOWN (9000).

Reason-code ranges (stable)

Number ranges have semantic meaning, so an unknown code from a newer engine version degrades gracefully:

rangemeaning
1xxxallow (success, optionally with warning)
2xxxthrottle (slow down)
3xxxdegrade (reduced features)
4xxxreject (refused)
9xxxengine-internal (cache staleness, unknown meter, degraded-mode overlay)

If you see a numeric code your client doesn't recognize, fall through to the range-based category rather than failing.

Response headers (rate-limited endpoints)

Endpoints that enforce a rate limit stamp three headers on every response, in parallel to returning the rate-limited payload as JSON:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1743465600

When action === "throttle", the engine additionally sets:

Retry-After: 3

Retry-After is in seconds, matching retry_after_seconds on the decision body. Use whichever is convenient; they are always equal.

Retry contract

statusretry?
allow, warnno retry (request succeeded)
throttleretry once after retry_after_seconds (+ a small jitter, 50–200 ms)
degradeno retry for the gated feature; the ungated path succeeds
rejectno retry — user action required (upgrade plan, request limit increase)

Never retry a reject on your own. The engine has determined the workspace cannot perform the action under its current entitlements. Retrying multiplies the refused cost with no upside.

Caching

The engine stamps cache_ttl_seconds (typically 1 for the hot path) on every decision. This is the maximum duration your client may re-use the decision without calling the engine again. It is safe to cache less aggressively than the TTL suggests.

For adapters that enforce tight budgets (e.g. per-request rate limits), the recommended pattern is:

  1. First request of a burst: call the engine, cache by (workspace_id, meter_key, entitlement_version).
  2. Subsequent requests within cache_ttl_seconds: apply the cached decision, decrementing a local counter.
  3. When cache_ttl_seconds elapses or entitlement_version changes (via a push-invalidation hook), re-evaluate.

If your adapter is event-driven and receives entitlement.changed broadcasts, invalidate immediately on the workspace you received the event for — do not wait for the TTL.

Degraded-mode behavior

The engine occasionally returns decisions with reason_code = 9002 DEGRADED_MODE_APPLIED. This indicates the live counter read failed (e.g. Dragonfly blip) and the engine fell back to the meter's configured degraded_mode:

meter degraded_modeobserved actionwhat the customer sees
fail_openunchanged (e.g. allow)normal behavior
fail_closedrejectrefused with DEGRADED_MODE_APPLIED
warnwarnallowed, with a "service degraded" log

Treat 9002 the same as the range would suggest — if action === "reject", don't retry; if action === "warn", proceed normally.

Failure modes

failureengine responseyour client
Engine timeout / 5xx504/503 from apps/apiretry once with backoff; if persistent, assume allow and log
Auth failure401re-authenticate
Meter not in catalogreject + METER_UNKNOWN (9000)check the meter key spelling; do not retry
Workspace not entitledreject + REJECTED_NOT_ENTITLED (4000)user must upgrade or activate the product

Self-serve limit increases

When your user hits REJECTED_PLAN_LIMIT / REJECTED_SEAT_LIMIT and wants headroom, surface a "Request increase" CTA that POSTs to /api/v1/entitlements/support-grants (authenticated as the workspace owner). This creates a pending grant_entitlement_support approval that a Meteorack operator 4-eyes-approves; on approval, the engine picks up the grant on its next evaluate cycle and the user sees the new effective_limit.

See the entitlement-engine grant-limit-bump runbook for the operator-side workflow.