vitessio/vitess · error

BUG: limiter was unable to reserve an event…

Error message

BUG: limiter was unable to reserve an event. threadThrottler: %+v, reservation:%v

What it means

After reserving an event with the rate limiter (ReserveN), throttle() expects reservation.OK() to be true; a false OK means the limiter cannot ever grant the event (typically limiter burst/limit configured to zero or negative, or limiter in a failed state). The throttler treats this as an unrecoverable BUG and panics.

Solutions

  1. Ensure setMaxRate is only called with strictly positive values
  2. Validate rate configuration at startup (reject 0 or negative rates before creating the throttler)
  3. If a 0-rate 'pause' is intended, gate it before calling Throttle instead of feeding it to the limiter
  4. Inspect the panic dump's threadThrottler state to confirm the configured limit/burst

Example fix

// before
throttler.SetMaxRate(0)
// after
if maxRate <= 0 { maxRate = defaultMaxRate }
throttler.SetMaxRate(maxRate)
Defensive patterns

Strategy: validation

Validate before calling

if maxRate <= 0 {
    return fmt.Errorf("maxRate must be positive, got %d", maxRate)
}
throttler.SetMaxRate(maxRate)

Type guard

func validRate(r float64) bool { return r > 0 }

Try / catch

defer func() {
    if r := recover(); r != nil {
        log.Errorf("throttle reservation panic: %v", r)
    }
}()

Prevention

When it happens

Trigger: Calling ThrottlerImpl.Throttle() on a threadThrottler whose rate.Limiter rejects the reservation — commonly after setMaxRate(0) or a limiter constructed with limit <= 0 / burst < 1.

Common situations: Configuring max rate to 0 via the throttler's SetMaxRate in a test or via misconfigured flags; passing negative rate values from config parsing bugs.

Related errors


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/170888e493ad49b5. Report an issue: GitHub.

Appendix: source

Thrown at go/vt/throttler/thread_throttler.go:100

			t.actualRateHistory.addPerThread(t.threadID, record{t.currentSecond, t.currentRate})
		}
		t.currentRate = 0
		t.currentSecond = nowSecond
	}

	if t.limiter.Limit() == 0 {
		// If the limit is 0 this request won't be let through. However, the caller
		// should poll again in the future in case the limit has changed.
		return 1 * time.Second
	}

	// Figure out how long to backoff: We use the limiter.ReserveN() method to reserve an event.
	// The returned reservation contains the backoff delay. We cancel the reservation if the
	// delay is greater than 0, since the caller is expected to call throttle() again at that time
	// rather than proceed.
	reservation := t.limiter.ReserveN(now, 1)
	if !reservation.OK() {
		panic(fmt.Sprintf("BUG: limiter was unable to reserve an event. "+
			"threadThrottler: %+v, reservation:%v", t, *reservation))
	}
	waitDuration := reservation.DelayFrom(now)
	if waitDuration <= 0 {
		t.currentRate++
		return NotThrottled
	}
	reservation.CancelAt(now)
	return waitDuration
}

// setMaxRate sets the maximum rate for the next time throttle() is called.
// setMaxRate() can be called concurrently with other methods of this object.
func (t *threadThrottler) setMaxRate(newRate int64) {
	t.maxRate.Store(newRate)
}

// maxRate returns the rate set by the last call to setMaxRate().

View on GitHub (pinned to 01a25a7d17)