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.
Configure a budget
Section titled “Configure a budget”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:
| 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.
Counting
Section titled “Counting”- 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_LIMITandREVERSE_PROXY_TRUSTED_PROXIESso 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_ACCOUNTSare never limited.
Refused requests
Section titled “Refused requests”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.
redisshares the counters between all processes and nodes. Use it in high availability setups.memoryandtwoqueuecount per process, so each node has its own allowance.twoqueuemay evict a counter under memory pressure, which resets that budget until the window ends.memcacheis not supported. Withmemcache, every enforcing budget returns 503.
Roll out a budget
Section titled “Roll out a budget”- Turn a budget on with
OBSERVE_ONLY = true. - Watch the metrics below for a few days to see how many requests would be refused.
- Adjust
AUTHENTICATED_REQUESTSandWINDOW_SECONDS, then setOBSERVE_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.
Other protections
Section titled “Other protections”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.