go-kit/kit · warning · ErrLimited
rate limit exceeded
Error message
rate limit exceeded
What it means
Returned by ratelimit.NewErroringLimiter when the configured Allower (typically rate.Limiter from golang.org/x/time/rate) reports Allow() == false for the current request (token_bucket.go:27-29). It is the deliberate rejection path of the erroring rate limiter: the request would exceed the configured rate and burst, so the endpoint is never invoked.
Source
Thrown at ratelimit/token_bucket.go:12
package ratelimit
import (
"context"
"errors"
"github.com/go-kit/kit/endpoint"
)
// ErrLimited is returned in the request path when the rate limiter is
// triggered and the request is rejected.
var ErrLimited = errors.New("rate limit exceeded")
// Allower dictates whether or not a request is acceptable to run.
// The Limiter from "golang.org/x/time/rate" already implements this interface,
// one is able to use that in NewErroringLimiter without any modifications.
type Allower interface {
Allow() bool
}
// NewErroringLimiter returns an endpoint.Middleware that acts as a rate
// limiter. Requests that would exceed the
// maximum request rate are simply rejected with an error.
func NewErroringLimiter(limit Allower) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
if !limit.Allow() {
return nil, ErrLimited
}
return next(ctx, request)View on GitHub (pinned to 78fbbceece)
Solutions
- Add client-side retry with exponential backoff and jitter, honoring any Retry-After signal
- Tune the limiter: raise rate.Limit and/or Burst to match real traffic (measure p99 QPS first)
- If rejecting is wrong for your UX, switch to ratelimit.NewDelayingLimiter so excess requests queue instead of failing
- For a global limit across instances, use a shared store implementation (e.g. redis-based Allower) instead of an in-process limiter
Example fix
// before: hard rejection at ~1 QPS e := ratelimit.NewErroringLimiter(rate.NewLimiter(rate.Every(time.Second), 1))(myEndpoint) // after: limits sized for real traffic, excess requests throttled not failed e := ratelimit.NewDelayingLimiter(rate.NewLimiter(rate.Every(10*time.Millisecond), 100))(myEndpoint)
Defensive patterns
Strategy: retry
Validate before calling
null
Type guard
null
Try / catch
var backoff time.Duration
for attempt := 0; attempt < 4; attempt++ {
resp, err = ep(ctx, req)
if err == nil || !errors.Is(err, ratelimit.ErrLimited) {
break
}
backoff = nextExponentialJitter(attempt) // e.g. 50ms,100ms,200ms +/- jitter
time.Sleep(backoff)
} Prevention
- Size limits from measured traffic (p99 QPS) and set Burst to absorb normal spikes
- Always pair NewErroringLimiter with client-side exponential backoff + jitter
- Consider NewDelayingLimiter when queueing is preferable to rejection
- For multi-instance deployments use a shared (e.g. Redis-backed) Allower so the effective limit matches intent
When it happens
Trigger: Sustained QPS above rate.Limit(n) with the burst bucket already drained; a traffic spike consuming the whole burst at once; limits sized per-instance while a load balancer fans traffic across many instances; a shared limiter set extremely low (e.g. rate.Every(time.Second) = 1 QPS) behind a chatty client.
Common situations: No client-side backoff, so rejected calls are retried immediately and amplify the load; per-process limiters in horizontally scaled deployments disagreeing with the intended global limit; healthy scraping/monitoring traffic eating the burst; choosing NewErroringLimiter where NewDelayingLimiter (throttle) was the intended behavior.
Related errors
- CasbinModel is required in context
- CasbinPolicy is required in context
- Unauthorized Access
- token up for parsing was not passed through the context
- JWT was invalid
AI-assisted analysis of go-kit/kit@78fbbceece (2026-08-15).
Data as JSON: /api/errors/9acaa290ab19a0ea.
Report an issue: GitHub.