Rate Limiting
Protect your Gordon instance from abuse with configurable rate limiting.
Overview
Rate limiting prevents:
- Brute force attacks on authentication endpoints
- Denial of Service (DoS) from excessive requests
- Resource exhaustion from misbehaving clients
- Registry abuse (e.g., automated scraping)
Rate limiting applies to:
- Registry API (
/v2/*) — Global + per-IP limits - Auth endpoints (
/auth/*) — Same limiter as registry (Global + per-IP) - Admin API (
/admin/*) — Global + per-IP limits (separate limiter instances)
Quick Start
Rate limiting is enabled by default with sensible defaults:
[api.rate_limit]
enabled = true
global_rps = 500
per_ip_rps = 50
burst = 100
trusted_proxies = []
Important: If you run Gordon behind a front proxy (Cloudflare, nginx, etc.), configure
trusted_proxiesso rate limiting sees real client IPs instead of your proxy's IP.
Configuration
[api.rate_limit]
enabled = true
global_rps = 500
per_ip_rps = 50
burst = 100
trusted_proxies = ["127.0.0.1", "10.0.0.0/8"]
Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable or disable rate limiting |
global_rps |
float | 500 |
Maximum requests per second across all clients combined |
per_ip_rps |
float | 50 |
Maximum requests per second per client IP address |
burst |
int | 100 |
Maximum burst size (requests allowed to exceed the rate temporarily) |
trusted_proxies |
[]string | [] |
IP addresses or CIDR ranges trusted to set X-Forwarded-For |
Architecture
Gordon uses two separate rate limiters:
┌─────────────────────────────────────────────────────────┐
│ Incoming Request │
└─────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ /v2/* routes │ │ /auth/* routes│ │ /admin/* routes│
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────┐
│ Registry Rate Limiter │ │ Admin Rate Limiter │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ Global Limiter │ │ │ │ Global Limiter │ │
│ │ (global_rps) │ │ │ │ (global_rps) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ Per-IP Limiters │ │ │ │ Per-IP Limiters │ │
│ │ (per_ip_rps) │ │ │ │ (per_ip_rps) │ │
│ └───────────────────┘ │ │ └───────────────────┘ │
└─────────────────────────────────┘ └─────────────────────────┘
Registry Limiter (for /v2/* and /auth/*):
- Checks global limit first (all clients combined)
- Then checks per-IP limit
- Both must pass for the request to proceed
- Auth endpoints share this limiter to prevent brute force attacks
Admin Limiter (for /admin/*):
- Separate global + per-IP limiters (independent instances)
- Isolates admin traffic from registry traffic
- Prevents CI/CD bursts from affecting admin access
Option Details
enabled
Master switch for rate limiting. When false, all requests are allowed without throttling.
[api.rate_limit]
enabled = false # Disable rate limiting (not recommended for production)
global_rps
The maximum number of requests per second that Gordon will accept from all clients combined. This is a hard cap that protects against distributed attacks where many IPs each send moderate traffic.
[api.rate_limit]
global_rps = 500 # 500 requests/second total
Sizing guidance:
- Small deployments (1-10 apps):
100-500RPS - Medium deployments (10-50 apps):
500-2000RPS - Large deployments (50+ apps):
2000-10000RPS
Consider your CI/CD patterns—parallel builds pushing multiple images can generate bursts of traffic.
per_ip_rps
The maximum requests per second allowed from a single client IP. This prevents any single client from monopolizing Gordon's resources.
[api.rate_limit]
per_ip_rps = 50 # 50 requests/second per IP
Sizing guidance:
- Normal Docker operations (pull, push) rarely exceed 10-20 RPS
- CI/CD pipelines with parallel jobs may need 50-100 RPS
- Automated tooling (vulnerability scanners, etc.) may need higher limits
burst
The token bucket burst size. Allows clients to temporarily exceed the rate limit for short bursts, which is useful for legitimate traffic patterns like:
- Initial connection setup (multiple manifest/blob requests)
- Parallel layer downloads
- Health check bursts
[api.rate_limit]
burst = 100 # Allow bursts of up to 100 requests
A burst of 100 with per_ip_rps = 50 means a client can send 100 requests instantly, then must wait ~2 seconds for the bucket to refill before another burst.
trusted_proxies
Critical for correct IP detection in production.
Gordon is a reverse proxy that routes requests to your containers. In production you can either terminate TLS directly in Gordon (server.tls_enabled = true) or place it behind a TLS-terminating proxy like Cloudflare, nginx, or a load balancer.
Internet → [Cloudflare/nginx] → Gordon → Containers
(HTTPS) (HTTP)
In this setup, Gordon sees all connections coming from your front proxy's IP, not the real clients. The proxy sets X-Forwarded-For to communicate the original client IP.
The problem: X-Forwarded-For can be spoofed. If Gordon trusted this header unconditionally, attackers could:
- Bypass per-IP rate limits by sending fake IPs
- Create unlimited rate limiter entries (memory exhaustion)
The solution: Gordon only honors X-Forwarded-For from IPs listed in trusted_proxies. For all other connections, it uses the direct connection IP.
[api.rate_limit]
trusted_proxies = ["127.0.0.1", "10.0.0.0/8"]
Supported formats:
- Single IP:
"192.168.1.1" - CIDR range:
"10.0.0.0/8" - IPv6:
"::1","fd00::/8"
If
trusted_proxiesis empty or misconfigured, all requests appear to come from your proxy's IP, making per-IP rate limiting ineffective (all clients share one limit).
Deployment Examples
Cloudflare (Recommended)
Cloudflare provides free TLS termination and DDoS protection. This is the recommended setup for production.
Cloudflare publishes their IP ranges at https://www.cloudflare.com/ips/
[api.rate_limit]
# Cloudflare IPs (verified 2026-01-18 - check cloudflare.com/ips for updates)
trusted_proxies = [
# IPv4
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22",
# IPv6
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32",
]
Note: Cloudflare IPs may change. Verify at https://www.cloudflare.com/ips/ periodically.
Local Development (No TLS)
For local development only, Gordon can be accessed directly without a proxy:
[api.rate_limit]
trusted_proxies = [] # Empty = use RemoteAddr directly
Warning: Never expose Gordon directly to the internet without TLS. Use this configuration only for local development.
Behind nginx (Same Host)
When nginx runs on the same machine as Gordon:
[api.rate_limit]
trusted_proxies = ["127.0.0.1", "::1"]
Behind nginx (Separate Host)
When nginx runs on a different server:
[api.rate_limit]
trusted_proxies = ["10.0.1.5"] # nginx server IP
Behind AWS Application Load Balancer (ALB)
ALB terminates TLS and forwards requests to your instances. Unlike Cloudflare, ALB IPs are dynamic and change as AWS scales the load balancer. You need to trust your VPC's private IP range (CIDR) since ALB connects from within your VPC.
[api.rate_limit]
trusted_proxies = ["10.0.0.0/16"] # Your VPC CIDR
For detailed setup instructions, see the AWS ALB Guide.
Behind Kubernetes Ingress
When running Gordon in Kubernetes with an ingress controller (nginx-ingress, Traefik, etc.), you need to trust the pod network CIDR. The ingress controller must also be configured to forward client IPs via X-Forwarded-For.
[api.rate_limit]
trusted_proxies = ["10.244.0.0/16"] # Your pod network CIDR
For detailed setup instructions, see the Kubernetes Ingress Guide.
Rate Limit Response
When a client exceeds the rate limit, Gordon returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Docker-Distribution-API-Version: registry/2.0
Retry-After: 1
{
"errors": [
{
"code": "TOOMANYREQUESTS",
"message": "rate limit exceeded"
}
]
}
The Retry-After header indicates when the client can retry (in seconds).
Monitoring
Rate-limited requests are logged at info level:
level=info msg="rate limit exceeded" client_ip=203.0.113.50 path=/v2/myapp/manifests/latest
To see rate limiting in action, enable debug logging:
[logging]
level = "debug"
Security Considerations
IP Spoofing Prevention
If trusted_proxies is misconfigured, attackers can:
- Send requests with fake
X-Forwarded-For: 1.2.3.4 - Each request appears to come from a different IP
- Per-IP rate limiting becomes ineffective
- Attackers create unbounded limiter entries (memory exhaustion)
Always verify your proxy configuration by checking logs to ensure client IPs are detected correctly.
Endpoint Coverage
All authenticated endpoints are protected:
| Endpoint | Rate Limiter | Limits Applied |
|---|---|---|
/v2/* (registry) |
Registry limiter | Global RPS + Per-IP RPS |
/auth/* (authentication) |
Registry limiter | Global RPS + Per-IP RPS |
/admin/* (management) |
Admin limiter | Global RPS + Per-IP RPS |
The auth endpoints share the registry limiter, preventing brute force attacks on credentials. The admin API uses separate limiter instances to avoid CI/CD traffic affecting admin access.
Recommended Production Settings
[api.rate_limit]
enabled = true
global_rps = 1000 # Adjust based on your traffic
per_ip_rps = 30 # Stricter per-IP limit
burst = 50 # Smaller burst for tighter control
trusted_proxies = [] # Configure based on your proxy setup
Troubleshooting
"I'm getting rate limited but traffic is low"
- Check if
trusted_proxiesis configured correctly - All traffic may appear to come from your proxy's IP
- Add your proxy to
trusted_proxiesto see real client IPs
"Rate limiting doesn't seem to work"
- Verify
enabled = true - Check that requests go through Gordon (not cached by CDN)
- Ensure
global_rpsandper_ip_rpsare set appropriately
"I need to whitelist certain IPs"
Rate limiting doesn't support IP whitelisting. Consider:
- Increasing
per_ip_rpsfor legitimate high-traffic clients - Using a reverse proxy with its own rate limiting rules
- Implementing IP-based access control at the network level