ratelimit

package
v0.63.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package ratelimit is the framework's general-purpose HTTP rate limiter.

It enforces a per-key sliding-window policy: up to Config.MaxAttempts requests are admitted within Config.Window; the next request triggers a hard block lasting Config.BlockDuration during which every request for that key gets 429. This is the "N actions per period, then lockout" shape — the right tool for brute-force surfaces, write-heavy endpoints, and any route where a steady refill rate (the token-bucket model) is the wrong abstraction.

Sliding window vs. token bucket

GoFastr ships two limiters with deliberately different semantics:

  • framework/ratelimit (this package): sliding window + lockout. Use it when you want "at most N per period, then block". The auth battery builds its login / register / password-reset limiters on top of it.
  • core/middleware.RateLimit: token bucket. Use it for steady API throughput ("1 req/s sustained, burst of 60") and when you want the RateLimit-* budget headers so well-behaved clients can self-pace.

Per-replica, not distributed

With a nil Config.Store the window is held in process memory, so the budget is per-replica: N replicas each allow MaxAttempts. For a replica-wide (or fleet-wide) budget, supply a Store such as battery/auth.SQLRateLimitStore — see "Shared store" below.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClientIP

func ClientIP(r *http.Request, trustXFF bool) string

ClientIP extracts the request IP. It honours X-Forwarded-For only when trustXFF is true (typically behind a trusted reverse proxy that strips client-supplied XFF). The default (trustXFF=false) ignores XFF — otherwise a single curl with a rotating X-Forwarded-For header bypasses every per-IP limit.

Types

type Config

type Config struct {
	MaxAttempts       int
	Window            time.Duration
	BlockDuration     time.Duration
	TrustForwardedFor bool
	Store             Store
	Scope             string

	// DevMode relaxes the limiter: when true, AllowContext short-circuits and
	// admits every attempt without touching either backend. Intended ONLY for
	// non-production deploys so local tooling that hammers an endpoint from one
	// IP (localhost) is never locked out. The default (false) keeps the limiter
	// fail-closed — production must NEVER set this.
	DevMode bool
}

Config controls a per-key sliding-window limiter.

MaxAttempts requests are permitted within Window. The MaxAttempts+1th request triggers a block of BlockDuration during which every request for that key gets 429.

Defaults (filled in by NewLimiter when zero): MaxAttempts=10, Window=15m, BlockDuration=30m.

TrustForwardedFor: when true, the leftmost X-Forwarded-For entry is used as the client IP by the default (IP-keyed) middleware. ONLY enable this if the server sits behind a trusted reverse proxy that strips client-supplied XFF headers — otherwise an attacker rotates the header per request and bypasses every per-IP limit. Default is false (use the connection's RemoteAddr).

Store: when non-nil, attempts are recorded in the shared backend instead of process memory, so the budget holds across replicas: MaxAttempts total, not MaxAttempts × N, and a block on one replica blocks on all. On a store error the limiter fails CLOSED (denies) — an attacker must never be able to lift the limit by degrading its backend. One store instance can back several limiters: keys are namespaced by Scope.

Scope namespaces this limiter's keys inside a shared Store. Set it explicitly when several limiters share one Store so their keys never collide. Ignored when Store is nil.

type Limiter

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

Limiter is a sliding-window rate limiter keyed by an arbitrary string (typically the client IP). The zero value is not usable — construct one with NewLimiter.

func NewLimiter

func NewLimiter(cfg Config) *Limiter

NewLimiter constructs a Limiter with the given config. Zero fields fall back to the documented defaults.

func (*Limiter) Allow

func (rl *Limiter) Allow(key string) (allowed bool, retryAfter time.Duration)

Allow records an attempt for key and returns whether it is allowed. If not allowed, retryAfter is the duration the caller should communicate in a Retry-After header. Equivalent to AllowContext with a background context — HTTP paths should prefer AllowContext(r.Context(), key) so a shared store can observe request cancellation.

func (*Limiter) AllowContext

func (rl *Limiter) AllowContext(ctx context.Context, key string) (allowed bool, retryAfter time.Duration)

AllowContext records an attempt for key against the configured backend: the shared Store when one is set (replica-wide budget), the in-process sliding window otherwise. A store failure DENIES the attempt — the limiter guards brute-force surfaces, so it must fail closed: degrading its backend must never lift the limit.

DevMode (see Config) is an explicit, tested short-circuit: when set, every attempt is admitted without touching either backend. This is the dev-only relaxation that stops local tooling being locked out; production never sets it, so the fail-closed guarantee holds.

func (*Limiter) Middleware

func (rl *Limiter) Middleware() func(http.Handler) http.Handler

Middleware returns an HTTP middleware that rate-limits by client IP (the default key). Blocked requests get 429 with a Retry-After header.

It emits ONLY Retry-After and never the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset budget headers that the token-bucket middleware (core/middleware.RateLimit) exposes: a live remaining-attempt count on a lockout-style limiter would hand an attacker exact brute-force pacing information on security-sensitive routes, and adds nothing useful on non-security routes where Retry-After already lets a client back off. For budget headers and a refill-rate model, use core/middleware.RateLimit.

To group by something other than IP (API key, user id, route param), use MiddlewareByKey.

func (*Limiter) MiddlewareByKey

func (rl *Limiter) MiddlewareByKey(keyFunc func(*http.Request) string) func(http.Handler) http.Handler

MiddlewareByKey returns an HTTP middleware that rate-limits using keyFunc to derive the per-request identity. A nil keyFunc falls back to the client IP. Blocked requests get 429 with a Retry-After header; see Middleware for why the budget headers are intentionally omitted.

type Store

type Store interface {
	Allow(ctx context.Context, key string, cfg Config) (allowed bool, retryAfter time.Duration, err error)
}

Store is the optional shared backend for a Limiter. When Config.Store is set, every replica consults the same attempt ledger, so the budget stays MaxAttempts total instead of MaxAttempts × replicas and a block on one replica holds on all of them.

battery/auth.SQLRateLimitStore implements this over SQLite or PostgreSQL and is the reference implementation; a custom Redis/etcd backend only needs to satisfy this interface. The implementation must derive per-limiter state from the namespaced key alone — it receives the full Config but should treat Scope + key as the identity.

Jump to

Keyboard shortcuts

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