Billionmail/BillionMail · warning

rate limiter: %w

Error message

rate limiter: %w

What it means

RateLimitedClient.Do waits on its internal rate limiter before executing an HTTP request. If limiter.Wait fails — almost always because the request context was cancelled or its deadline expired while blocked waiting for a rate-limit slot — the error is wrapped as 'rate limiter: %w'. This separates rate-wait failures from HTTP/circuit-breaker failures.

Source

Thrown at core/internal/service/video_gen/apiclient.go:42

		limiter: rate.NewLimiter(rate.Limit(rps), burst),
		cb: gobreaker.NewCircuitBreaker[*http.Response](gobreaker.Settings{
			Name:        name,
			MaxRequests: 2,                // half-open: allow 2 probe requests
			Interval:    60 * time.Second, // rolling window for failure counting
			Timeout:     30 * time.Second, // time in open state before half-open
			ReadyToTrip: func(counts gobreaker.Counts) bool {
				return counts.ConsecutiveFailures >= 5
			},
		}),
	}
}

// Do executes an HTTP request with rate limiting and circuit breaking.
// Blocks until rate limiter allows, then executes through circuit breaker.
func (c *RateLimitedClient) Do(req *http.Request) (*http.Response, error) {
	ctx := req.Context()
	if err := c.limiter.Wait(ctx); err != nil {
		return nil, fmt.Errorf("rate limiter: %w", err)
	}

	resp, err := c.cb.Execute(func() (*http.Response, error) {
		return c.client.Do(req)
	})
	if err != nil {
		return nil, fmt.Errorf("circuit breaker [%s]: %w", c.cb.Name(), err)
	}
	return resp, nil
}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Increase the request/context timeout so it comfortably exceeds the worst-case rate-limit wait
  2. Lower the request rate or increase the limiter's burst so requests don't queue long
  3. If the caller is cancelling intentionally, treat this as expected and skip retrying
  4. Check that the context isn't already expired when Do is called
  5. Log limiter config vs actual traffic to right-size the rate

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
// after
ctx, cancel := context.WithTimeout(ctx, 10*time.Second) // budget for rate-limit wait + request
Defensive patterns

Strategy: retry

Validate before calling

// ensure the deadline is larger than worst-case rate-limit wait
wait := time.Duration(1.0/float64(rate) * float64(len(pending)+1)) * time.Second
if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) < wait {
    return errors.New("context deadline too short for rate-limited call")
}

Try / catch

resp, err := client.Do(req)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
        // rate-limit wait exceeded budget — retry with a longer deadline or back off
        return retryWithLongerTimeout(req)
    }
    return err
}

Prevention

When it happens

Trigger: req.Context() is cancelled or times out while Do is blocked in c.limiter.Wait(ctx); the limiter has no tokens available within the context's remaining deadline.

Common situations: Client timeout shorter than the rate-limited wait; caller cancels the request mid-flight; rate limit configured too aggressively (e.g. 1 req/sec with a 500ms HTTP timeout) under burst load.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/3c85e42c152a81ea. Report an issue: GitHub.