From: Junio C Hamano <gitster@pobox.com>
To: "Ravi Mistry via GitGitGadget" <gitgitgadget@gmail.com>
Cc: git@vger.kernel.org,
Vaidas Pilkauskas <vaidas.pilkauskas@shopify.com>,
Patrick Steinhardt <ps@pks.im>,
"brian m. carlson" <sandals@crustytoothpaste.net>,
Ravi Mistry <rmistry@google.com>
Subject: Re: [PATCH 2/4] http: add exponential backoff without Retry-After
Date: Tue, 03 Nov 2026 05:21:14 -0800 [thread overview]
Message-ID: <xmqqpkwmaw51.fsf@gitster.g> (raw)
In-Reply-To: <36cef34668b6e2ceb867ca99029e8bc11d9cd63f.1793462224.git.gitgitgadget@gmail.com> (Ravi Mistry via GitGitGadget's message of "Sat, 31 Oct 2026 15:57:02 +0000")
"Ravi Mistry via GitGitGadget" <gitgitgadget@gmail.com> writes:
> From: Ravi Mistry <rmistry@google.com>
>
> When a server returns HTTP 429 without a Retry-After header, Git waits
> the same fixed http.retryAfter delay on every retry. If that delay is
> short (e.g., 1s), Git burns through all of its retries in just a few
> seconds before a busy server can recover. If that delay is long, even a
> brief glitch has to wait a long time on the first retry.
>
> Pass the 1-based retry attempt count to handle_rate_limit_retry() and
> double http.retryAfter on each subsequent retry (1s, 2s, 4s, ..., via
> http.retryAfter * 2^(attempt - 1)), clamping the delay at
> http.maxRetryTime. This keeps the first retry fast while giving the
> server more time to recover on later attempts, matching curl's own
> --retry behavior. When the server provides an explicit Retry-After
> header, continue to honor the server-provided delay (or fail fast if it
> exceeds http.maxRetryTime).
The logic makes perfect sense (I am taking the above description at
face value without checking if this really matches cURL's behavior
myself, though).
> http.retryAfter::
> + Initial wait time in seconds before retrying when a server returns
> HTTP 429 (Too Many Requests) without a Retry-After header.
> + Defaults to 0 (retry immediately). When set to a positive value and
> + no Retry-After header is present, the delay doubles on each
> + subsequent retry attempt (`http.retryAfter * 2^(attempt - 1)`),
> + capped at `http.maxRetryTime`. When a Retry-After header is
It somehow feels that the default value of 0 (immediate retry) does
not mesh well with exponential backoff. Will we keep retrying
immediately after failing until we run out of retries?
Also, it feels somewhat inconsistent with how maxretrytime is
currently treated. Specifically, when the other side tells us via
Retry-After, or the configuration specifies via http.retryAfter, to
wait longer than maxretrytime, instead of capping the wait, we
immediately give up without retrying. But when retryAfter grows
with exponential backoff, the wait is capped at maxretrytime instead
of resulting in an immediate failure.
The handling of maxretrytime in this definition, where it caps the
waiting time before retrying, is definitely more useful than the
traditional "if we are told to wait longer than this, we fail
instead of capping", I would think.
> @@ -348,7 +351,9 @@ http.maxRetryTime::
> handling HTTP 429 (Too Many Requests) responses. If the server
> requests a delay (via Retry-After header) or if `http.retryAfter`
> is configured with a value that exceeds this maximum, Git will fail
> - immediately rather than waiting. Default is 300 seconds (5 minutes).
> + immediately rather than waiting. Exponentially backed-off delays
> + computed from `http.retryAfter` are capped at this maximum. Default
> + is 300 seconds (5 minutes).
> -static long handle_rate_limit_retry(long slot_retry_after)
> +static long handle_rate_limit_retry(long slot_retry_after, int attempt)
> {
> /* Use the slot-specific retry_after value or configured default */
> if (slot_retry_after >= 0) {
> @@ -2427,6 +2427,8 @@ static long handle_rate_limit_retry(long slot_retry_after)
> }
> return slot_retry_after;
> } else {
> + long delay = http_retry_after;
> +
> /* No Retry-After header provided, use configured default */
> if (http_retry_after > http_max_retry_time) {
> error(_("configured http.retryAfter exceeds http.maxRetryTime (%ld > %ld seconds)"),
> @@ -2437,7 +2439,14 @@ static long handle_rate_limit_retry(long slot_retry_after)
> }
> trace2_data_string("http", the_repository,
> "http/429-retry-source", "config-default");
> - return http_retry_after;
> + for (int a = 1; a < attempt && delay > 0; a++) {
> + if (delay >= http_max_retry_time - delay) {
> + delay = http_max_retry_time;
> + break;
> + }
> + delay *= 2;
> + }
> + return delay;
> }
> }
I guess what is in the loop is a tricky arithmetic to compute
delay = ((delay * 2) > http_max_retry_time)
? http_max_retry_time
: (delay * 2);
without integer overflow? Very effective.
> @@ -2449,6 +2458,7 @@ static int http_request_recoverable(const char *url,
> int i = 3;
> int ret;
> int rate_limit_retries = http_max_retries;
> + int attempt = 0;
>
> if (!options)
> options = &empty_opts;
> @@ -2498,7 +2508,7 @@ static int http_request_recoverable(const char *url,
> BUG("Unknown http_request target");
> }
> if (ret == HTTP_RATE_LIMITED) {
> - retry_delay = handle_rate_limit_retry(options->retry_after);
> + retry_delay = handle_rate_limit_retry(options->retry_after, ++attempt);
> if (retry_delay < 0)
> return HTTP_ERROR;
Interesting.
We already have to deal with the maximum number of retries to honor
http_max_retries, so we must be counting how many times we have
retried so far. But we still need to introduce another variable to
count the number of attempts.
Or would it suffice to pass (http_max_retries - rate_limit_retries)
in place of the 'attempt' parameter?
Not complaining. Just making an observation.
next prev parent reply other threads:[~2026-11-03 13:21 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-31 15:57 [PATCH 0/4] http: exponential backoff, configurable status codes, and post_rpc() retries Ravi Mistry via GitGitGadget
2026-10-31 15:57 ` [PATCH 1/4] http: fix retry count and Retry-After timing Ravi Mistry via GitGitGadget
2026-11-03 1:03 ` Junio C Hamano
2026-11-03 19:05 ` Ravi Mistry
2026-10-31 15:57 ` [PATCH 2/4] http: add exponential backoff without Retry-After Ravi Mistry via GitGitGadget
2026-11-03 13:21 ` Junio C Hamano [this message]
2026-11-03 19:05 ` Ravi Mistry
2026-10-31 15:57 ` [PATCH 3/4] http: add http.retryStatusCodes config option Ravi Mistry via GitGitGadget
2026-11-03 13:41 ` Junio C Hamano
2026-11-03 19:06 ` Ravi Mistry
2026-10-31 15:57 ` [PATCH 4/4] remote-curl: retry transient HTTP errors in post_rpc() Ravi Mistry via GitGitGadget
2026-11-03 14:47 ` Junio C Hamano
2026-11-03 18:57 ` [PATCH v2 0/4] http: exponential backoff, configurable status codes, and post_rpc() retries Ravi Mistry via GitGitGadget
2026-11-03 18:57 ` [PATCH v2 1/4] http: fix retry count and Retry-After timing Ravi Mistry via GitGitGadget
2026-11-03 18:57 ` [PATCH v2 2/4] http: add exponential backoff without Retry-After Ravi Mistry via GitGitGadget
2026-11-03 18:57 ` [PATCH v2 3/4] http: add http.retryStatusCodes config option Ravi Mistry via GitGitGadget
2026-11-03 20:56 ` Junio C Hamano
2026-11-03 18:57 ` [PATCH v2 4/4] remote-curl: retry transient HTTP errors in post_rpc() Ravi Mistry via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=xmqqpkwmaw51.fsf@gitster.g \
--to=gitster@pobox.com \
--cc=git@vger.kernel.org \
--cc=gitgitgadget@gmail.com \
--cc=ps@pks.im \
--cc=rmistry@google.com \
--cc=sandals@crustytoothpaste.net \
--cc=vaidas.pilkauskas@shopify.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox