Forge components use the shared middleware in src/middleware/rateLimit.ts.
Counters are durable D1 fixed-window rows and are partitioned by component,
policy class, stable principal (or client IP), and window start.
The complete checked-in policy lives in
src/middleware/rateLimitPolicy.ts. Route classification and numeric defaults
must not be duplicated in route handlers.
| Profile | Requests | Window | Identity | Applies to |
|---|---|---|---|---|
account-create |
5 | 1 hour | IP | Exact POST /api/auth/register |
repository-create |
30 | 1 hour | account, then user/IP fallback | Exact POST /api/repos and POST /api/orgs/:org/repos |
gist-write-authenticated |
120 | 1 hour | account | Authenticated Gist mutations |
gist-write-anonymous |
20 | 1 hour | IP | Anonymous Gist mutations |
git-read |
12,000 | 1 hour | account, then user/IP fallback | Git clone/fetch and LFS reads |
git-lfs-transfer |
12,000 | 1 hour | account, then user/IP fallback | Signed LFS byte upload/verification |
git-write |
300 | 1 hour | account, then user/IP fallback | Git push and Git metadata mutations |
api-write-authenticated |
300 | 1 minute | account | Other authenticated API mutations |
api-write-anonymous |
180 | 1 minute | IP | Other anonymous API mutations |
api-read-authenticated |
600 | 1 minute | account | Other authenticated API reads |
api-read-anonymous |
300 | 1 minute | IP | Other anonymous API reads |
Repository creation is deliberately an exact collection-route match. Actions,
agents, invitations, releases, sites, imports, and every other nested
/api/repos/... mutation use the ordinary write profile.
Health checks and non-API frontend assets are excluded. Git Smart HTTP is
always classified even though its paths do not begin with /api/.
X-Forge-Signup-Invite skips only the account-create limiter when its
SHA-256 digest matches the SIGNUP_INVITE_CODE_SHA256 identity Worker secret.
Alpha capacity and all registration validation still apply.
Configure or rotate the reusable code without committing it:
printf '%s' "$CODE" | shasum -a 256 | awk '{print $1}'
npx wrangler versions secret put SIGNUP_INVITE_CODE_SHA256 \
--config packages/identity/wrangler.toml
# Paste the digest from the first command at Wrangler's secret prompt.This creates an undeployed Worker version. Release it through Forge's normal
versioned deployment path; do not use wrangler secret put, which attempts an
immediate deployment and is rejected while a newer uploaded version exists.
FORGE_RATE_LIMIT_OVERRIDES optionally replaces request counts for a Worker.
Its value is a JSON object whose keys are the profile names above and whose
values are integers from 1 through 100,000:
{
"repository-create": 45,
"api-read-authenticated": 900
}Overrides are service-local because each component owns its counters. Unknown profiles, malformed JSON, non-integers, and out-of-range values reject the entire override and make the Worker use checked-in defaults. Windows, identity scope, and telemetry class remain checked in so semantic bucket changes still receive code review.
Append-only personal-account caps remain available to operators for supported authenticated classes. A personal cap can only lower the effective global limit and cannot change its fixed window.
Every admitted or rejected request includes:
X-RateLimit-Profile: the explicit profile selected for the requestX-RateLimit-Limit: requests allowed in the current windowX-RateLimit-Window: window duration in secondsX-RateLimit-Remaining: remaining requestsX-RateLimit-Reset: Unix timestamp when the fixed window resetsA rejected request also includes Retry-After and returns HTTP 429:
{
"error": "Too many requests",
"message": "Rate limit exceeded. Try again in 45 seconds.",
"retryAfter": 45
}The middleware runs after optional authentication. Authenticated requests use the durable personal-account identity across IP changes; anonymous traffic and registration use client IP. Rate-limit failures never expose another account's warning or enforcement data.