Skip to content
Arbinel

Request budgets

Request budgets limit how often one account can make an expensive type of request, such as downloading an archive or rendering a large diff. They protect a shared instance from a single script or user that overloads it with costly requests. All budgets are disabled by default.

Each budget is a section named [codefloe.ratelimit.<name>]. Arbinel’s routes use four names:

Name What it counts
archive GET downloads of repository archives on the web and through the API, including tarball, zipball and bundle. Each attempt costs one unit, even for an archive that is already cached or a ref that does not exist. Starting and polling an archive in the browser is free.
code-search Code searches on the explore page, on user and organization pages, and in a repository, including the Git grep fallback. An empty search page is free.
issue-filters Issue and pull request lists of a repository that filter or sort, on the web and in the API, and every cross-repository API issue search (/api/v1/repos/issues/search). Plain open and closed lists are free. The dashboard lists are not counted.
compare-diff Compare pages, commit diffs, pull request file views, and raw .diff and .patch downloads, on the web and in the API. Each GET costs one unit.

For example, to allow each account 60 diffs every 10 minutes:

[codefloe.ratelimit.compare-diff]
ENABLED = true
AUTHENTICATED_REQUESTS = 60
WINDOW_SECONDS = 600
Key Default Meaning
ENABLED false Turn the budget on.
OBSERVE_ONLY false Count requests and report what would be refused, without refusing anything.
AUTHENTICATED_REQUESTS 60 Requests each signed-in account may make per window. Must be at least 1.
WINDOW_SECONDS 60 Length of the window, from 1 to 31536000 seconds.
ANONYMOUS_REQUESTS 0 Requests each IP address may make per window without signing in. 0 means no limit for anonymous requests.
EXEMPT_ACCOUNTS empty Comma-separated account names that are never limited.

A section with an invalid name, value or boolean stops Arbinel at startup. A section with a name that no route uses has no effect.

custom/conf/app.example.ini in the Arbinel repository suggests a 600-second window for code-search, issue-filters and compare-diff.

  • Signed-in requests count against the account, so its browser sessions and API tokens share one allowance.
  • Anonymous requests count against the client IP address. Behind a reverse proxy, set Forgejo’s [security] REVERSE_PROXY_LIMIT and REVERSE_PROXY_TRUSTED_PROXIES so Arbinel sees the real address. Otherwise every anonymous client shares the proxy’s address.
  • Windows are fixed, not sliding. A 600-second window starts at each multiple of 600 seconds since the Unix epoch, and the count resets when it ends.
  • A request is counted before the work starts, so a refused request costs the server almost nothing.
  • Site administrators and the accounts in EXEMPT_ACCOUNTS are never limited.

A refused request gets HTTP 429 with the plain text body request budget exhausted, on the web and in the API alike. The response carries these headers:

Header Value
Retry-After Seconds until the window ends.
RateLimit-Limit The budget for this window.
RateLimit-Remaining Units left, usually 0.
RateLimit-Reset Seconds until the window ends.

If Arbinel cannot reach the cache with the counters, enforcing budgets return HTTP 503 with request budget unavailable. Budgets in OBSERVE_ONLY mode let the request through instead.

Arbinel keeps the counters in the cache configured under Forgejo’s [cache] section.

  • redis shares the counters between all processes and nodes. Use it in high availability setups.
  • memory and twoqueue count per process, so each node has its own allowance. twoqueue may evict a counter under memory pressure, which resets that budget until the window ends.
  • memcache is not supported. With memcache, every enforcing budget returns 503.
  1. Turn a budget on with OBSERVE_ONLY = true.
  2. Watch the metrics below for a few days to see how many requests would be refused.
  3. Adjust AUTHENTICATED_REQUESTS and WINDOW_SECONDS, then set OBSERVE_ONLY = false.

With [metrics] ENABLED = true, /metrics reports:

Metric Labels Meaning
codefloe_ratelimit_requests_total policy, mode, result Counted requests. result is admitted, refused, would_refuse or error. mode is enforce or observe.
codefloe_ratelimit_units_total policy, mode, result The same in units.
codefloe_ratelimit_refusals_total policy Refused requests.
codefloe_ratelimit_refunded_units_total policy, mode Units given back for work that never started.

Requests from exempt accounts, and requests while a budget is off, are not counted.

These need no configuration.

  • Outbound HTTP calls to reCAPTCHA, Turnstile, hCaptcha and Have I Been Pwned time out after 10 seconds. In Forgejo they have no timeout. Arbinel also reads only a bounded amount of the reCAPTCHA, Turnstile and update checker responses.
  • On Linux, pull mirror syncs run their Git commands at the lowest CPU priority and the idle I/O class, so they do not slow down interactive Git traffic.
  • Code search with zoekt stops after 2 seconds and returns at most 1,000 files. See Search.