temporalio/temporal · error

expect at least one rate limiter

Error message

expect at least one rate limiter

What it means

NewMultiRateLimiter composes several staged RateLimiters into a MultiRateLimiterImpl. It panics when the input slice is empty because a multi-stage limiter with no stages would silently allow or reject all traffic — an invalid configuration the library refuses to construct.

Source

Thrown at common/quotas/multi_rate_limiter_impl.go:31

	InfDuration = rate.InfDuration
)

type (
	// MultiRateLimiterImpl is a wrapper around the limiter interface
	MultiRateLimiterImpl struct {
		rateLimiters []RateLimiter
		recycleCh    chan struct{}
	}
)

var _ RateLimiter = (*MultiRateLimiterImpl)(nil)

// NewMultiRateLimiter returns a new rate limiter that have multiple stage
func NewMultiRateLimiter(
	rateLimiters []RateLimiter,
) *MultiRateLimiterImpl {
	if len(rateLimiters) == 0 {
		panic("expect at least one rate limiter")
	}
	return &MultiRateLimiterImpl{
		rateLimiters: rateLimiters,
		recycleCh:    make(chan struct{}),
	}
}

// Allow immediately returns with true or false indicating if a rate limit
// token is available or not
func (rl *MultiRateLimiterImpl) Allow() bool {
	return rl.AllowN(time.Now(), 1)
}

// AllowN immediately returns with true or false indicating if n rate limit
// token is available or not
func (rl *MultiRateLimiterImpl) AllowN(now time.Time, numToken int) bool {
	length := len(rl.rateLimiters)
	reservations := make([]Reservation, 0, length)

View on GitHub (pinned to bde624efd1)

Solutions

  1. Ensure the rateLimiters slice has at least one limiter before calling NewMultiRateLimiter
  2. Fall back to a default NoopRateLimiter or NewLocalRateLimiter when the configured list is empty
  3. Fix the config source that yields an empty limiter list

Example fix

// before
rl := quotas.NewMultiRateLimiter(cfg.RateLimiters)
// after
if len(cfg.RateLimiters) == 0 {
    cfg.RateLimiters = []quotas.RateLimiter{quotas.NewNoopRateLimiter()}
}
rl := quotas.NewMultiRateLimiter(cfg.RateLimiters)
Defensive patterns

Strategy: validation

Validate before calling

if len(rateLimiters) == 0 {
    rateLimiters = []quotas.RateLimiter{quotas.NewNoopRateLimiter()}
}
rl := quotas.NewMultiRateLimiter(rateLimiters)

Try / catch

func() (rl *quotas.MultiRateLimiterImpl) {
    defer func() {
        if r := recover(); r != nil { rl = quotas.NewMultiRateLimiter(defaultLimiter) }
    }()
    return quotas.NewMultiRateLimiter(limiters)
}()

Prevention

When it happens

Trigger: Calling NewMultiRateLimiter(nil) or NewMultiRateLimiter([]quotas.RateLimiter{}) — e.g. when a config-driven list of rate limiters resolves to nothing.

Common situations: Empty or misparsed dynamic config for rate limiter stages; building limiters conditionally so all are skipped; feature flags disabling every limiter.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/ce8ffe26aae77748. Report an issue: GitHub.