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

  1. Add client-side retry with exponential backoff and jitter, honoring any Retry-After signal
  2. Tune the limiter: raise rate.Limit and/or Burst to match real traffic (measure p99 QPS first)
  3. If rejecting is wrong for your UX, switch to ratelimit.NewDelayingLimiter so excess requests queue instead of failing
  4. 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

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


AI-assisted analysis of go-kit/kit@78fbbceece (2026-08-15). Data as JSON: /api/errors/9acaa290ab19a0ea. Report an issue: GitHub.