vitessio/vitess · error

BUG: throttle() must not be called with a time of less than

Error message

BUG: throttle() must not be called with a time of less than 1 second. now: %v

What it means

threadThrottler.throttle() requires 'now' to be at least oneSecond (epoch + 1s) because rate limiting math operates on per-second buckets relative to a real clock. A time before that is treated as an uninitialized or zero-valued time.Time, which is a programming bug, so it panics.

Source

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

	// throttle() call to be accepted after setMaxRate() has been called with a nonzero rate.
	// Unfortunately, if we initialize the limiter rate to 0, the internal token buffer will be
	// empty by the time the first throttle() call is executed and it will be denied.
	// Instead, we initialize the limiter rate to 1. This way the token buffer will be full (assuming
	// the 'now' parameter of the first throttle() call is at least 1 second) and the rate will
	// be reset to 0 if setMaxRate() has not been called with a nonzero rate.
	result := threadThrottler{
		threadID:          threadID,
		actualRateHistory: actualRateHistory,
		limiter:           rate.NewLimiter(1 /* limit */, 1 /* burst */),
	}
	return &result
}

var oneSecond = time.Time{}.Add(1 * time.Second)

func (t *threadThrottler) throttle(now time.Time) time.Duration {
	if now.Before(oneSecond) {
		panic(fmt.Sprintf(
			"BUG: throttle() must not be called with a time of less than 1 second. now: %v",
			now))
	}

	// Pass the limit set by the last call to setMaxRate. Limiter.SetLimitAt
	// is idempotent, so we can call it with the same value more than once without
	// issues.
	t.limiter.SetLimitAt(now, rate.Limit(t.maxRate.Load()))

	// Initialize or advance the current second interval when necessary.
	nowSecond := now.Truncate(time.Second)
	if t.currentSecond != nowSecond {
		// Report the number of successful (not-throttled) requests from the "last" second if this is
		// not the first time 'throttle' is called.
		if !t.currentSecond.IsZero() {
			t.actualRateHistory.addPerThread(t.threadID, record{t.currentSecond, t.currentRate})
		}
		t.currentRate = 0

View on GitHub (pinned to 01a25a7d17)

Solutions

  1. Initialize the injected nowFunc to return times at or after oneSecond (e.g. time.Unix(1,0) or real time.Now())
  2. If using a fake clock, base it on time.Time{}.Add(n) with n >= 1s
  3. Check that ThrottlerImpl construction actually wires a working nowFunc instead of leaving the default nil/zero path

Example fix

// before
nowFunc: func() time.Time { return time.Time{} }
// after
nowFunc: func() time.Time { return time.Time{}.Add(time.Second) }
Defensive patterns

Strategy: validation

Validate before calling

if now.Before(time.Time{}.Add(time.Second)) {
    return errors.New("throttler clock must return epoch+1s or later")
}

Type guard

func validClock(f func() time.Time) bool {
    return !f().Before(time.Time{}.Add(time.Second))
}

Try / catch

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

Prevention

When it happens

Trigger: Calling ThrottlerImpl.Throttle() when the throttler's nowFunc returns a zero or near-zero time.Time — e.g. a test constructed the Throttler without setting nowFunc, or injected a clock returning time.Time{}.

Common situations: Unit tests (like TestThrottle_NoBurst) or benchmark harnesses that build a ThrottlerImpl with a stub clock initialized to the zero value instead of a realistic epoch-based time.

Related errors


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