A library that rate limits Callers using the Rate Capabilities in their Tokens.
A rate limiter whose subject and limits come from an authenticated Token.
It limits how fast each Caller (the Token's principal) may make requests,
using the Rate Capabilities the Token carries, e.g. prefix:rate:50/1m.
A Token may carry several, such as 1000/1m and 100000/24h, and every
one applies. A deployment's own policy, such as a ceiling for everyone or
an override for one Caller, is a function it supplies with WithResolver.
tokenrate depends on nothing beyond the standard library: no bascule, no JWT library, no HTTP framework. A service passes in the principal and capability strings after authenticating a request, and decides how to apply the decision it gets back.
go get github.com/xmidt-org/tokenratePlace the middleware after the one that validates the request's credentials. It must never run before: the limiter keeps state per principal, so an unauthenticated caller who could choose the principal could fill memory with one request per name. It checks the Caller, writes one warning header per Capability Warning, and answers 429 Too Many Requests (with Retry-After) or 403 Forbidden itself. The only service-specific part is finding the principal and capabilities in the request. A Token with no rate is unlimited unless a Resolver says otherwise:
limiter, err := tokenrate.New(
tokenrate.WithPrefixes("prefix:rate:"),
)
if err != nil {
return err
}
handler := tokenrate.Middleware{
Limiter: limiter,
Extract: func(r *http.Request) (string, []string) {
token, ok := bascule.Get(r.Context())
if !ok {
return "", nil // unauthenticated: Unrestricted unless the Resolver says otherwise
}
caps, _ := bascule.GetCapabilities(token)
return token.Principal(), caps
},
}.Wrap(next)
// With bascule's own middleware, which stores the Token in the context:
protected := basculeMiddleware.Then(handler)Outside HTTP, call limiter.Check(ctx, principal, capabilities) and apply
the Decision yourself.
A Resolver has the final say over the rates a Token counts as carrying,
so policy lives in your code, not in options. The library always enforces
what it returns: no rates means no limit, the zero rate refuses, and an
error denies the request. DefaultResolver handles malformed rates and
the Window Bounds; call it from yours to keep that.
tokenrate.WithResolver(func(ctx context.Context, principal string, provided []tokenrate.Rate) ([]tokenrate.Rate, error) {
if override, ok := overrides[principal]; ok {
return override, nil // replaces whatever the Token says
}
rates, _ := tokenrate.DefaultResolver(ctx, principal, provided)
if len(rates) == 0 {
return nil, errors.New("no rate capability") // every Token must carry a rate
}
return append(rates, ceiling), nil // nobody exceeds the ceiling
})See the examples for all three.
examples/server is a small service limited by tokenrate, with a table of
API keys standing in for credential validation, and examples/client makes
a burst of requests and prints each answer. Both are standard library only.
go run ./examples/server$ go run ./examples/client -key key-abc -n 7
1 200 OK
2 200 OK
3 200 OK
4 200 OK
5 200 OK
6 429 Too Many Requests Retry-After: 12s
7 429 Too Many Requests Retry-After: 12skey-ghi carries a malformed rate alongside a valid one, so every answer
carries a warning; key-def carries no rate and is Unrestricted.
- CONTEXT.md defines the terms.
- docs/design.md describes the behavior.
- ADR 0002 explains why a Caller is held to every rate that applies.
- ADR 0003 explains why the library always enforces and a Resolver decides policy.
This project and everyone participating in it are governed by the XMiDT Code Of Conduct. By participating, you agree to this Code.
Refer to CONTRIBUTING.md.