ProvenPin Install

httpx client + tenacity retries with backoff, tested with respx: working versions (tested 2026-10-15)

Retry transient HTTP failures (5xx, 429, timeouts) with tenacity around an httpx client, honoring a max attempt count, tested offline with respx.

passed4 passed · 0 failed2026-10-15 13:23 UTC
Status
passed
Last passed
2026-10-15 13:23 UTC
Tests
4 / 4
Language
Python
Runtime
Python 3.13.16
pinned versions4
httpx@0.28.1tenacity@9.2.1respx@0.23.1pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38741369695

The code

Exactly the files that passed. Your agent can pull them with get_recipe("httpx-tenacity-retry") (install), or see them on GitHub.

requirements.txt
httpx==0.28.1
tenacity==9.2.1
respx==0.23.1
pytest==9.1.1
client.py
import httpx
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential

def _transient(exc: BaseException) -> bool:
    if isinstance(exc, (httpx.TimeoutException, httpx.ConnectError)):
        return True
    return isinstance(exc, httpx.HTTPStatusError) and (exc.response.status_code == 429 or exc.response.status_code >= 500)

class ApiClient:
    def __init__(self, base_url: str, attempts: int = 4, wait_max: float = 8.0):
        self.http = httpx.Client(base_url=base_url, timeout=5.0)
        self._get = retry(
            retry=retry_if_exception(_transient),
            stop=stop_after_attempt(attempts),
            wait=wait_exponential(multiplier=0.5, max=wait_max),
            reraise=True,  # surface the real httpx error, not tenacity.RetryError
        )(self._get_once)

    def _get_once(self, path: str):
        r = self.http.get(path)
        r.raise_for_status()
        return r.json()

    def get_json(self, path: str):
        return self._get(path)
tests/test_client.py
import httpx
import pytest
import respx
from client import ApiClient

BASE = "https://api.example.test"

def client(attempts=4):
    return ApiClient(BASE, attempts=attempts, wait_max=0)

@respx.mock
def test_retries_5xx_then_succeeds():
    route = respx.get(f"{BASE}/x").mock(side_effect=[httpx.Response(503), httpx.Response(502), httpx.Response(200, json={"ok": True})])
    assert client().get_json("/x") == {"ok": True}
    assert route.call_count == 3

@respx.mock
def test_retries_timeouts():
    route = respx.get(f"{BASE}/x").mock(side_effect=[httpx.ReadTimeout("slow"), httpx.Response(200, json=[])])
    assert client().get_json("/x") == [] and route.call_count == 2

@respx.mock
def test_no_retry_on_404():
    route = respx.get(f"{BASE}/x").mock(return_value=httpx.Response(404))
    with pytest.raises(httpx.HTTPStatusError):
        client().get_json("/x")
    assert route.call_count == 1

@respx.mock
def test_gives_up_and_reraises():
    route = respx.get(f"{BASE}/x").mock(return_value=httpx.Response(500))
    with pytest.raises(httpx.HTTPStatusError):
        client(attempts=3).get_json("/x")
    assert route.call_count == 3
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.