Skip to content

Commit 24f20a3

Browse files
committed
fix(gooddata-eval): wait out the Langfuse rate-limit window on a 429
Langfuse Cloud rate-limits public API calls per organization in fixed one-minute windows, and score writes share the bucket with observation and score reads (30/min on Hobby, 1,000/min on Pro). A 429 carries a Retry-After counting down to the window's reset, often 30-60 s. create_score capped every retry wait at 5 s, so both retries landed in the same exhausted window and the score was dropped. On a Hobby project a 60-request burst lost 43 scores this way. A 429 now waits up to the 60 s window; 5xx retries keep the 5 s cap. jira: trivial risk: low
1 parent 081ab83 commit 24f20a3

2 files changed

Lines changed: 19 additions & 8 deletions

File tree

‎packages/gooddata-eval/src/gooddata_eval/core/langfuse/client.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
_MAX_SCORE_ATTEMPTS = 3
3030
_DEFAULT_RETRY_DELAY = 0.5
3131
_MAX_RETRY_DELAY = 5.0
32+
# Langfuse Cloud rate-limits in fixed one-minute windows, and a 429's Retry-After counts down to
33+
# the window's reset. A shorter wait retries into the same exhausted window and loses the score.
34+
_MAX_THROTTLE_DELAY = 60.0
3235

3336

3437
def _is_retryable(resp: httpx.Response) -> bool:
@@ -38,16 +41,16 @@ def _is_retryable(resp: httpx.Response) -> bool:
3841
def _retry_delay(resp: httpx.Response) -> float:
3942
"""Seconds to wait before the next attempt, from `Retry-After` when the server names one.
4043
41-
Unparsable, negative or NaN values fall back to the default; the cap bounds the wait so a
42-
throttled score cannot hold a linking worker for long.
44+
Unparsable, negative or NaN values fall back to the default. A 429 waits up to one rate-limit
45+
window; any other retryable status is capped at a few seconds.
4346
"""
4447
try:
4548
asked_for = float(resp.headers.get("Retry-After", ""))
4649
except ValueError:
4750
return _DEFAULT_RETRY_DELAY
4851
if not asked_for >= 0:
4952
return _DEFAULT_RETRY_DELAY
50-
return min(asked_for, _MAX_RETRY_DELAY)
53+
return min(asked_for, _MAX_THROTTLE_DELAY if resp.status_code == 429 else _MAX_RETRY_DELAY)
5154

5255

5356
class _TraceListResult:

‎packages/gooddata-eval/tests/test_langfuse_client.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,22 @@ def test_a_retry_after_that_cannot_be_slept_falls_back_to_the_default(header):
151151
assert client_module._retry_delay(httpx.Response(429, headers={"Retry-After": header})) == 0.5
152152

153153

154-
def test_a_retry_after_beyond_the_cap_is_clamped():
155-
assert client_module._retry_delay(httpx.Response(429, headers={"Retry-After": "60"})) == 5.0
154+
def test_a_throttled_score_waits_out_the_whole_rate_limit_window():
155+
# Langfuse limits in fixed one-minute windows and names the wait until the window resets.
156+
assert client_module._retry_delay(httpx.Response(429, headers={"Retry-After": "56"})) == 56.0
157+
158+
159+
def test_a_retry_after_beyond_one_window_is_clamped_to_one_window():
160+
assert client_module._retry_delay(httpx.Response(429, headers={"Retry-After": "600"})) == 60.0
161+
162+
163+
def test_a_server_error_retry_after_stays_short():
164+
assert client_module._retry_delay(httpx.Response(503, headers={"Retry-After": "60"})) == 5.0
156165

157166

158167
def test_a_retry_after_given_as_a_date_falls_back_to_the_default():
159-
# Retry-After is allowed to be an HTTP-date; this client reads seconds only. Langfuse
160-
# documents the header as a number of seconds, and the cap already bounds the wait, so
161-
# parsing a date could only turn the 0.5s fallback into the same 5s ceiling.
168+
# Retry-After is allowed to be an HTTP-date; this client reads seconds only, which is the
169+
# form Langfuse documents and sends.
162170
response = httpx.Response(429, headers={"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"})
163171
assert client_module._retry_delay(response) == 0.5
164172

0 commit comments

Comments
 (0)