Skip to content

RetryBudget

Per-call retry limits do not stop retry storms. When a dependency degrades, every client retries: three attempts per call can triple the load on the one service that is already struggling. A retry budget (the Finagle, gRPC and Envoy approach) caps retries across calls, as a fraction of real traffic.

from nopanic import RetryBudget, retry

user_service = RetryBudget(ratio=0.1)   # retries <= 10% of calls, plus a floor

@retry(attempts=4, budget=user_service)
def get_user(uid): ...

@retry(attempts=4, budget=user_service)  # one budget per downstream, shared
def get_orders(uid): ...

How it counts

Within a sliding window (default 10 s):

allowed retries = ratio x first attempts + min_retries_per_sec x window
  • ratio (default 0.2): retries earned per first attempt.
  • min_retries_per_sec (default 1.0): a floor, so a client with little traffic can still retry at all.

When the budget is spent, retry stops and re-raises the original exception (event retry.gave_up, reason budget). Under a real outage, retry traffic shrinks to the floor instead of multiplying; once things recover, the budget refills with traffic.

budget.available reports retries spendable right now. The budget is thread-safe and uses constant memory (bucketed counters, like the breaker).