Skip to content
GantryCD

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_CIDRS for 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

  • disabled is valid for local development and tests.
  • redis is the expected production mode.
  • The Redis client is created at startup, but startup connectivity is not treated as a hard requirement beyond URL parsing.