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 = 0View on GitHub (pinned to 01a25a7d17)
Solutions
- Initialize the injected nowFunc to return times at or after oneSecond (e.g. time.Unix(1,0) or real time.Now())
- If using a fake clock, base it on time.Time{}.Add(n) with n >= 1s
- 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
- Base fake clocks on time.Time{}.Add(n) with n >= 1s
- Verify nowFunc in ThrottlerImpl constructor tests
- Never pass a zero-value time.Time into Throttle
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
- BUG: cannot add record because it is already covered by a pr
- BUG: cannot add record because it does not start at the begi
- BUG: invalid TabletType forwarded: %v
- rate recalculation was triggered with a zero replication lag
- BUG: invalid state: %v
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/110a7548360526e6.
Report an issue: GitHub.