Vetrix Docs

Rate Limits

Vetrix rate-limits every request through the same middleware regardless of transport. This document is the single reference for API clients, git users, registry clients, and the analytics beacon. Operators configure budgets from the admin UI (/admin/rate-limits); this doc describes what callers see on the wire and how to request an override.


Scopes and default budgets

Every request is stamped with one of seven scopes. Budgets are held per user group × scope in user_group_rate_limits and can be changed at runtime without a deploy. The table below is the launch default (also the value the admin "Reset to defaults" button writes back).

Scope Meaning Anonymous General Admin
api.read Read-only JSON API (GET /api/v1/...) 60 / min 600 / min 6000 / min
api.write Mutating JSON API (POST/PATCH/PUT/DELETE) 10 / min 120 / min 1200 / min
api.expensive Search, analytics, browse-fused endpoints 5 / min 30 / min 300 / min
git.read git clone / git fetch over HTTP or SSH 30 / min 300 / min 3000 / min
git.write git push over HTTP or SSH disabled (anonymous cannot push) 60 / min 600 / min
registry /v2/..., /npm/..., /pypi/..., /go/..., generic packages 60 / min 300 / min 3000 / min
beacon Frontend analytics beacon 120 / min 600 / min 6000 / min

Admin-tier rows carry action="allow": admins are metered but never throttled. Anonymous git.write is seeded with enabled=false (not a quota of zero): anonymous pushes are rejected by ACL before the limiter ever looks at them.

Window size is 60 s and burst is roughly 10 % of the RPW for every seeded row. Operators can change all five fields (requests_per_window, window_seconds, burst, action, enabled) per group × scope.


Header contract

Throttled and metered-but-allowed requests both carry the five response headers below. A request that falls through the master switch (ratelimit.enabled=false) or Redis fail-open path does not carry them — absence of X-RateLimit-* on a 2xx is a signal that the request was not metered, not that it was.

Header Value
X-RateLimit-Limit Requests allowed in the current window
X-RateLimit-Remaining Requests remaining before deny
X-RateLimit-Reset Unix epoch seconds when the bucket refills
X-RateLimit-Scope Scope name from the table above
Retry-After Seconds until the next request will succeed (present only on deny)

Retry-After is seconds (RFC 6585) — ceiling of the bucket's next refill, minimum 1.


Per-transport deny shape

Different clients expect different failure shapes. The limiter decision is identical; only the response envelope changes.

JSON API and registry — 429 Too Many Requests

$ curl -i -H "Authorization: Bearer $TOKEN" https://api.gitvetrix.com/api/v1/repos/alice/myrepo/issues
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714082400
X-RateLimit-Scope: api.read
Content-Type: application/json

{"error":"rate limited","retry_after_seconds":42,"scope":"api.read"}

docker pull / npm install / pip install / go get all treat 429 as a transient back-off and will retry automatically once Retry-After has elapsed.

Git smart-HTTP — 503 Service Unavailable (429 remap)

The git smart-HTTP protocol has no native 429 semantics. Git clients treat 503 as a transient failure and back off exactly as desired, so the limiter emits 503 on this surface (not 429).

$ git clone https://www.gitvetrix.com/alice/myrepo.git
Cloning into 'myrepo'...
fatal: unable to access 'https://www.gitvetrix.com/alice/myrepo.git/':
The requested URL returned error: 503

The response carries Retry-After and the body is the single line rate limited\n. The info/refs handshake is deliberately excluded from metering so a single git clone consumes one budget call, not two.

Git SSH — stderr message + exit 128

Git over SSH has no HTTP back-channel; the server writes a human-readable line to stderr and closes the channel with exit status 128 (git's convention for fatal transport errors).

$ git clone git@gitvetrix.com:alice/myrepo.git
Cloning into 'myrepo'...
rate limited: retry in 42s
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

The violation row in the admin UI shows http_method="" and http_path="ssh:git-upload-pack" (or ssh:git-receive-pack) so SSH traffic is filterable separately from HTTP.

Analytics beacon — 429 with the same headers

The beacon is just another API endpoint; it uses the scope beacon rather than api.write so the limiter can be tuned per-scope without affecting write-API traffic. Frontend code surfaces a non-blocking toast on 429 and drops the event.


Shadow mode

On first deploy Vetrix ships with ratelimit.shadow_mode=true. Every decision still runs; denies are logged to rate_limit_violations with action_taken="shadow" but the request is allowed through without any X-RateLimit-* headers or 429. Operators use this for a 48-hour soak: verify violation counts look sane in /admin/rate-limits/violations before flipping the master switch.

Flip via /admin/settings or PATCH /api/v1/admin/settings with {"ratelimit.shadow_mode":"false"}.


Asking for an override

Budgets can be narrowed (never widened) per-repo and per-OAuth2-app. The override is min(group, repo_override, app_override) — removing either override restores the group budget.

Repo admins open the repo's Settings → Rate Limits tab:

  • https://www.gitvetrix.com/<owner>/<repo>/settings/rate-limits
  • Requires repo:rate_limit permission (Repo Admin, Org Owner, or Instance Admin by default).
  • CRUD is end-to-end there — no ticket needed.

Instance admins manage group budgets and OAuth2-app overrides under /admin/rate-limits:

  • Group matrix: 3 tiers × 7 scopes, editable per cell.
  • Overrides landing: repo and OAuth2-app overrides with create / detail / delete.
  • Violations feed + live tail + CSV export.

Non-admin override requests go through email. The "request an override" link on the repo settings page opens a mailto: with a pre-filled subject; operators route these into the normal support flow. There is no in-app request-and-approval workflow.


Legacy knobs

oauth2_apps.rate_limit_per_min was the per-OAuth2-app cap before the rate-limit subsystem existed. It is preserved for schema compatibility but is ignored by the middleware. Canonical configuration lives in rate_limit_oauth_app_overrides, editable from the admin UI / API under /admin/rate-limits/overrides/apps.