vxcontrol/pentagi · warning · RetryableError

%s (HTTP 429)

Error message

%s (HTTP 429)

What it means

ClassifyHTTPStatus maps an HTTP status code from a search backend into a typed searcher error. HTTP 429 means the engine rate-limited the request, so the function returns a RetryableError, telling the orchestrator the same engine may succeed after backing off (retryAfter=0 means immediate/secondary retry).

Source

Thrown at backend/pkg/tools/searchers/errors.go:57

	return &RetryableError{Err: err, RetryAfter: retryAfter}
}

// Fatal wraps err as a FatalError.
func Fatal(err error) error {
	return &FatalError{Err: err}
}

// ClassifyHTTPStatus maps a non-2xx HTTP status to the right typed error so every
// engine classifies identically. The engine supplies a human-readable message; the
// retry/fatal decision is centralized here.
//
//	429       -> RetryableError (rate limited; back off and retry the same engine)
//	5xx       -> RetryableError (upstream problem; may clear on retry)
//	4xx/other -> FatalError     (auth/bad-request/etc.; retrying the same engine is pointless)
func ClassifyHTTPStatus(status int, msg string) error {
	switch {
	case status == http.StatusTooManyRequests:
		return Retryable(fmt.Errorf("%s (HTTP 429)", msg), 0)
	case status >= 500:
		return Retryable(fmt.Errorf("%s (HTTP %d)", msg, status), 0)
	default:
		return Fatal(fmt.Errorf("%s (HTTP %d)", msg, status))
	}
}

// IsRetryable reports whether err (or anything it wraps) is a RetryableError.
func IsRetryable(err error) bool {
	var t *RetryableError
	return errors.As(err, &t)
}

// IsFatal reports whether err (or anything it wraps) is a FatalError.
func IsFatal(err error) bool {
	var t *FatalError
	return errors.As(err, &t)
}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Back off and retry the same engine after a delay (the error is marked retryable by design).
  2. Reduce request rate or add jitter/caching between searches in the flow.
  3. Use a higher-tier plan or rotate to a different API key for the engine.
  4. Configure fallback engines so the orchestrator can switch when one is rate-limited.

Example fix

// before: hammering the engine in a loop
for _, q := range queries { s.Handle(ctx, q) }
// after: respect retryability
err := s.Handle(ctx, q)
if searchers.IsRetryable(err) { time.Sleep(backoff); retry() } else if err != nil { fallbackEngine() }
Defensive patterns

Strategy: retry

Validate before calling

// preflight quota probe
resp, _ := http.Head("https://api.firecrawl.dev")
if resp != nil && resp.StatusCode == 429 { time.Sleep(backoff) }

Type guard

func isRateLimited(err error) bool {
  var r *searchers.RetryableError
  return errors.As(err, &r) && strings.Contains(err.Error(), "HTTP 429")
}

Try / catch

if err := searcher.Handle(ctx, q); err != nil {
  if searchers.IsRetryable(err) { time.Sleep(backoff); go retry(q) } else { log.Error(err) }
}

Prevention

When it happens

Trigger: Any searcher (e.g. Google via classifyGoogleError) receives HTTP 429 from the upstream engine API during a web_search call.

Common situations: Exhausted per-key query quotas on Google/Firecrawl-style APIs, many flows sharing one API key, bursty agent traffic hitting free-tier limits.

Understand the failure class

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/e588a2a5e57cee70. Report an issue: GitHub.