Skip to content

fix(mcp): derive streamable HTTP retry backoff from backoffs taken - #4174

Merged
seratch merged 1 commit into
openai:mainfrom
rxits:fix/mcp-backoff-schedule
Aug 4, 2026
Merged

fix(mcp): derive streamable HTTP retry backoff from backoffs taken#4174
seratch merged 1 commit into
openai:mainfrom
rxits:fix/mcp-backoff-schedule

Conversation

@rxits

@rxits rxits commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

MCPServerStreamableHttp.call_tool replaces the shared retry loop with its own, and takes the backoff exponent from retries_used — a retry budget counter rather than a count of backoffs already taken. The two disagree in both directions:

  • retries_used is only advanced at the top of the loop when max_retry_attempts != -1. So with max_retry_attempts = -1 ("retry indefinitely") the generic except Exception path evaluates retry_backoff_seconds_base * 2**0 on every pass and the delay never grows — an unreachable server is retried at a flat interval forever.
  • One isolated-session retry charges retries_used twice, once at the top of the loop and once in the _IsolatedSessionRetryFailed handler. With a finite budget the delay then quadruples per sleep: 1s, 4s, 16s instead of 1s, 2s, 4s.

.agents/references/local-mcp-server-lifecycle.md states that generic list_tools() / call_tool() retries "use the configured attempt count and backoff", and _run_with_retries — the schedule every other retry path in this file follows — is base * 2 ** (attempts - 1) over actual attempts. This change makes the streamable HTTP override agree with it by counting backoffs in a dedicated local and using that as the exponent. Budget accounting, attempt counts, and isolated-session retry eligibility are unchanged.

Test plan

Two tests added to tests/mcp/test_client_session_retries.py, both patching asyncio.sleep to record the delay sequence:

  • test_streamable_http_backoff_grows_with_unlimited_retriesmax_retry_attempts = -1, expects [1.0, 2.0, 4.0]. On unmodified main it records [1.0, 1.0, 1.0].
  • test_streamable_http_backoff_matches_generic_schedule_on_isolated_retry — finite budget with an isolated-session retry, expects [1.0, 2.0, 4.0]. On unmodified main it records [1.0, 4.0, 16.0].

Both were confirmed failing against unmodified main before the fix and passing after it.

.agents/skills/code-change-verification/scripts/run.sh: make format, make lint and make typecheck pass. make tests reports a single failure, tests/test_run_step_execution.py::test_multiple_tool_calls_raise_late_fatal_sibling_exception_after_cancellation[20], which is pre-existing and unrelated to this change: it reproduces identically on unmodified main at 19e364c under the same pytest -n auto --dist worksteal -m "not serial" invocation, and passes when that test is run on its own. I have left the "all verification steps pass" box unchecked for that reason.

Issue number

N/A

Checks

  • I've added new tests, if relevant
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass
  • If using Codex, I've run /review before submitting this PR

MCPServerStreamableHttp.call_tool used retries_used, a retry-budget
counter, as its backoff exponent. That counter is deliberately frozen
while max_retry_attempts is -1, and one isolated-session retry charges
it twice, so the delay was constant in the first case and grew 4x per
sleep in the second instead of following the configured schedule.

Count backoffs in a dedicated local and use that for the exponent.
Budget accounting, attempt counts, and isolated-session retry
eligibility are unchanged.
@seratch seratch added this to the 0.19.x milestone Aug 4, 2026
@seratch
seratch merged commit 0f5d3c3 into openai:main Aug 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants