Skip to content

Configuration

wiki edited this page Sep 4, 2026 · 1 revision

Configuration

Unlike the other extensions, this one takes functional options directly rather than a *Config:

app := rex.New(ratelimit.WithRateLimit(
	ratelimit.WithGlobalLimit(600, time.Minute),
	ratelimit.WithRouterLimit("partner", 100, time.Minute),
	ratelimit.WithTrustedProxies(ratelimit.PrivateNetworks()...),
	ratelimit.WithMaxBuckets(100_000),
))

ratelimit.WithRateLimit() with no options attaches the extension with no limits configured — every route is unlimited unless it declares its own.

Options

WithGlobalLimit(rate, window) a fixed limit across every router
WithGlobalLimitFunc(fn) a dynamic global limit
WithRouterLimit(name, rate, window) a fixed limit for one router
WithRouterLimitFunc(name, fn) a dynamic limit for one router
WithKeyFunc(kf) override client identification
WithTrustedProxies(prefixes…) networks whose X-Forwarded-For is believed
WithMaxBuckets(n) cap distinct buckets; 0 → 100,000; negative → no cap

Config

type Config struct {
	Global         LimitProvider
	Routers        map[string]LimitProvider
	KeyFunc        KeyFunc          // default RemoteIPKeyFunc()
	TrustedProxies []netip.Prefix   // empty trusts nothing
	MaxBuckets     int              // 0 → DefaultMaxBuckets
}

WithTrustedProxies also switches the key function to the forwarding-aware one, so you do not set both.

Prefix helpers

ratelimit.PrivateNetworks()                            // RFC 1918, RFC 4193, loopback
ratelimit.MustParsePrefixes("10.0.0.0/8", "10.1.0.0/16")

MustParsePrefixes panics on a malformed CIDR — for configuration written as a literal, where a bad prefix is a programming error. The alternative, silently producing an empty trust list, would look like working code behind a proxy while actually rate limiting every client as one.

For prefixes read from configuration, parse with netip.ParsePrefix yourself and fail startup on the error.

A production shape

app := rex.New(ratelimit.WithRateLimit(
	// A backstop well above normal aggregate traffic.
	ratelimit.WithGlobalLimit(6_000, time.Minute),

	// Audience-shaped budgets.
	ratelimit.WithRouterLimit("partner", 600, time.Minute),

	// The ingress subnet, not the whole private range.
	ratelimit.WithTrustedProxies(ratelimit.MustParsePrefixes("10.42.0.0/16")...),

	ratelimit.WithMaxBuckets(100_000),
))

with the endpoints that matter declaring their own:

func (r *LoginRoute) RateLimit() ratelimit.LimitConfig {
	return ratelimit.LimitConfig{Rate: 5, Window: time.Minute}
}

Interaction with CORS

The X-RateLimit-* and Retry-After headers are useless to a browser client unless exposed. rextension-cors exposes them by default — but WithExposedHeaders replaces the defaults, so if you set it, include them:

cors.WithExposedHeaders(
	"X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After",
	"X-Request-Id",
)

Composing it yourself

app.UsePerRoute(ratelimit.RateLimitFactory(cfg, store), rextension.PriorityRateLimit)

ratelimit.RateLimitMiddleware(routerName, cfg, index, store) is the deprecated plain-http form — it resolves the applicable limit per request, which is what made per-endpoint limits silently inoperative for parameterized routes.

Clone this wiki locally