Finding
calculateRetryDelay in packages/utils/src/translate/index.ts
uses pure exponential backoff on 429 responses.
Retry-After is never consulted.
RETRY_CONFIG = {
maxRetries: 3,
baseDelay: 1000ms,
backoffMultiplier: 2,
maxDelay: 10000ms
}
429 and 5xx are handled identically.
What happens under rate limiting
When a translation provider returns 429 with
Retry-After: 60:
- Retry 1 fires at 1s — inside cooldown
- Retry 2 fires at 2s — inside cooldown
- Retry 3 fires at 4s — inside cooldown
- All three retries fail
- Retry budget exhausted in 7 seconds
- Provider said wait 60 seconds. Client didn't listen.
The consequence
On a 60-second provider cooldown, the entire
retry budget is consumed in 7 seconds against
requests that cannot succeed.
The correct behavior
On 429 with Retry-After:
- Read the header value
- Wait at least that duration before retrying
- Fall back to exponential backoff only when
Retry-After is absent
Related pattern
retry-after-ignored-under-concurrency
Corpus reference: https://github.com/SirBrenton/pitstop-truth
Finding
calculateRetryDelayinpackages/utils/src/translate/index.tsuses pure exponential backoff on 429 responses.
Retry-Afteris never consulted.429 and 5xx are handled identically.
What happens under rate limiting
When a translation provider returns 429 with
Retry-After: 60:The consequence
On a 60-second provider cooldown, the entire
retry budget is consumed in 7 seconds against
requests that cannot succeed.
The correct behavior
On 429 with
Retry-After:Retry-Afteris absentRelated pattern
retry-after-ignored-under-concurrencyCorpus reference: https://github.com/SirBrenton/pitstop-truth