Rate Limiting
Rate limiting is configured in the backend and applied in the router through middleware wrappers.
Backend Configuration
The backend startup config requires:
RATE_LIMIT_MODE=redis- or
RATE_LIMIT_MODE=disabled
When RATE_LIMIT_MODE=redis, REDIS_URL is required.
Optional:
RATE_LIMIT_TRUSTED_PROXY_CIDRSfor trusted proxy networks
The backend refuses startup on invalid mode values.
How It Is Applied
The router wraps handlers with middleware.WrapRateLimit(...).
Patterns used in internal/backend/server/router.go:
- anonymous/login routes: bucket by client IP
- session-auth routes: bucket by authenticated user ID
- runner-group routes: usually two layers, one by client IP and one by runner group ID
- ephemeral-runner routes: usually two layers, one by client IP and one by runner ID or run ID
This means runner APIs are protected both against broad IP abuse and against a single actor hammering its own endpoint.
Identifier Sources
Common identifier functions:
middleware.ClientIPIdentifier(resolver)middleware.ContextIdentifier(contextutil.GetUserID)middleware.ContextIdentifier(contextutil.GetRunnerGroupID)middleware.ContextIdentifier(contextutil.GetRunnerID)middleware.ContextIdentifier(contextutil.GetRunID)
Never parse X-Forwarded-For directly in route code. Use the configured resolver.
Policies
Policy definitions live under internal/backend/ratelimit/ and are selected per route in the router.
Examples:
- login policy
- shared session API policy
- runner-group poll / heartbeat policies
- ephemeral ready / accept / status / live-logs / done / cancel / deregister policies
Login is special-cased because it needs behavior different from the generic route wrapper.
Per-org runs/hour dispatch budget
PolicyOrgRunsPerHour(runsPerHour) is the one policy not applied by the HTTP
route wrapper. It is consumed inside RunnerGroupService.PollForWork (keyed on
org id) to meter how fast queued runs are handed out to an org’s runner groups —
see runner polling. Unlike the
per-minute route policies it runs on an hourly interval with a burst of
runsPerHour × 24 (a fixed 1-day window), so an idle org can peak a full day’s
budget in one hour and then sustains the hourly rate as tokens refill
continuously. The rate is the per-org purchased value
organizations.runs_per_hour_limit (default 2), configured by super-admins in
Backstage; the window is a fixed constant (OrgRunsWindowHours). The service is
handed the same shared ratelimit.Store as the router, so the disabled mode
(NopRateLimiter) turns the throttle off along with every other limiter.
Operational Notes
disabledis valid for local development and tests.redisis the expected production mode.- The Redis client is created at startup, but startup connectivity is not treated as a hard requirement beyond URL parsing.