Software Factories on AWS

Self-hosted platform for standing up and running software factories on AWS: complete DevSecOps environments with secure delivery pipelines, supply-chain controls, compliance evidence and multi-tenant governance — in minutes, not months.

API Quotas

How every service meters and enforces the per-period call budgets — apiCalls for people, automationCalls for automation — which routes count, which deliberately don’t, and what a client sees when a budget runs out.

Highlights

Overview

The quota service holds one counter per org per quota type per period (QUOTA_RESET_DAYS, 3 days by default), pooled at the account root for accounts with teams. Tier baselines and add-on bundles set the limits. This page covers the two call budgets; the count quotas (pipelines, plugins, dashboards, …) are reserved atomically by the route that creates the thing and are described with their features.

Budget Spent by Raised by
apiCalls People — browser sessions Tier, API call packs, Member Seat
automationCalls Non-interactive credentials — org service accounts (pb_sa_…: self-mutating pipelines, registration, reporting ingest, registry push) and personal access keys (pb_pat_…, and any token minted while holding one) Tier (sized for automation), Automation Packs
aiCalls Any AI generation or Ask request — in addition to the apiCalls/automationCalls the call itself costs Tier, AI call packs

How it works

Where it is mounted

Each service mounts api-server’s apiCallsQuota(quotaService) once per URL prefix, at the top of its app-routes.ts, ahead of every router under that prefix. It is two middlewares:

  1. The meter (meterQuotaOnSuccess) counts one unit on response finish, only for a 2xx, only for a verified caller, never for an idempotency-cache replay. Each unit carries attribution — principal, key, route pattern, pipeline — for the usage ledger.
  2. The check (checkQuota) is armed at the mount and run by requireAuth the moment it has verified the caller. Authentication is per route (a router mixes public, user and machine routes), so at mount time there is no verified org to check; checking an unverified one would leak a victim org’s usage in the 429 body. A route that never authenticates never runs it.

What is not counted

Both halves skip the same requests (api-core callQuotaExemption), so the gate never refuses a call the meter would not bill:

Hot-path cost: check cache, fast timeout, batched increments

Automation calls

automationCalls defaults are sized from an itemized count of what one pipeline run calls with its keys — 12 plugin lookups, 1 synth, 1 runtime check, 1 security-report upload, 1 registry register and ~12 reporting-ingest batches = 28 calls per run — at hourly runs for the 3-day period (2,016 per active pipeline), for 4 active pipelines per tier pipeline creation, plus the per-org ingest heartbeat (4,320 per period), with 25% headroom, rounded up to 10,000:

Tier Formula Default
Developer (2×4×2,016 + 4,320) × 1.25 30,000
Pro (5×4×2,016 + 4,320) × 1.25 60,000
Team (10×4×2,016 + 4,320) × 1.25 110,000
Business (20×4×2,016 + 4,320) × 1.25 210,000
Enterprise (30×4×2,016 + 4,320) × 1.25 310,000

The cap is soft: at 100% the account gets the “reached” notice and automation keeps running up to QUOTA_AUTOMATION_GRACE_PCT (10%) over the limit; the 429 starts beyond that band. A self-mutating pipeline that hard-stopped at exactly 100% stayed stopped until the period reset — up to three days — so the band buys time to add an Automation Pack. Every other quota stays hard.

Service Prefixes covered
platform /user, /users, /organization (incl. support contacts), /invitation, /audit, /observability, /dashboards
pipeline /pipelines, /pipeline-templates
plugin every user prefix (plugins, installs, publisher, reviews, queue status, pipeline security, exposures, triage, runtime check, security notifications, kept setup images)
message /messages
compliance /compliance
reporting /reports/* user surfaces (settings, retention, deployments, stakeholder reports, executions, plugin reports, ingest health / incident admin reads)
image-registry /api/images

Not metered: platform’s /auth* (sign-in, refresh, MFA, OAuth, SSO, device flow — a locked-out org must still be able to sign in), /scim/v2 (directory sync has its own per-org bucket), /config, every /internal/* route, the system-org and vendor /admin surfaces, unauthenticated public routes, the quota service itself (reading your quota is always free) and billing.

Machine ingest: metered, never refused

POST /reports/events, POST /reports/ingest-health and the incident webhooks (POST /reports/incidents, …/alertmanager) carry the reporting:ingest scope. They meter (a service-account key spends automationCalls) but are not checked. Dropping CI telemetry or an incident because a budget ran out loses deploy and incident history that can’t be re-sent, and corrupts DORA; overshooting a call budget is the lesser harm, and the meter still makes the volume visible and billable.

Exempt routes

The console makes these calls on every page load or on a timer. Metering them would burn an idle tab’s budget all day, and checking them would leave a quota-exhausted org unable to render the banner telling it so. The list lives in api-core (QUOTA_EXEMPT_ROUTES) and is read by the meter, the check and the route table alike:

Route Why it is free
GET /user/profile Session bootstrap, every page load
GET /user/organizations The org switcher, every page load
GET /organization/:id/feature-entitlements Navigation gating, every page load
GET /organization/:id/seat-usage Billing/quota page read beside the (free) quota reads
GET /messages/unread/count Sidebar badge, polled every 30s when the live channel is down
POST /messages/notifications/ticket SSE ticket, re-minted on every reconnect
POST /reports/execution/stream/ticket SSE ticket, re-minted on every reconnect

The quota banner’s own poll (GET /quotas, every 60s) needs no entry: the quota service meters nothing.

When a budget runs out

Every refusal — from the check or from a reservation — is the same response:

HTTP/1.1 429 Too Many Requests
Retry-After: 172800
X-Quota-Limit: 25000
X-Quota-Used: 25000
X-Quota-Remaining: 0
X-Quota-Reset: 2026-11-05T00:00:00.000Z

{ "success": false, "code": "QUOTA_EXCEEDED", "message": "…",
  "details": { "quotaType": "apiCalls", "limit": 25000, "used": 25000,
               "resetAt": "2026-11-05T00:00:00.000Z", "upgradeUrl": "/dashboard/billing", "quota": { … } } }

Retry-After is the time to the period reset, not a back-off hint. A 429 without code: QUOTA_EXCEEDED is an ordinary rate limit.

Threshold notices

Crossing 80% and 100% of a budget sends one notice per quota per period to the account root: an inbox message (message service), an email to the root’s admins/owners and billing contacts (members whose Roles grant billing:manage, via platform’s email relay), and a quota.threshold webhook when configured. Opt out on the root’s Quotas page (PUT /quotas/:orgId/notice-settings). Each quota pod remembers which notices it has already seen claimed this period, so an org pinned at its cap costs no notice lookups per refusal.

Usage ledger

Usage is stored as hourly buckets (quota_usage_buckets): one document per org, hour, quota type, outcome and (principal, key, route, pipeline), incremented in place. Outcomes are counted, denied (a reservation refused at the cap), overshoot (metered calls already served that did not fit under the cap) and refunded. Denials, overshoots and refunds additionally keep a raw per-event row with the request id (quota_usage_events). GET /quotas/:orgId/usage and its CSV read the buckets, so a range’s start is rounded down to its hour. Volume scales with the number of distinct active (principal × route × pipeline) combinations per hour, not with requests — about 32 MB for an account with 50 active combinations over the default 90-day retention (QUOTA_USAGE_EVENT_RETENTION_DAYS).

Configuration

Setting Effect
QUOTA_TIER_<TIER>_API_CALLS, QUOTA_TIER_<TIER>_AUTOMATION_CALLS Per-tier budgets (Environment variables)
QUOTA_RESET_DAYS Period length
QUOTA_AUTOMATION_GRACE_PCT Soft band above the automationCalls cap (default 10, 0 = hard)
QUOTA_CHECK_CACHE_TTL_MS, QUOTA_CHECK_DENIED_CACHE_MS Per-pod check cache (5 s allowed / ≤ 60 s denied)
QUOTA_CHECK_TIMEOUT_MS Check read budget before failing open (300 ms)
QUOTA_INCREMENT_FLUSH_MS, QUOTA_INCREMENT_BUFFER_MAX Increment batching interval (1 s; 0 = per request) and buffer bound
QUOTA_USAGE_EVENT_RETENTION_DAYS Usage-ledger retention (buckets and raw rows)
QUOTA_THRESHOLD_NOTICES_ENABLED Inbox/email/webhook threshold notices

Adding a route. Put it under a prefix that already mounts apiCallsQuota, or mount it for the new prefix. Never add a per-route meterQuotaOnSuccess(…, 'apiCalls') under a covered prefix — the route-coverage test fails a route metered twice. A route that genuinely must not be metered or checked needs a QUOTA_EXCEPTIONS entry with its reason in that service’s test/route-coverage.test.ts; a new console-polling read belongs in QUOTA_EXEMPT_ROUTES. The generated route tables (frontend/src/generated/route-table/*.json) show each route’s quota: { metered, checked, exempt }.