Skip to content

Commit 55bbd21

Browse files
Use rate limit headers for smarter retry in http backoff (#3577)
* use rate limit headers for smarter retry in http backoff * nit * remove capping * remove redundant line
1 parent 644d94a commit 55bbd21

File tree

2 files changed

+45
-3
lines changed

2 files changed

+45
-3
lines changed

src/huggingface_hub/utils/_http.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ def _http_backoff_base(
362362

363363
nb_tries = 0
364364
sleep_time = base_wait_time
365+
ratelimit_reset: Optional[int] = None # seconds to wait for rate limit reset if 429 response
365366

366367
# If `data` is used and is a file object (or any IO), it will be consumed on the
367368
# first HTTP request. We need to save the initial position so that the full content
@@ -373,6 +374,7 @@ def _http_backoff_base(
373374
client = get_session()
374375
while True:
375376
nb_tries += 1
377+
ratelimit_reset = None
376378
try:
377379
# If `data` is used and is a file object (or any IO), set back cursor to
378380
# initial position.
@@ -382,6 +384,8 @@ def _http_backoff_base(
382384
# Perform request and handle response
383385
def _should_retry(response: httpx.Response) -> bool:
384386
"""Handle response and return True if should retry, False if should return/yield."""
387+
nonlocal ratelimit_reset
388+
385389
if response.status_code not in retry_on_status_codes:
386390
return False # Success, don't retry
387391

@@ -393,6 +397,12 @@ def _should_retry(response: httpx.Response) -> bool:
393397
# user ask for retry on a status code that doesn't raise_for_status.
394398
return False # Don't retry, return/yield response
395399

400+
# get rate limit reset time from headers if 429 response
401+
if response.status_code == 429:
402+
ratelimit_info = parse_ratelimit_headers(response.headers)
403+
if ratelimit_info is not None:
404+
ratelimit_reset = ratelimit_info.reset_in_seconds
405+
396406
return True # Should retry
397407

398408
if stream:
@@ -415,9 +425,14 @@ def _should_retry(response: httpx.Response) -> bool:
415425
if nb_tries > max_retries:
416426
raise err
417427

418-
# Sleep for X seconds
419-
logger.warning(f"Retrying in {sleep_time}s [Retry {nb_tries}/{max_retries}].")
420-
time.sleep(sleep_time)
428+
if ratelimit_reset is not None:
429+
actual_sleep = float(ratelimit_reset) + 1 # +1s to avoid rounding issues
430+
logger.warning(f"Rate limited. Waiting {actual_sleep}s before retry [Retry {nb_tries}/{max_retries}].")
431+
else:
432+
actual_sleep = sleep_time
433+
logger.warning(f"Retrying in {actual_sleep}s [Retry {nb_tries}/{max_retries}].")
434+
435+
time.sleep(actual_sleep)
421436

422437
# Update sleep time for next retry
423438
sleep_time = min(max_wait_time, sleep_time * 2) # Exponential backoff

tests/test_utils_http.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,33 @@ def _side_effect_timer() -> Generator[ConnectTimeout, None, None]:
151151
expected_sleep_times = [0.1, 0.2, 0.4, 0.5, 0.5]
152152
self.assertListEqual(sleep_times, expected_sleep_times)
153153

154+
def test_backoff_on_429_uses_ratelimit_header(self) -> None:
155+
"""Test that 429 wait time uses full reset time from ratelimit header."""
156+
sleep_times = []
157+
158+
def _side_effect_timer() -> Generator:
159+
t0 = time.time()
160+
mock_429 = Mock()
161+
mock_429.status_code = 429
162+
mock_429.headers = {"ratelimit": '"api";r=0;t=1'} # Server says wait 1s
163+
yield mock_429
164+
t1 = time.time()
165+
sleep_times.append(round(t1 - t0, 1))
166+
t0 = t1
167+
mock_200 = Mock()
168+
mock_200.status_code = 200
169+
yield mock_200
170+
171+
self.mock_request.side_effect = _side_effect_timer()
172+
173+
response = http_backoff(
174+
"GET", URL, base_wait_time=0.1, max_wait_time=0.5, max_retries=3, retry_on_status_codes=429
175+
)
176+
177+
assert self.mock_request.call_count == 2
178+
assert sleep_times == [2.0]
179+
assert response.status_code == 200
180+
154181

155182
class TestConfigureSession(unittest.TestCase):
156183
def setUp(self) -> None:

0 commit comments

Comments
 (0)