ratelimit

package
v1.101.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package ratelimit provides a per-key token-bucket rate limiter shared across the platform's HTTP surfaces (the public portal viewer and the OAuth authorization server). Keeping one implementation avoids per-caller forks of the bucket math, cleanup goroutine, and eviction policy.

The limiter is keyed on an arbitrary string. Callers choose the key: a client IP for per-client fairness, or a fixed sentinel for a global backstop that bounds total throughput regardless of client attribution. Client-IP extraction (including trusted-proxy handling) lives in Resolver (clientip.go); this file is only the bucket accounting.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	RequestsPerMinute int `yaml:"requests_per_minute"`
	BurstSize         int `yaml:"burst_size"`
}

Config configures a token-bucket limiter.

type HTTPLimiter

type HTTPLimiter struct {
	// contains filtered or unexported fields
}

HTTPLimiter rate-limits requests to a single HTTP endpoint with two layers: a per-client-IP bucket for fairness and a global bucket that bounds total throughput regardless of how requests attribute to IPs. Client-IP attribution (including trusted-proxy handling) is delegated to a Resolver, so the per-IP layer is not trivially defeated by a spoofed X-Forwarded-For.

func NewHTTPLimiter

func NewHTTPLimiter(rpm, burst int, resolver *Resolver) *HTTPLimiter

NewHTTPLimiter builds an HTTPLimiter for one endpoint from a per-IP rate/burst and a client-IP resolver. The global backstop bucket is sized at globalBackstopFactor times the per-IP rate and burst. Call Close to stop the background cleanup goroutines.

func (*HTTPLimiter) Allow

func (h *HTTPLimiter) Allow(r *http.Request) bool

Allow reports whether the request may proceed. A request is admitted only when it is within BOTH the client's own per-IP budget and the global budget, and a token is consumed from each bucket only on admission:

  1. Peek per-IP (no consume). A client over its own limit is rejected here without ever touching the global bucket, so an over-limit client cannot drain the shared backstop against everyone else.
  2. Consume global. If the system is saturated the request is rejected here with the per-IP bucket left untouched, so a legitimate low-volume client is not additionally penalized by a global-saturation event.
  3. Consume per-IP to record the admitted request.

func (*HTTPLimiter) Close

func (h *HTTPLimiter) Close()

Close stops both buckets' background cleanup goroutines.

func (*HTTPLimiter) RetryAfter

func (h *HTTPLimiter) RetryAfter() int

RetryAfter returns the advisory Retry-After value in seconds: the time for one per-IP token to refill, at least 1.

type Limiter

type Limiter struct {
	// contains filtered or unexported fields
}

Limiter provides per-key token-bucket rate limiting.

func New

func New(cfg Config) *Limiter

New creates a rate limiter from config, applying defaults for non-positive values, and starts the background eviction goroutine. Call Close to stop it.

func (*Limiter) Allow

func (l *Limiter) Allow(key string) bool

Allow checks whether a request under the given key should be allowed, consuming one token when it is.

func (*Limiter) Cleanup

func (l *Limiter) Cleanup(maxAge time.Duration)

Cleanup removes stale entries older than the given duration.

func (*Limiter) Close

func (l *Limiter) Close()

Close stops the background cleanup goroutine. It is idempotent.

func (*Limiter) Rate

func (l *Limiter) Rate() float64

Rate returns the refill rate in tokens per second. It backs Retry-After computation: the time for a single token to refill is 1/Rate seconds.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver extracts the originating client IP from an HTTP request with trusted-proxy awareness. It exists because naively trusting the leftmost X-Forwarded-For entry is both spoofable (a caller sets any leftmost value to mint unlimited rate-limit buckets) and fragile behind a load balancer that does not forward the header (every client collapses onto the proxy IP). Neither failure mode is acceptable for a rate limiter guarding an unauthenticated, CPU-amplifying endpoint.

Trust model: X-Forwarded-For is consulted ONLY when the direct peer (RemoteAddr) is itself a configured trusted proxy. In that case the client IP is the rightmost XFF entry that is not itself a trusted proxy — the last hop a trusted proxy actually observed, which a spoofing client cannot forge past the trust boundary. When no trusted proxies are configured, or the peer is not trusted, the header is ignored entirely and RemoteAddr is used. This makes the safe default (trust nothing) the zero value.

func NewResolver

func NewResolver(cidrs []string) (*Resolver, error)

NewResolver builds a Resolver from a list of trusted proxy CIDRs (e.g. "10.0.0.0/8", "127.0.0.1/32"). An empty list yields a resolver that always returns the direct peer address and never consults X-Forwarded-For. A malformed CIDR is a configuration error and is returned.

func (*Resolver) ClientIP

func (r *Resolver) ClientIP(req *http.Request) string

ClientIP returns the rate-limit key for the request: the originating client IP per the trust model documented on Resolver. The returned value is a bare IP string when parseable, otherwise the raw RemoteAddr as a last resort so the caller always gets a non-empty key.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL