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
- Back off and retry the same engine after a delay (the error is marked retryable by design).
- Reduce request rate or add jitter/caching between searches in the flow.
- Use a higher-tier plan or rotate to a different API key for the engine.
- 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
- Respect per-engine rate limits; add client-side throttling/jitter.
- Cache repeated queries to reduce engine calls.
- Rotate or upgrade API keys before quotas are hit.
- Configure multiple fallback engines in web_search fallbackStrategy.
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
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
- %s (HTTP %d)
- there are requesting too many results
- unexpected status code: %d
- failed to do request: %w
- Sploitus API rate limit exceeded (HTTP %d), please try again
AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01).
Data as JSON: /api/errors/e588a2a5e57cee70.
Report an issue: GitHub.