Skip to content
Rate limiting

Rate limiting

Limit how often clients can call your routes, and how often they can attempt actions such as logging in.

Before you start

Rate limits count hits in the app’s cache, so set it up with cache.ForApp (see Cache values). With the memory store each instance counts on its own; with several instances, use the database or Redis store so a limit holds across all of them.

Behind a load balancer or reverse proxy, set HTTP_TRUSTED_PROXIES to your proxies’ networks, so the server’s RealIP middleware finds the client’s address; otherwise every request seems to come from the proxy, and all clients share one limit.

Steps

1. Limit a group of routes

// 120 requests a minute per client IP, counted in the app's cache.
r = r.Group("", ratelimit.Middleware("api", ratelimit.PerMinute(120)))

(Copied from examples/database, region ratelimit.)

The name ("api") keeps these counts apart from other limits’: give each Middleware its own. A request over the limit gets 429 Too Many Requests through the app’s error handler, with a Retry-After header in seconds. Every limited response carries X-RateLimit-Limit and X-RateLimit-Remaining.

Pass several limits to enforce them all. They are counted from the shortest window to the longest, and a request over one isn’t counted against the longer ones, so a client retrying too fast doesn’t use up its daily quota:

// illustrative
api := r.Group("/api", ratelimit.Middleware("api", ratelimit.PerMinute(60), ratelimit.PerDay(5000)))

The limits are ratelimit.PerSecond, PerMinute, PerHour, PerDay, and ratelimit.Per(n, window) for any window.

2. Count per user or token

By default, hits are counted per client IP address (per /64 network for IPv6). Count per something else with By, such as the signed-in user:

// illustrative
perUser := ratelimit.PerMinute(600).By(func(r *http.Request) string {
	return userID(r) // the user an authentication middleware found; "" if none
})
perGuest := ratelimit.PerMinute(60).By(func(r *http.Request) string {
	if userID(r) != "" {
		return "" // signed in: perUser applies
	}
	return ratelimit.IP(r)
})
api := r.Group("/api", auth, ratelimit.Middleware("api", perUser, perGuest))

Returning "" leaves the request unlimited by that limit. Key only by something the server has verified: a key the client chooses (a header, a token that isn’t checked yet) can change with every request, and gets it a fresh count each time. Make sure every request has some limit, as the per-IP limit for guests does above. Keys are stored as hashes, so they may be secrets.

3. Limit an action in a handler

For attempts that aren’t whole requests, such as logins, count them with ratelimit.Allow and clear the count when the attempt succeeds:

// illustrative
key := "login:" + strings.ToLower(in.Email) + "|" + ratelimit.IP(c.Request())
limit := ratelimit.PerMinute(5)
res, err := ratelimit.Allow(c, key, limit)
if err != nil {
	return nil, err
}
if !res.Allowed {
	return nil, web.Errorf(http.StatusTooManyRequests, "Too many attempts. Try again in %d seconds.", int(res.RetryAfter().Seconds())+1)
}
// … check the password; on success:
_ = ratelimit.Clear(c, key, limit)

To count only some attempts (failures), check first with ratelimit.Check, which doesn’t count, and count with ratelimit.Hit when the attempt fails.

To limit an amount rather than a number of events (bytes uploaded, a model’s tokens), count it with ratelimit.AllowN(ctx, key, n, limit): ratelimit.PerDay(100_000) then allows 100,000 a day. AI budgets (ai.Budget) work this way.

4. Test

Each anetostest app has its own cache, so tests don’t share counts. To see a limit trip, send more requests than it allows:

// illustrative
for range 120 {
	app.GetJSON("/stats").AssertOK()
}
app.GetJSON("/stats").AssertStatus(http.StatusTooManyRequests)

A test can start a new window partway through; the example’s TestRateLimit sends up to two windows’ worth of requests to allow for that.

How it works

Each limit counts hits in fixed windows aligned to the clock: a per-minute limit starts a new count at every minute. A client can therefore make up to twice the limit around the end of a window, which is the usual trade-off for one cache operation per request. A hit is one atomic cache.Increment of a key made of a hash of the middleware’s name, the kind of key and the client’s key (or of your Allow key), the limit and the window; it expires after its window, plus up to a minute for clocks that differ between instances. Hits over a limit count against it too, and the window’s end is the same for every client.

If the cache fails, the request fails with its error (a 500) rather than go through unlimited.

Common problems

SymptomCauseFix
Every request fails with ratelimit: cache: no cache in contextcache.ForApp wasn’t calledSet up the cache in setup
All clients share one limit in productionThe app sees the proxy’s addressSet HTTP_TRUSTED_PROXIES to your proxies’ networks
A limit allows more than expected with several instancesThe memory store counts per instanceCACHE_STORE=database or redis
Two routes’ limits share a countTheir Middleware calls have the same nameGive each its own name

Next steps