[{"data":1,"prerenderedAt":2720},["ShallowReactive",2],{"page-\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fretrying-failed-http-requests-with-tenacity\u002F":3,"faq-schema-\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fretrying-failed-http-requests-with-tenacity\u002F":2699},{"id":4,"title":5,"body":6,"description":2689,"extension":2690,"meta":2691,"navigation":566,"path":2695,"seo":2696,"stem":2697,"__hash__":2698},"content\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fretrying-failed-http-requests-with-tenacity\u002Findex.md","Retrying Failed HTTP Requests with tenacity Without Amplifying the Outage",{"type":7,"value":8,"toc":2677},"minimark",[9,13,23,31,36,46,136,156,316,320,328,343,451,466,1314,1320,1324,1327,1330,1417,1685,1692,1696,1703,1711,1984,1987,1991,1998,2077,2427,2446,2450,2516,2533,2537,2562,2579,2583,2591,2595,2602,2608,2624,2630,2636,2640,2673],[10,11,5],"h1",{"id":12},"retrying-failed-http-requests-with-tenacity-without-amplifying-the-outage",[14,15,16,17,22],"p",{},"A naive retry loop is the fastest way to turn a supplier's five-second blip into your own thirty-minute incident. Three retries per call sounds harmless until every worker in your fleet does it at the same instant against an already-struggling upstream, and you have quietly tripled the load on the exact service that just told you it could not cope. This page resolves one decision: what your retry policy should actually contain when the call is a paid, customer-facing dependency. Part of the ",[18,19,21],"a",{"href":20},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002F","Making HTTP Requests with the Requests Library"," guide.",[14,24,25,26,30],{},"The short version: use ",[27,28,29],"code",{},"tenacity",", retry only on transport errors and a narrow allowlist of status codes, wait with full jitter, cap the whole operation with a deadline rather than an attempt count, attach an idempotency key to every mutating call, and put a circuit breaker in front of the retry so a sustained outage costs you one request per cooldown window instead of thousands.",[32,33,35],"h2",{"id":34},"which-failures-are-actually-worth-retrying","Which failures are actually worth retrying",[14,37,38,39,42,43,45],{},"Retrying is only correct when the failure is transient and the request is safe to repeat. Every other retry burns money and latency for nothing. A ",[27,40,41],{},"400"," will be a ",[27,44,41],{}," on the fourth attempt too; retrying it just multiplies your egress bill and pushes the eventual error to your customer four seconds late instead of one.",[47,48,49,65],"table",{},[50,51,52],"thead",{},[53,54,55,59,62],"tr",{},[56,57,58],"th",{},"Response",[56,60,61],{},"Retry?",[56,63,64],{},"Why",[66,67,68,80,90,104,115,126],"tbody",{},[53,69,70,74,77],{},[71,72,73],"td",{},"Connect \u002F read error",[71,75,76],{},"Yes",[71,78,79],{},"Never reached the handler",[53,81,82,85,87],{},[71,83,84],{},"408, 425",[71,86,76],{},[71,88,89],{},"Server asked for a repeat",[53,91,92,95,101],{},[71,93,94],{},"429",[71,96,97,98],{},"Yes, honour ",[27,99,100],{},"Retry-After",[71,102,103],{},"Pacing signal, not a fault",[53,105,106,109,112],{},[71,107,108],{},"500, 502, 503, 504",[71,110,111],{},"Yes, if idempotent",[71,113,114],{},"Usually transient",[53,116,117,120,123],{},[71,118,119],{},"400, 401, 403, 404, 422",[71,121,122],{},"No",[71,124,125],{},"Deterministic, fix the request",[53,127,128,131,133],{},[71,129,130],{},"409",[71,132,122],{},[71,134,135],{},"Conflict needs a decision",[14,137,138,139,142,143,146,147,150,151,155],{},"The nuance sits on ",[27,140,141],{},"500"," and on read timeouts. Both mean the server may have already applied your write before failing to tell you about it. Retrying a ",[27,144,145],{},"POST \u002Fcharges"," after a read timeout can double-bill a customer, which is a refund, a support ticket, and a chargeback fee — far more expensive than the failed request. So the rule is not \"retry 5xx\", it is \"retry 5xx when the call is idempotent, and make mutating calls idempotent on purpose\". A ",[27,148,149],{},"401"," is never a retry candidate either; if you see one intermittently you have a token refresh bug, and ",[18,152,154],{"href":153},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fdebugging-401-unauthorized-api-errors\u002F","debugging 401 unauthorized errors"," is the right thread to pull instead.",[157,158,166,167,166,171,166,175,166,182,166,201,166,211,166,219,166,224,166,231,166,236,166,241,166,244,166,247,166,250,166,254,166,257,166,262,166,266,166,268,166,273,166,279,166,284,166,288,166,292,166,296,166,299,166,304,166,308,166,312],"svg",{"viewBox":159,"role":160,"ariaLabelledBy":161,"xmlns":164,"style":165},"0 0 720 300","img",[162,163],"tnc-cls-t","tnc-cls-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[168,169,170],"title",{"id":162},"Retry classification decision tree",[172,173,174],"desc",{"id":163},"A failed response is routed by transport error, status class and idempotency into retry, honour Retry-After, or fail fast.",[176,177],"rect",{"x":178,"y":178,"width":179,"height":180,"fill":181},"0","720","300","var(--c-surface)",[183,184,185,186,166],"defs",{},"\n    ",[187,188,195,196,185],"marker",{"id":189,"viewBox":190,"refX":191,"refY":192,"markerWidth":193,"markerHeight":193,"orient":194},"tnc-cls-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[197,198],"path",{"d":199,"fill":200},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[176,202],{"x":203,"y":204,"width":205,"height":206,"rx":207,"fill":208,"stroke":209,"style":210},"16","126","132","48","10","var(--c-surface-alt)","var(--c-border)","stroke-width:2;",[212,213,218],"text",{"x":214,"y":215,"fill":216,"style":217},"82","147","var(--c-text)","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Call failed",[212,220,223],{"x":214,"y":221,"fill":200,"style":222},"163","text-anchor:middle;font-size:11;font-family:var(--font-sans);","classify first",[225,226],"line",{"x1":227,"y1":228,"x2":229,"y2":228,"stroke":200,"style":230},"148","150","196","stroke-width:2;marker-end:url(#tnc-cls-arrow);",[176,232],{"x":229,"y":233,"width":228,"height":234,"rx":207,"fill":181,"stroke":235,"style":210},"120","60","var(--c-blue)",[212,237,240],{"x":238,"y":239,"fill":216,"style":217},"271","145","Got a status?",[212,242,243],{"x":238,"y":221,"fill":200,"style":222},"no = transport",[225,245],{"x1":238,"y1":233,"x2":238,"y2":246,"stroke":200,"style":210},"70",[225,248],{"x1":238,"y1":246,"x2":249,"y2":246,"stroke":200,"style":230},"424",[212,251,253],{"x":252,"y":234,"fill":200,"style":222},"330","no status",[225,255],{"x1":256,"y1":228,"x2":249,"y2":228,"stroke":200,"style":230},"346",[212,258,261],{"x":259,"y":260,"fill":200,"style":222},"385","141","4xx",[225,263],{"x1":238,"y1":264,"x2":238,"y2":265,"stroke":200,"style":210},"180","238",[225,267],{"x1":238,"y1":265,"x2":249,"y2":265,"stroke":200,"style":230},[212,269,272],{"x":270,"y":271,"fill":200,"style":222},"336","229","429 \u002F 5xx",[176,274],{"x":249,"y":275,"width":276,"height":277,"rx":207,"fill":181,"stroke":278,"style":210},"44","272","52","var(--c-teal)",[212,280,283],{"x":281,"y":282,"fill":216,"style":217},"560","66","Retry with full jitter",[212,285,287],{"x":281,"y":286,"fill":200,"style":222},"83","connect and read errors",[176,289],{"x":249,"y":290,"width":276,"height":277,"rx":207,"fill":181,"stroke":291,"style":210},"124","var(--c-coral)",[212,293,295],{"x":281,"y":294,"fill":216,"style":217},"146","Fail fast, no retry",[212,297,298],{"x":281,"y":221,"fill":200,"style":222},"400 401 403 404 409 422",[176,300],{"x":249,"y":301,"width":276,"height":302,"rx":207,"fill":181,"stroke":303,"style":210},"212","64","var(--c-yellow)",[212,305,307],{"x":281,"y":306,"fill":216,"style":217},"234","Idempotent?",[212,309,311],{"x":281,"y":310,"fill":200,"style":222},"251","yes = retry, honour Retry-After",[212,313,315],{"x":281,"y":314,"fill":200,"style":222},"267","no = send an idempotency key",[32,317,319],{"id":318},"backoff-with-real-jitter-not-decorative-jitter","Backoff with real jitter, not decorative jitter",[14,321,322,323,327],{},"Exponential backoff spaces attempts out. Jitter is what stops your workers from synchronising. Without it, fifty containers that all failed at the same second retry at the same second, again at second three, again at second seven — a self-inflicted load spike arriving exactly when the upstream is trying to recover. This is the same failure shape covered in ",[18,324,326],{"href":325},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fbest-practices-for-api-rate-limiting\u002F","best practices for API rate limiting",", and the fix is identical: randomise across the whole wait window, not a slice of it.",[14,329,330,331,334,335,338,339,342],{},"Read tenacity's built-in helper carefully. ",[27,332,333],{},"wait_exponential_jitter(initial, max, exp_base, jitter)"," computes the exponential delay and then adds a random value between zero and ",[27,336,337],{},"jitter",". With ",[27,340,341],{},"jitter=1"," on a delay that has already grown to eight seconds, your fleet is still spread across a one-second band — barely spread at all. What you want is full jitter: pick uniformly from zero to the current ceiling, so attempt five lands anywhere in an eight-second window and the herd genuinely disperses.",[157,344,166,349,166,352,166,355,166,358,166,363,166,367,166,371,166,375,166,379,166,383,166,387,166,391,166,397,166,401,166,405,166,409,166,413,166,417,166,420,166,424,166,427,166,431,166,435,166,440,166,444,166,447],{"viewBox":345,"role":160,"ariaLabelledBy":346,"xmlns":164,"style":165},"0 0 720 260",[347,348],"tnc-jit-t","tnc-jit-d",[168,350,351],{"id":347},"Additive jitter versus full jitter spread",[172,353,354],{"id":348},"Per attempt, additive jitter keeps retries in a one-second band while full jitter spreads them across the whole exponential window.",[176,356],{"x":178,"y":178,"width":179,"height":357,"fill":181},"260",[212,359,362],{"x":203,"y":360,"fill":216,"style":361},"24","font-size:13;font-family:var(--font-sans);","Wait window per attempt (base 0.5s, doubling)",[225,364],{"x1":365,"y1":301,"x2":366,"y2":301,"stroke":209,"style":210},"110","700",[212,368,370],{"x":365,"y":369,"fill":200,"style":222},"232","0s",[212,372,374],{"x":373,"y":369,"fill":200,"style":222},"257","2s",[212,376,378],{"x":377,"y":369,"fill":200,"style":222},"404","4s",[212,380,382],{"x":381,"y":369,"fill":200,"style":222},"551","6s",[212,384,386],{"x":385,"y":369,"fill":200,"style":222},"698","8s",[212,388,390],{"x":203,"y":277,"fill":200,"style":389},"font-size:11;font-family:var(--font-sans);","attempt 2",[176,392],{"x":365,"y":393,"width":394,"height":395,"rx":396,"fill":278},"42","18","14","3",[176,398],{"x":365,"y":234,"width":399,"height":395,"rx":400,"fill":291},"4","2",[212,402,404],{"x":203,"y":403,"fill":200,"style":389},"96","attempt 3",[176,406],{"x":365,"y":407,"width":408,"height":395,"rx":396,"fill":278},"86","37",[176,410],{"x":411,"y":412,"width":394,"height":395,"rx":396,"fill":291},"129","104",[212,414,416],{"x":203,"y":415,"fill":200,"style":389},"140","attempt 4",[176,418],{"x":365,"y":419,"width":215,"height":395,"rx":396,"fill":278},"130",[176,421],{"x":422,"y":227,"width":423,"height":395,"rx":396,"fill":291},"184","73",[212,425,426],{"x":203,"y":422,"fill":200,"style":389},"attempt 5",[176,428],{"x":365,"y":429,"width":430,"height":395,"rx":396,"fill":278},"174","588",[176,432],{"x":433,"y":434,"width":423,"height":395,"rx":396,"fill":291},"625","192",[176,436],{"x":437,"y":438,"width":395,"height":439,"rx":396,"fill":278},"440","8","12",[212,441,443],{"x":442,"y":394,"fill":216,"style":389},"460","full jitter",[176,445],{"x":446,"y":438,"width":395,"height":439,"rx":396,"fill":291},"546",[212,448,450],{"x":449,"y":394,"fill":216,"style":389},"566","additive (1s)",[14,452,453,454,457,458,462,463,465],{},"Here is the policy I ship. It uses ",[27,455,456],{},"httpx"," because new outbound code should be async — the reasoning is in ",[18,459,461],{"href":460},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fhttpx-vs-requests-for-async\u002F","httpx vs requests for async"," — and it honours ",[27,464,100],{}," when the server sends one, because the server's number always beats your guess.",[467,468,473],"pre",{"className":469,"code":470,"language":471,"meta":472,"style":472},"language-python shiki shiki-themes github-light github-dark","import logging\nimport os\nimport random\nimport httpx\nfrom tenacity import (\n    AsyncRetrying,\n    RetryError,\n    retry_if_exception_type,\n    stop_after_attempt,\n    stop_after_delay,\n)\n\nlogger = logging.getLogger(__name__)\n\nRETRYABLE_STATUS = {408, 425, 429, 500, 502, 503, 504}\nBASE_DELAY = float(os.getenv(\"RETRY_BASE_DELAY\", \"0.5\"))\nMAX_DELAY = float(os.getenv(\"RETRY_MAX_DELAY\", \"8\"))\nTOTAL_BUDGET = float(os.getenv(\"RETRY_TOTAL_BUDGET\", \"20\"))\n\n\nclass RetryableStatus(Exception):\n    def __init__(self, status_code: int, retry_after: float | None) -> None:\n        super().__init__(f\"upstream returned {status_code}\")\n        self.status_code = status_code\n        self.retry_after = retry_after\n\n\ndef full_jitter(retry_state) -> float:\n    \"\"\"Uniform pick from 0..ceiling, or the server's Retry-After if larger.\"\"\"\n    ceiling = min(MAX_DELAY, BASE_DELAY * 2 ** (retry_state.attempt_number - 1))\n    delay = random.uniform(0, ceiling)\n    exc = retry_state.outcome.exception() if retry_state.outcome else None\n    if isinstance(exc, RetryableStatus) and exc.retry_after:\n        return max(delay, exc.retry_after)\n    return delay\n\n\ndef parse_retry_after(response: httpx.Response) -> float | None:\n    raw = response.headers.get(\"retry-after\")\n    match raw:\n        case None:\n            return None\n        case value if value.isdigit():\n            return float(value)\n        case _:\n            return None\n\n\nasync def call_upstream(client: httpx.AsyncClient, path: str) -> dict:\n    retryer = AsyncRetrying(\n        retry=retry_if_exception_type((httpx.TransportError, RetryableStatus)),\n        wait=full_jitter,\n        stop=stop_after_attempt(5) | stop_after_delay(TOTAL_BUDGET),\n        reraise=True,\n    )\n    async for attempt in retryer:\n        with attempt:\n            response = await client.get(path)\n            if response.status_code in RETRYABLE_STATUS:\n                raise RetryableStatus(\n                    response.status_code, parse_retry_after(response)\n                )\n            response.raise_for_status()\n            return response.json()\n    raise RetryError(None)\n","python","",[27,474,475,487,495,503,511,525,531,537,543,549,555,561,568,586,591,638,664,686,708,713,718,737,773,807,821,834,839,844,860,866,905,921,944,962,974,983,988,993,1012,1028,1037,1047,1055,1068,1078,1086,1093,1098,1103,1128,1139,1151,1162,1189,1203,1209,1227,1236,1250,1266,1275,1281,1287,1293,1301],{"__ignoreMap":472},[476,477,479,483],"span",{"class":225,"line":478},1,[476,480,482],{"class":481},"szBVR","import",[476,484,486],{"class":485},"sVt8B"," logging\n",[476,488,490,492],{"class":225,"line":489},2,[476,491,482],{"class":481},[476,493,494],{"class":485}," os\n",[476,496,498,500],{"class":225,"line":497},3,[476,499,482],{"class":481},[476,501,502],{"class":485}," random\n",[476,504,506,508],{"class":225,"line":505},4,[476,507,482],{"class":481},[476,509,510],{"class":485}," httpx\n",[476,512,514,517,520,522],{"class":225,"line":513},5,[476,515,516],{"class":481},"from",[476,518,519],{"class":485}," tenacity ",[476,521,482],{"class":481},[476,523,524],{"class":485}," (\n",[476,526,528],{"class":225,"line":527},6,[476,529,530],{"class":485},"    AsyncRetrying,\n",[476,532,534],{"class":225,"line":533},7,[476,535,536],{"class":485},"    RetryError,\n",[476,538,540],{"class":225,"line":539},8,[476,541,542],{"class":485},"    retry_if_exception_type,\n",[476,544,546],{"class":225,"line":545},9,[476,547,548],{"class":485},"    stop_after_attempt,\n",[476,550,552],{"class":225,"line":551},10,[476,553,554],{"class":485},"    stop_after_delay,\n",[476,556,558],{"class":225,"line":557},11,[476,559,560],{"class":485},")\n",[476,562,564],{"class":225,"line":563},12,[476,565,567],{"emptyLinePlaceholder":566},true,"\n",[476,569,571,574,577,580,584],{"class":225,"line":570},13,[476,572,573],{"class":485},"logger ",[476,575,576],{"class":481},"=",[476,578,579],{"class":485}," logging.getLogger(",[476,581,583],{"class":582},"sj4cs","__name__",[476,585,560],{"class":485},[476,587,589],{"class":225,"line":588},14,[476,590,567],{"emptyLinePlaceholder":566},[476,592,594,597,600,603,606,609,612,614,616,618,620,622,625,627,630,632,635],{"class":225,"line":593},15,[476,595,596],{"class":582},"RETRYABLE_STATUS",[476,598,599],{"class":481}," =",[476,601,602],{"class":485}," {",[476,604,605],{"class":582},"408",[476,607,608],{"class":485},", ",[476,610,611],{"class":582},"425",[476,613,608],{"class":485},[476,615,94],{"class":582},[476,617,608],{"class":485},[476,619,141],{"class":582},[476,621,608],{"class":485},[476,623,624],{"class":582},"502",[476,626,608],{"class":485},[476,628,629],{"class":582},"503",[476,631,608],{"class":485},[476,633,634],{"class":582},"504",[476,636,637],{"class":485},"}\n",[476,639,641,644,646,649,652,656,658,661],{"class":225,"line":640},16,[476,642,643],{"class":582},"BASE_DELAY",[476,645,599],{"class":481},[476,647,648],{"class":582}," float",[476,650,651],{"class":485},"(os.getenv(",[476,653,655],{"class":654},"sZZnC","\"RETRY_BASE_DELAY\"",[476,657,608],{"class":485},[476,659,660],{"class":654},"\"0.5\"",[476,662,663],{"class":485},"))\n",[476,665,667,670,672,674,676,679,681,684],{"class":225,"line":666},17,[476,668,669],{"class":582},"MAX_DELAY",[476,671,599],{"class":481},[476,673,648],{"class":582},[476,675,651],{"class":485},[476,677,678],{"class":654},"\"RETRY_MAX_DELAY\"",[476,680,608],{"class":485},[476,682,683],{"class":654},"\"8\"",[476,685,663],{"class":485},[476,687,689,692,694,696,698,701,703,706],{"class":225,"line":688},18,[476,690,691],{"class":582},"TOTAL_BUDGET",[476,693,599],{"class":481},[476,695,648],{"class":582},[476,697,651],{"class":485},[476,699,700],{"class":654},"\"RETRY_TOTAL_BUDGET\"",[476,702,608],{"class":485},[476,704,705],{"class":654},"\"20\"",[476,707,663],{"class":485},[476,709,711],{"class":225,"line":710},19,[476,712,567],{"emptyLinePlaceholder":566},[476,714,716],{"class":225,"line":715},20,[476,717,567],{"emptyLinePlaceholder":566},[476,719,721,724,728,731,734],{"class":225,"line":720},21,[476,722,723],{"class":481},"class",[476,725,727],{"class":726},"sScJk"," RetryableStatus",[476,729,730],{"class":485},"(",[476,732,733],{"class":582},"Exception",[476,735,736],{"class":485},"):\n",[476,738,740,743,746,749,752,755,758,761,764,767,770],{"class":225,"line":739},22,[476,741,742],{"class":481},"    def",[476,744,745],{"class":582}," __init__",[476,747,748],{"class":485},"(self, status_code: ",[476,750,751],{"class":582},"int",[476,753,754],{"class":485},", retry_after: ",[476,756,757],{"class":582},"float",[476,759,760],{"class":481}," |",[476,762,763],{"class":582}," None",[476,765,766],{"class":485},") -> ",[476,768,769],{"class":582},"None",[476,771,772],{"class":485},":\n",[476,774,776,779,782,785,787,790,793,796,799,802,805],{"class":225,"line":775},23,[476,777,778],{"class":582},"        super",[476,780,781],{"class":485},"().",[476,783,784],{"class":582},"__init__",[476,786,730],{"class":485},[476,788,789],{"class":481},"f",[476,791,792],{"class":654},"\"upstream returned ",[476,794,795],{"class":582},"{",[476,797,798],{"class":485},"status_code",[476,800,801],{"class":582},"}",[476,803,804],{"class":654},"\"",[476,806,560],{"class":485},[476,808,810,813,816,818],{"class":225,"line":809},24,[476,811,812],{"class":582},"        self",[476,814,815],{"class":485},".status_code ",[476,817,576],{"class":481},[476,819,820],{"class":485}," status_code\n",[476,822,824,826,829,831],{"class":225,"line":823},25,[476,825,812],{"class":582},[476,827,828],{"class":485},".retry_after ",[476,830,576],{"class":481},[476,832,833],{"class":485}," retry_after\n",[476,835,837],{"class":225,"line":836},26,[476,838,567],{"emptyLinePlaceholder":566},[476,840,842],{"class":225,"line":841},27,[476,843,567],{"emptyLinePlaceholder":566},[476,845,847,850,853,856,858],{"class":225,"line":846},28,[476,848,849],{"class":481},"def",[476,851,852],{"class":726}," full_jitter",[476,854,855],{"class":485},"(retry_state) -> ",[476,857,757],{"class":582},[476,859,772],{"class":485},[476,861,863],{"class":225,"line":862},29,[476,864,865],{"class":654},"    \"\"\"Uniform pick from 0..ceiling, or the server's Retry-After if larger.\"\"\"\n",[476,867,869,872,874,877,879,881,883,885,888,891,894,897,900,903],{"class":225,"line":868},30,[476,870,871],{"class":485},"    ceiling ",[476,873,576],{"class":481},[476,875,876],{"class":582}," min",[476,878,730],{"class":485},[476,880,669],{"class":582},[476,882,608],{"class":485},[476,884,643],{"class":582},[476,886,887],{"class":481}," *",[476,889,890],{"class":582}," 2",[476,892,893],{"class":481}," **",[476,895,896],{"class":485}," (retry_state.attempt_number ",[476,898,899],{"class":481},"-",[476,901,902],{"class":582}," 1",[476,904,663],{"class":485},[476,906,908,911,913,916,918],{"class":225,"line":907},31,[476,909,910],{"class":485},"    delay ",[476,912,576],{"class":481},[476,914,915],{"class":485}," random.uniform(",[476,917,178],{"class":582},[476,919,920],{"class":485},", ceiling)\n",[476,922,924,927,929,932,935,938,941],{"class":225,"line":923},32,[476,925,926],{"class":485},"    exc ",[476,928,576],{"class":481},[476,930,931],{"class":485}," retry_state.outcome.exception() ",[476,933,934],{"class":481},"if",[476,936,937],{"class":485}," retry_state.outcome ",[476,939,940],{"class":481},"else",[476,942,943],{"class":582}," None\n",[476,945,947,950,953,956,959],{"class":225,"line":946},33,[476,948,949],{"class":481},"    if",[476,951,952],{"class":582}," isinstance",[476,954,955],{"class":485},"(exc, RetryableStatus) ",[476,957,958],{"class":481},"and",[476,960,961],{"class":485}," exc.retry_after:\n",[476,963,965,968,971],{"class":225,"line":964},34,[476,966,967],{"class":481},"        return",[476,969,970],{"class":582}," max",[476,972,973],{"class":485},"(delay, exc.retry_after)\n",[476,975,977,980],{"class":225,"line":976},35,[476,978,979],{"class":481},"    return",[476,981,982],{"class":485}," delay\n",[476,984,986],{"class":225,"line":985},36,[476,987,567],{"emptyLinePlaceholder":566},[476,989,991],{"class":225,"line":990},37,[476,992,567],{"emptyLinePlaceholder":566},[476,994,996,998,1001,1004,1006,1008,1010],{"class":225,"line":995},38,[476,997,849],{"class":481},[476,999,1000],{"class":726}," parse_retry_after",[476,1002,1003],{"class":485},"(response: httpx.Response) -> ",[476,1005,757],{"class":582},[476,1007,760],{"class":481},[476,1009,763],{"class":582},[476,1011,772],{"class":485},[476,1013,1015,1018,1020,1023,1026],{"class":225,"line":1014},39,[476,1016,1017],{"class":485},"    raw ",[476,1019,576],{"class":481},[476,1021,1022],{"class":485}," response.headers.get(",[476,1024,1025],{"class":654},"\"retry-after\"",[476,1027,560],{"class":485},[476,1029,1031,1034],{"class":225,"line":1030},40,[476,1032,1033],{"class":481},"    match",[476,1035,1036],{"class":485}," raw:\n",[476,1038,1040,1043,1045],{"class":225,"line":1039},41,[476,1041,1042],{"class":481},"        case",[476,1044,763],{"class":582},[476,1046,772],{"class":485},[476,1048,1050,1053],{"class":225,"line":1049},42,[476,1051,1052],{"class":481},"            return",[476,1054,943],{"class":582},[476,1056,1058,1060,1063,1065],{"class":225,"line":1057},43,[476,1059,1042],{"class":481},[476,1061,1062],{"class":485}," value ",[476,1064,934],{"class":481},[476,1066,1067],{"class":485}," value.isdigit():\n",[476,1069,1071,1073,1075],{"class":225,"line":1070},44,[476,1072,1052],{"class":481},[476,1074,648],{"class":582},[476,1076,1077],{"class":485},"(value)\n",[476,1079,1081,1083],{"class":225,"line":1080},45,[476,1082,1042],{"class":481},[476,1084,1085],{"class":485}," _:\n",[476,1087,1089,1091],{"class":225,"line":1088},46,[476,1090,1052],{"class":481},[476,1092,943],{"class":582},[476,1094,1096],{"class":225,"line":1095},47,[476,1097,567],{"emptyLinePlaceholder":566},[476,1099,1101],{"class":225,"line":1100},48,[476,1102,567],{"emptyLinePlaceholder":566},[476,1104,1106,1109,1112,1115,1118,1121,1123,1126],{"class":225,"line":1105},49,[476,1107,1108],{"class":481},"async",[476,1110,1111],{"class":481}," def",[476,1113,1114],{"class":726}," call_upstream",[476,1116,1117],{"class":485},"(client: httpx.AsyncClient, path: ",[476,1119,1120],{"class":582},"str",[476,1122,766],{"class":485},[476,1124,1125],{"class":582},"dict",[476,1127,772],{"class":485},[476,1129,1131,1134,1136],{"class":225,"line":1130},50,[476,1132,1133],{"class":485},"    retryer ",[476,1135,576],{"class":481},[476,1137,1138],{"class":485}," AsyncRetrying(\n",[476,1140,1142,1146,1148],{"class":225,"line":1141},51,[476,1143,1145],{"class":1144},"s4XuR","        retry",[476,1147,576],{"class":481},[476,1149,1150],{"class":485},"retry_if_exception_type((httpx.TransportError, RetryableStatus)),\n",[476,1152,1154,1157,1159],{"class":225,"line":1153},52,[476,1155,1156],{"class":1144},"        wait",[476,1158,576],{"class":481},[476,1160,1161],{"class":485},"full_jitter,\n",[476,1163,1165,1168,1170,1173,1175,1178,1181,1184,1186],{"class":225,"line":1164},53,[476,1166,1167],{"class":1144},"        stop",[476,1169,576],{"class":481},[476,1171,1172],{"class":485},"stop_after_attempt(",[476,1174,192],{"class":582},[476,1176,1177],{"class":485},") ",[476,1179,1180],{"class":481},"|",[476,1182,1183],{"class":485}," stop_after_delay(",[476,1185,691],{"class":582},[476,1187,1188],{"class":485},"),\n",[476,1190,1192,1195,1197,1200],{"class":225,"line":1191},54,[476,1193,1194],{"class":1144},"        reraise",[476,1196,576],{"class":481},[476,1198,1199],{"class":582},"True",[476,1201,1202],{"class":485},",\n",[476,1204,1206],{"class":225,"line":1205},55,[476,1207,1208],{"class":485},"    )\n",[476,1210,1212,1215,1218,1221,1224],{"class":225,"line":1211},56,[476,1213,1214],{"class":481},"    async",[476,1216,1217],{"class":481}," for",[476,1219,1220],{"class":485}," attempt ",[476,1222,1223],{"class":481},"in",[476,1225,1226],{"class":485}," retryer:\n",[476,1228,1230,1233],{"class":225,"line":1229},57,[476,1231,1232],{"class":481},"        with",[476,1234,1235],{"class":485}," attempt:\n",[476,1237,1239,1242,1244,1247],{"class":225,"line":1238},58,[476,1240,1241],{"class":485},"            response ",[476,1243,576],{"class":481},[476,1245,1246],{"class":481}," await",[476,1248,1249],{"class":485}," client.get(path)\n",[476,1251,1253,1256,1259,1261,1264],{"class":225,"line":1252},59,[476,1254,1255],{"class":481},"            if",[476,1257,1258],{"class":485}," response.status_code ",[476,1260,1223],{"class":481},[476,1262,1263],{"class":582}," RETRYABLE_STATUS",[476,1265,772],{"class":485},[476,1267,1269,1272],{"class":225,"line":1268},60,[476,1270,1271],{"class":481},"                raise",[476,1273,1274],{"class":485}," RetryableStatus(\n",[476,1276,1278],{"class":225,"line":1277},61,[476,1279,1280],{"class":485},"                    response.status_code, parse_retry_after(response)\n",[476,1282,1284],{"class":225,"line":1283},62,[476,1285,1286],{"class":485},"                )\n",[476,1288,1290],{"class":225,"line":1289},63,[476,1291,1292],{"class":485},"            response.raise_for_status()\n",[476,1294,1296,1298],{"class":225,"line":1295},64,[476,1297,1052],{"class":481},[476,1299,1300],{"class":485}," response.json()\n",[476,1302,1304,1307,1310,1312],{"class":225,"line":1303},65,[476,1305,1306],{"class":481},"    raise",[476,1308,1309],{"class":485}," RetryError(",[476,1311,769],{"class":582},[476,1313,560],{"class":485},[14,1315,1316,1319],{},[27,1317,1318],{},"stop_after_attempt(5) | stop_after_delay(TOTAL_BUDGET)"," is the important line. Either condition ends the loop, so a slow upstream cannot stretch one call to forty seconds just because five attempts were technically allowed.",[32,1321,1323],{"id":1322},"per-attempt-timeouts-versus-the-total-budget","Per-attempt timeouts versus the total budget",[14,1325,1326],{},"Most retry incidents I have debugged were really timeout incidents. The team set a generous thirty-second client timeout, then wrapped it in three retries, and produced a worst case of ninety-plus seconds per inbound request. Their own web server had a sixty-second worker timeout, so the request died anyway — after occupying a worker for a full minute and holding a database connection the whole time. Under load that pattern eats your process pool and takes down endpoints that have nothing to do with the failing supplier.",[14,1328,1329],{},"Set two limits and treat them as different things. The per-attempt timeout should be tight and split by phase: connect timeouts belong near two seconds because a healthy TCP handshake is milliseconds, while read timeouts should sit just above the upstream's genuine p99. The total budget is the number that protects your service, and it must be smaller than whatever your caller will tolerate.",[157,1331,166,1336,166,1339,166,1342,166,1345,166,1349,166,1353,166,1359,166,1363,166,1368,166,1370,166,1374,166,1377,166,1381,166,1384,166,1387,166,1390,166,1394,166,1397,166,1401,166,1406,166,1410,166,1414],{"viewBox":1332,"role":160,"ariaLabelledBy":1333,"xmlns":164,"style":165},"0 0 720 250",[1334,1335],"tnc-tmo-t","tnc-tmo-d",[168,1337,1338],{"id":1334},"Attempt timeline inside a total budget",[172,1340,1341],{"id":1335},"Three attempts with tight per-attempt read timeouts and jittered waits fit inside a twenty second total budget that cuts the fourth attempt.",[176,1343],{"x":178,"y":178,"width":179,"height":1344,"fill":181},"250",[212,1346,1348],{"x":203,"y":1347,"fill":216,"style":361},"26","Total budget 20s, read timeout 5s per attempt",[176,1350],{"x":203,"y":275,"width":1351,"height":1347,"rx":1352,"fill":208,"stroke":209,"style":210},"672","6",[212,1354,1358],{"x":1355,"y":1356,"fill":216,"style":1357},"352","62","text-anchor:middle;font-size:12;font-family:var(--font-sans);","stop_after_delay(20)",[176,1360],{"x":203,"y":403,"width":1361,"height":1362,"rx":1352,"fill":181,"stroke":235,"style":210},"168","30",[212,1364,1367],{"x":1365,"y":1366,"fill":216,"style":1357},"100","116","attempt 1: 5s",[176,1369],{"x":422,"y":403,"width":1362,"height":1362,"rx":1352,"fill":278},[212,1371,1373],{"x":1372,"y":239,"fill":200,"style":222},"199","0.9s",[176,1375],{"x":1376,"y":403,"width":1361,"height":1362,"rx":1352,"fill":181,"stroke":235,"style":210},"214",[212,1378,1380],{"x":1379,"y":1366,"fill":216,"style":1357},"298","attempt 2: 5s",[176,1382],{"x":1383,"y":403,"width":277,"height":1362,"rx":1352,"fill":278},"382",[212,1385,1386],{"x":605,"y":239,"fill":200,"style":222},"1.6s",[176,1388],{"x":1389,"y":403,"width":1361,"height":1362,"rx":1352,"fill":181,"stroke":235,"style":210},"434",[212,1391,1393],{"x":1392,"y":1366,"fill":216,"style":1357},"518","attempt 3: 5s",[176,1395],{"x":1396,"y":403,"width":407,"height":1362,"rx":1352,"fill":291},"602",[212,1398,1400],{"x":1399,"y":1366,"fill":216,"style":222},"645","cut off",[225,1402],{"x1":1403,"y1":407,"x2":1403,"y2":1404,"stroke":291,"style":1405},"688","176","stroke-width:2;stroke-dasharray:5 4;",[212,1407,1409],{"x":1408,"y":434,"fill":200,"style":222},"640","budget spent",[212,1411,1413],{"x":203,"y":1412,"fill":200,"style":389},"220","Attempt 4 never starts: the deadline, not the attempt counter, ends the call.",[212,1415,1416],{"x":203,"y":265,"fill":200,"style":389},"Caller sees one bounded failure instead of a worker held open for a minute.",[467,1418,1420],{"className":469,"code":1419,"language":471,"meta":472,"style":472},"import os\nimport httpx\n\ndef build_client() -> httpx.AsyncClient:\n    timeout = httpx.Timeout(\n        connect=float(os.getenv(\"HTTP_CONNECT_TIMEOUT\", \"2.0\")),\n        read=float(os.getenv(\"HTTP_READ_TIMEOUT\", \"5.0\")),\n        write=float(os.getenv(\"HTTP_WRITE_TIMEOUT\", \"5.0\")),\n        pool=float(os.getenv(\"HTTP_POOL_TIMEOUT\", \"1.0\")),\n    )\n    limits = httpx.Limits(\n        max_connections=int(os.getenv(\"HTTP_MAX_CONNECTIONS\", \"50\")),\n        max_keepalive_connections=int(os.getenv(\"HTTP_MAX_KEEPALIVE\", \"20\")),\n    )\n    return httpx.AsyncClient(\n        base_url=os.environ[\"UPSTREAM_BASE_URL\"],\n        timeout=timeout,\n        limits=limits,\n        headers={\"Authorization\": f\"Bearer {os.environ['UPSTREAM_API_KEY']}\"},\n    )\n",[27,1421,1422,1428,1434,1438,1448,1458,1480,1501,1521,1542,1546,1556,1577,1597,1601,1608,1624,1634,1644,1681],{"__ignoreMap":472},[476,1423,1424,1426],{"class":225,"line":478},[476,1425,482],{"class":481},[476,1427,494],{"class":485},[476,1429,1430,1432],{"class":225,"line":489},[476,1431,482],{"class":481},[476,1433,510],{"class":485},[476,1435,1436],{"class":225,"line":497},[476,1437,567],{"emptyLinePlaceholder":566},[476,1439,1440,1442,1445],{"class":225,"line":505},[476,1441,849],{"class":481},[476,1443,1444],{"class":726}," build_client",[476,1446,1447],{"class":485},"() -> httpx.AsyncClient:\n",[476,1449,1450,1453,1455],{"class":225,"line":513},[476,1451,1452],{"class":485},"    timeout ",[476,1454,576],{"class":481},[476,1456,1457],{"class":485}," httpx.Timeout(\n",[476,1459,1460,1463,1465,1467,1469,1472,1474,1477],{"class":225,"line":527},[476,1461,1462],{"class":1144},"        connect",[476,1464,576],{"class":481},[476,1466,757],{"class":582},[476,1468,651],{"class":485},[476,1470,1471],{"class":654},"\"HTTP_CONNECT_TIMEOUT\"",[476,1473,608],{"class":485},[476,1475,1476],{"class":654},"\"2.0\"",[476,1478,1479],{"class":485},")),\n",[476,1481,1482,1485,1487,1489,1491,1494,1496,1499],{"class":225,"line":533},[476,1483,1484],{"class":1144},"        read",[476,1486,576],{"class":481},[476,1488,757],{"class":582},[476,1490,651],{"class":485},[476,1492,1493],{"class":654},"\"HTTP_READ_TIMEOUT\"",[476,1495,608],{"class":485},[476,1497,1498],{"class":654},"\"5.0\"",[476,1500,1479],{"class":485},[476,1502,1503,1506,1508,1510,1512,1515,1517,1519],{"class":225,"line":539},[476,1504,1505],{"class":1144},"        write",[476,1507,576],{"class":481},[476,1509,757],{"class":582},[476,1511,651],{"class":485},[476,1513,1514],{"class":654},"\"HTTP_WRITE_TIMEOUT\"",[476,1516,608],{"class":485},[476,1518,1498],{"class":654},[476,1520,1479],{"class":485},[476,1522,1523,1526,1528,1530,1532,1535,1537,1540],{"class":225,"line":545},[476,1524,1525],{"class":1144},"        pool",[476,1527,576],{"class":481},[476,1529,757],{"class":582},[476,1531,651],{"class":485},[476,1533,1534],{"class":654},"\"HTTP_POOL_TIMEOUT\"",[476,1536,608],{"class":485},[476,1538,1539],{"class":654},"\"1.0\"",[476,1541,1479],{"class":485},[476,1543,1544],{"class":225,"line":551},[476,1545,1208],{"class":485},[476,1547,1548,1551,1553],{"class":225,"line":557},[476,1549,1550],{"class":485},"    limits ",[476,1552,576],{"class":481},[476,1554,1555],{"class":485}," httpx.Limits(\n",[476,1557,1558,1561,1563,1565,1567,1570,1572,1575],{"class":225,"line":563},[476,1559,1560],{"class":1144},"        max_connections",[476,1562,576],{"class":481},[476,1564,751],{"class":582},[476,1566,651],{"class":485},[476,1568,1569],{"class":654},"\"HTTP_MAX_CONNECTIONS\"",[476,1571,608],{"class":485},[476,1573,1574],{"class":654},"\"50\"",[476,1576,1479],{"class":485},[476,1578,1579,1582,1584,1586,1588,1591,1593,1595],{"class":225,"line":570},[476,1580,1581],{"class":1144},"        max_keepalive_connections",[476,1583,576],{"class":481},[476,1585,751],{"class":582},[476,1587,651],{"class":485},[476,1589,1590],{"class":654},"\"HTTP_MAX_KEEPALIVE\"",[476,1592,608],{"class":485},[476,1594,705],{"class":654},[476,1596,1479],{"class":485},[476,1598,1599],{"class":225,"line":588},[476,1600,1208],{"class":485},[476,1602,1603,1605],{"class":225,"line":593},[476,1604,979],{"class":481},[476,1606,1607],{"class":485}," httpx.AsyncClient(\n",[476,1609,1610,1613,1615,1618,1621],{"class":225,"line":640},[476,1611,1612],{"class":1144},"        base_url",[476,1614,576],{"class":481},[476,1616,1617],{"class":485},"os.environ[",[476,1619,1620],{"class":654},"\"UPSTREAM_BASE_URL\"",[476,1622,1623],{"class":485},"],\n",[476,1625,1626,1629,1631],{"class":225,"line":666},[476,1627,1628],{"class":1144},"        timeout",[476,1630,576],{"class":481},[476,1632,1633],{"class":485},"timeout,\n",[476,1635,1636,1639,1641],{"class":225,"line":688},[476,1637,1638],{"class":1144},"        limits",[476,1640,576],{"class":481},[476,1642,1643],{"class":485},"limits,\n",[476,1645,1646,1649,1651,1653,1656,1659,1661,1664,1666,1668,1671,1674,1676,1678],{"class":225,"line":710},[476,1647,1648],{"class":1144},"        headers",[476,1650,576],{"class":481},[476,1652,795],{"class":485},[476,1654,1655],{"class":654},"\"Authorization\"",[476,1657,1658],{"class":485},": ",[476,1660,789],{"class":481},[476,1662,1663],{"class":654},"\"Bearer ",[476,1665,795],{"class":582},[476,1667,1617],{"class":485},[476,1669,1670],{"class":654},"'UPSTREAM_API_KEY'",[476,1672,1673],{"class":485},"]",[476,1675,801],{"class":582},[476,1677,804],{"class":654},[476,1679,1680],{"class":485},"},\n",[476,1682,1683],{"class":225,"line":715},[476,1684,1208],{"class":485},[14,1686,1687,1688,1691],{},"Note the ",[27,1689,1690],{},"pool"," timeout. When retries pile up, every waiting attempt holds a connection slot; without a pool timeout your coroutines block invisibly on connection acquisition and the symptom looks like an upstream problem when it is yours.",[32,1693,1695],{"id":1694},"idempotency-keys-make-writes-retryable","Idempotency keys make writes retryable",[14,1697,1698,1699,1702],{},"Once retries are safe for reads, the remaining risk is writes. If you ",[27,1700,1701],{},"POST"," a charge and the read times out, you do not know whether the charge happened. Both choices are bad: retry and risk double-charging, or give up and risk a silent success your database never recorded.",[14,1704,1705,1706,1710],{},"An idempotency key removes the guess. Generate a stable key derived from the business event — not from the attempt — send it on every attempt, and the upstream returns the original result instead of performing the work twice. Stripe, and most payment or messaging APIs worth integrating, support this header. The mirror image of the pattern, applied to traffic you receive, is covered in ",[18,1707,1709],{"href":1708},"\u002Fautomating-side-hustle-operations-with-apis\u002Fprocessing-webhooks-with-python\u002Fbuilding-an-idempotent-webhook-receiver\u002F","building an idempotent webhook receiver",".",[467,1712,1714],{"className":469,"code":1713,"language":471,"meta":472,"style":472},"import hashlib\nimport json\nimport os\nimport httpx\n\nIDEMPOTENCY_HEADER = os.getenv(\"IDEMPOTENCY_HEADER\", \"Idempotency-Key\")\n\n\ndef idempotency_key(event_id: str, payload: dict) -> str:\n    \"\"\"Stable across retries, unique per business event.\"\"\"\n    body = json.dumps(payload, sort_keys=True, separators=(\",\", \":\"))\n    digest = hashlib.sha256(f\"{event_id}:{body}\".encode()).hexdigest()\n    return digest[:40]\n\n\nasync def create_charge(\n    client: httpx.AsyncClient, event_id: str, payload: dict\n) -> dict:\n    response = await client.post(\n        \"\u002Fcharges\",\n        json=payload,\n        headers={IDEMPOTENCY_HEADER: idempotency_key(event_id, payload)},\n    )\n    response.raise_for_status()\n    return response.json()\n",[27,1715,1716,1723,1730,1736,1742,1746,1766,1770,1774,1797,1802,1838,1874,1887,1891,1895,1907,1919,1927,1939,1946,1956,1969,1973,1978],{"__ignoreMap":472},[476,1717,1718,1720],{"class":225,"line":478},[476,1719,482],{"class":481},[476,1721,1722],{"class":485}," hashlib\n",[476,1724,1725,1727],{"class":225,"line":489},[476,1726,482],{"class":481},[476,1728,1729],{"class":485}," json\n",[476,1731,1732,1734],{"class":225,"line":497},[476,1733,482],{"class":481},[476,1735,494],{"class":485},[476,1737,1738,1740],{"class":225,"line":505},[476,1739,482],{"class":481},[476,1741,510],{"class":485},[476,1743,1744],{"class":225,"line":513},[476,1745,567],{"emptyLinePlaceholder":566},[476,1747,1748,1751,1753,1756,1759,1761,1764],{"class":225,"line":527},[476,1749,1750],{"class":582},"IDEMPOTENCY_HEADER",[476,1752,599],{"class":481},[476,1754,1755],{"class":485}," os.getenv(",[476,1757,1758],{"class":654},"\"IDEMPOTENCY_HEADER\"",[476,1760,608],{"class":485},[476,1762,1763],{"class":654},"\"Idempotency-Key\"",[476,1765,560],{"class":485},[476,1767,1768],{"class":225,"line":533},[476,1769,567],{"emptyLinePlaceholder":566},[476,1771,1772],{"class":225,"line":539},[476,1773,567],{"emptyLinePlaceholder":566},[476,1775,1776,1778,1781,1784,1786,1789,1791,1793,1795],{"class":225,"line":545},[476,1777,849],{"class":481},[476,1779,1780],{"class":726}," idempotency_key",[476,1782,1783],{"class":485},"(event_id: ",[476,1785,1120],{"class":582},[476,1787,1788],{"class":485},", payload: ",[476,1790,1125],{"class":582},[476,1792,766],{"class":485},[476,1794,1120],{"class":582},[476,1796,772],{"class":485},[476,1798,1799],{"class":225,"line":551},[476,1800,1801],{"class":654},"    \"\"\"Stable across retries, unique per business event.\"\"\"\n",[476,1803,1804,1807,1809,1812,1815,1817,1819,1821,1824,1826,1828,1831,1833,1836],{"class":225,"line":557},[476,1805,1806],{"class":485},"    body ",[476,1808,576],{"class":481},[476,1810,1811],{"class":485}," json.dumps(payload, ",[476,1813,1814],{"class":1144},"sort_keys",[476,1816,576],{"class":481},[476,1818,1199],{"class":582},[476,1820,608],{"class":485},[476,1822,1823],{"class":1144},"separators",[476,1825,576],{"class":481},[476,1827,730],{"class":485},[476,1829,1830],{"class":654},"\",\"",[476,1832,608],{"class":485},[476,1834,1835],{"class":654},"\":\"",[476,1837,663],{"class":485},[476,1839,1840,1843,1845,1848,1850,1852,1854,1857,1859,1862,1864,1867,1869,1871],{"class":225,"line":563},[476,1841,1842],{"class":485},"    digest ",[476,1844,576],{"class":481},[476,1846,1847],{"class":485}," hashlib.sha256(",[476,1849,789],{"class":481},[476,1851,804],{"class":654},[476,1853,795],{"class":582},[476,1855,1856],{"class":485},"event_id",[476,1858,801],{"class":582},[476,1860,1861],{"class":654},":",[476,1863,795],{"class":582},[476,1865,1866],{"class":485},"body",[476,1868,801],{"class":582},[476,1870,804],{"class":654},[476,1872,1873],{"class":485},".encode()).hexdigest()\n",[476,1875,1876,1878,1881,1884],{"class":225,"line":570},[476,1877,979],{"class":481},[476,1879,1880],{"class":485}," digest[:",[476,1882,1883],{"class":582},"40",[476,1885,1886],{"class":485},"]\n",[476,1888,1889],{"class":225,"line":588},[476,1890,567],{"emptyLinePlaceholder":566},[476,1892,1893],{"class":225,"line":593},[476,1894,567],{"emptyLinePlaceholder":566},[476,1896,1897,1899,1901,1904],{"class":225,"line":640},[476,1898,1108],{"class":481},[476,1900,1111],{"class":481},[476,1902,1903],{"class":726}," create_charge",[476,1905,1906],{"class":485},"(\n",[476,1908,1909,1912,1914,1916],{"class":225,"line":666},[476,1910,1911],{"class":485},"    client: httpx.AsyncClient, event_id: ",[476,1913,1120],{"class":582},[476,1915,1788],{"class":485},[476,1917,1918],{"class":582},"dict\n",[476,1920,1921,1923,1925],{"class":225,"line":688},[476,1922,766],{"class":485},[476,1924,1125],{"class":582},[476,1926,772],{"class":485},[476,1928,1929,1932,1934,1936],{"class":225,"line":710},[476,1930,1931],{"class":485},"    response ",[476,1933,576],{"class":481},[476,1935,1246],{"class":481},[476,1937,1938],{"class":485}," client.post(\n",[476,1940,1941,1944],{"class":225,"line":715},[476,1942,1943],{"class":654},"        \"\u002Fcharges\"",[476,1945,1202],{"class":485},[476,1947,1948,1951,1953],{"class":225,"line":720},[476,1949,1950],{"class":1144},"        json",[476,1952,576],{"class":481},[476,1954,1955],{"class":485},"payload,\n",[476,1957,1958,1960,1962,1964,1966],{"class":225,"line":739},[476,1959,1648],{"class":1144},[476,1961,576],{"class":481},[476,1963,795],{"class":485},[476,1965,1750],{"class":582},[476,1967,1968],{"class":485},": idempotency_key(event_id, payload)},\n",[476,1970,1971],{"class":225,"line":775},[476,1972,1208],{"class":485},[476,1974,1975],{"class":225,"line":809},[476,1976,1977],{"class":485},"    response.raise_for_status()\n",[476,1979,1980,1982],{"class":225,"line":823},[476,1981,979],{"class":481},[476,1983,1300],{"class":485},[14,1985,1986],{},"Compute the key once, at the top of the operation, and pass it down. If you generate it inside the retry wrapper you have built an expensive random string generator that protects nothing.",[32,1988,1990],{"id":1989},"circuit-breaking-the-cap-on-your-outage-bill","Circuit breaking: the cap on your outage bill",[14,1992,1993,1994,1710],{},"Retries assume the failure is transient. When it is not — a supplier is down for twenty minutes — retries become a fixed multiplier on wasted spend and latency. A breaker sits in front of the retry and short-circuits calls once the failure rate crosses a threshold, so a sustained outage costs you one probe request per cooldown window rather than five attempts per user request. At 200 requests per minute with five attempts each, twenty minutes of blind retrying is 20,000 wasted calls; a breaker with a thirty-second cooldown makes it forty. If those calls are metered, the difference lands straight on your invoice, which is exactly the arithmetic in ",[18,1995,1997],{"href":1996},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request",[157,1999,166,2003,166,2006,166,2009,166,2011,166,2018,166,2022,166,2027,166,2030,166,2033,166,2036,166,2039,166,2042,166,2045,166,2048,166,2052,166,2056,166,2060,166,2064,166,2068,166,2071,166,2074],{"viewBox":1332,"role":160,"ariaLabelledBy":2000,"xmlns":164,"style":165},[2001,2002],"tnc-cb-t","tnc-cb-d",[168,2004,2005],{"id":2001},"Circuit breaker state machine",[172,2007,2008],{"id":2002},"Closed opens after a failure threshold, opens reject calls until a cooldown, half open probes once and returns to closed or open.",[176,2010],{"x":178,"y":178,"width":179,"height":1344,"fill":181},[183,2012,185,2013,166],{},[187,2014,195,2016,185],{"id":2015,"viewBox":190,"refX":191,"refY":192,"markerWidth":193,"markerHeight":193,"orient":194},"tnc-cb-arrow",[197,2017],{"d":199,"fill":200},[2019,2020],"circle",{"cx":365,"cy":365,"r":1356,"fill":181,"stroke":278,"style":2021},"stroke-width:3;",[212,2023,2026],{"x":365,"y":2024,"fill":216,"style":2025},"106","text-anchor:middle;font-size:14;font-family:var(--font-sans);","CLOSED",[212,2028,2029],{"x":365,"y":204,"fill":200,"style":222},"calls pass",[2019,2031],{"cx":2032,"cy":365,"r":1356,"fill":181,"stroke":291,"style":2021},"360",[212,2034,2035],{"x":2032,"y":2024,"fill":216,"style":2025},"OPEN",[212,2037,2038],{"x":2032,"y":204,"fill":200,"style":222},"reject at once",[2019,2040],{"cx":2041,"cy":365,"r":1356,"fill":181,"stroke":303,"style":2021},"610",[212,2043,2044],{"x":2041,"y":2024,"fill":216,"style":2025},"HALF-OPEN",[212,2046,2047],{"x":2041,"y":204,"fill":200,"style":222},"one probe",[225,2049],{"x1":2050,"y1":403,"x2":1379,"y2":403,"stroke":200,"style":2051},"172","stroke-width:2;marker-end:url(#tnc-cb-arrow);",[212,2053,2055],{"x":2054,"y":407,"fill":216,"style":222},"235","5 fails in 30s",[225,2057],{"x1":2058,"y1":365,"x2":2059,"y2":365,"stroke":200,"style":2051},"422","548",[212,2061,2063],{"x":2062,"y":1365,"fill":216,"style":222},"485","cooldown 30s",[197,2065],{"d":2066,"fill":2067,"stroke":200,"style":2051},"M610 172 L610 214 L110 214 L110 172","none",[212,2069,2070],{"x":2032,"y":369,"fill":216,"style":222},"probe succeeded",[197,2072],{"d":2073,"fill":2067,"stroke":200,"style":2051},"M610 48 L610 24 L360 24 L360 48",[212,2075,2076],{"x":2062,"y":394,"fill":216,"style":222},"probe failed",[467,2078,2080],{"className":469,"code":2079,"language":471,"meta":472,"style":472},"import os\nimport time\nfrom dataclasses import dataclass, field\n\n\nclass BreakerOpen(Exception):\n    \"\"\"Raised without touching the network. Never retry this.\"\"\"\n\n\n@dataclass\nclass CircuitBreaker:\n    threshold: int = int(os.getenv(\"BREAKER_THRESHOLD\", \"5\"))\n    cooldown: float = float(os.getenv(\"BREAKER_COOLDOWN\", \"30\"))\n    failures: int = 0\n    opened_at: float | None = field(default=None)\n\n    def before_call(self) -> None:\n        if self.opened_at is None:\n            return\n        if time.monotonic() - self.opened_at \u003C self.cooldown:\n            raise BreakerOpen(\"upstream circuit is open\")\n        self.opened_at = None  # half-open: allow one probe through\n\n    def record(self, ok: bool) -> None:\n        if ok:\n            self.failures = 0\n            self.opened_at = None\n        else:\n            self.failures += 1\n            if self.failures >= self.threshold:\n                self.opened_at = time.monotonic()\n",[27,2081,2082,2088,2095,2107,2111,2115,2128,2133,2137,2141,2146,2155,2179,2202,2214,2239,2243,2257,2275,2280,2301,2314,2328,2332,2351,2358,2370,2380,2387,2399,2415],{"__ignoreMap":472},[476,2083,2084,2086],{"class":225,"line":478},[476,2085,482],{"class":481},[476,2087,494],{"class":485},[476,2089,2090,2092],{"class":225,"line":489},[476,2091,482],{"class":481},[476,2093,2094],{"class":485}," time\n",[476,2096,2097,2099,2102,2104],{"class":225,"line":497},[476,2098,516],{"class":481},[476,2100,2101],{"class":485}," dataclasses ",[476,2103,482],{"class":481},[476,2105,2106],{"class":485}," dataclass, field\n",[476,2108,2109],{"class":225,"line":505},[476,2110,567],{"emptyLinePlaceholder":566},[476,2112,2113],{"class":225,"line":513},[476,2114,567],{"emptyLinePlaceholder":566},[476,2116,2117,2119,2122,2124,2126],{"class":225,"line":527},[476,2118,723],{"class":481},[476,2120,2121],{"class":726}," BreakerOpen",[476,2123,730],{"class":485},[476,2125,733],{"class":582},[476,2127,736],{"class":485},[476,2129,2130],{"class":225,"line":533},[476,2131,2132],{"class":654},"    \"\"\"Raised without touching the network. Never retry this.\"\"\"\n",[476,2134,2135],{"class":225,"line":539},[476,2136,567],{"emptyLinePlaceholder":566},[476,2138,2139],{"class":225,"line":545},[476,2140,567],{"emptyLinePlaceholder":566},[476,2142,2143],{"class":225,"line":551},[476,2144,2145],{"class":726},"@dataclass\n",[476,2147,2148,2150,2153],{"class":225,"line":557},[476,2149,723],{"class":481},[476,2151,2152],{"class":726}," CircuitBreaker",[476,2154,772],{"class":485},[476,2156,2157,2160,2162,2164,2167,2169,2172,2174,2177],{"class":225,"line":563},[476,2158,2159],{"class":485},"    threshold: ",[476,2161,751],{"class":582},[476,2163,599],{"class":481},[476,2165,2166],{"class":582}," int",[476,2168,651],{"class":485},[476,2170,2171],{"class":654},"\"BREAKER_THRESHOLD\"",[476,2173,608],{"class":485},[476,2175,2176],{"class":654},"\"5\"",[476,2178,663],{"class":485},[476,2180,2181,2184,2186,2188,2190,2192,2195,2197,2200],{"class":225,"line":570},[476,2182,2183],{"class":485},"    cooldown: ",[476,2185,757],{"class":582},[476,2187,599],{"class":481},[476,2189,648],{"class":582},[476,2191,651],{"class":485},[476,2193,2194],{"class":654},"\"BREAKER_COOLDOWN\"",[476,2196,608],{"class":485},[476,2198,2199],{"class":654},"\"30\"",[476,2201,663],{"class":485},[476,2203,2204,2207,2209,2211],{"class":225,"line":588},[476,2205,2206],{"class":485},"    failures: ",[476,2208,751],{"class":582},[476,2210,599],{"class":481},[476,2212,2213],{"class":582}," 0\n",[476,2215,2216,2219,2221,2223,2225,2227,2230,2233,2235,2237],{"class":225,"line":593},[476,2217,2218],{"class":485},"    opened_at: ",[476,2220,757],{"class":582},[476,2222,760],{"class":481},[476,2224,763],{"class":582},[476,2226,599],{"class":481},[476,2228,2229],{"class":485}," field(",[476,2231,2232],{"class":1144},"default",[476,2234,576],{"class":481},[476,2236,769],{"class":582},[476,2238,560],{"class":485},[476,2240,2241],{"class":225,"line":640},[476,2242,567],{"emptyLinePlaceholder":566},[476,2244,2245,2247,2250,2253,2255],{"class":225,"line":666},[476,2246,742],{"class":481},[476,2248,2249],{"class":726}," before_call",[476,2251,2252],{"class":485},"(self) -> ",[476,2254,769],{"class":582},[476,2256,772],{"class":485},[476,2258,2259,2262,2265,2268,2271,2273],{"class":225,"line":688},[476,2260,2261],{"class":481},"        if",[476,2263,2264],{"class":582}," self",[476,2266,2267],{"class":485},".opened_at ",[476,2269,2270],{"class":481},"is",[476,2272,763],{"class":582},[476,2274,772],{"class":485},[476,2276,2277],{"class":225,"line":710},[476,2278,2279],{"class":481},"            return\n",[476,2281,2282,2284,2287,2289,2291,2293,2296,2298],{"class":225,"line":715},[476,2283,2261],{"class":481},[476,2285,2286],{"class":485}," time.monotonic() ",[476,2288,899],{"class":481},[476,2290,2264],{"class":582},[476,2292,2267],{"class":485},[476,2294,2295],{"class":481},"\u003C",[476,2297,2264],{"class":582},[476,2299,2300],{"class":485},".cooldown:\n",[476,2302,2303,2306,2309,2312],{"class":225,"line":720},[476,2304,2305],{"class":481},"            raise",[476,2307,2308],{"class":485}," BreakerOpen(",[476,2310,2311],{"class":654},"\"upstream circuit is open\"",[476,2313,560],{"class":485},[476,2315,2316,2318,2320,2322,2324],{"class":225,"line":739},[476,2317,812],{"class":582},[476,2319,2267],{"class":485},[476,2321,576],{"class":481},[476,2323,763],{"class":582},[476,2325,2327],{"class":2326},"sJ8bj","  # half-open: allow one probe through\n",[476,2329,2330],{"class":225,"line":775},[476,2331,567],{"emptyLinePlaceholder":566},[476,2333,2334,2336,2339,2342,2345,2347,2349],{"class":225,"line":809},[476,2335,742],{"class":481},[476,2337,2338],{"class":726}," record",[476,2340,2341],{"class":485},"(self, ok: ",[476,2343,2344],{"class":582},"bool",[476,2346,766],{"class":485},[476,2348,769],{"class":582},[476,2350,772],{"class":485},[476,2352,2353,2355],{"class":225,"line":823},[476,2354,2261],{"class":481},[476,2356,2357],{"class":485}," ok:\n",[476,2359,2360,2363,2366,2368],{"class":225,"line":836},[476,2361,2362],{"class":582},"            self",[476,2364,2365],{"class":485},".failures ",[476,2367,576],{"class":481},[476,2369,2213],{"class":582},[476,2371,2372,2374,2376,2378],{"class":225,"line":841},[476,2373,2362],{"class":582},[476,2375,2267],{"class":485},[476,2377,576],{"class":481},[476,2379,943],{"class":582},[476,2381,2382,2385],{"class":225,"line":846},[476,2383,2384],{"class":481},"        else",[476,2386,772],{"class":485},[476,2388,2389,2391,2393,2396],{"class":225,"line":862},[476,2390,2362],{"class":582},[476,2392,2365],{"class":485},[476,2394,2395],{"class":481},"+=",[476,2397,2398],{"class":582}," 1\n",[476,2400,2401,2403,2405,2407,2410,2412],{"class":225,"line":868},[476,2402,1255],{"class":481},[476,2404,2264],{"class":582},[476,2406,2365],{"class":485},[476,2408,2409],{"class":481},">=",[476,2411,2264],{"class":582},[476,2413,2414],{"class":485},".threshold:\n",[476,2416,2417,2420,2422,2424],{"class":225,"line":907},[476,2418,2419],{"class":582},"                self",[476,2421,2267],{"class":485},[476,2423,576],{"class":481},[476,2425,2426],{"class":485}," time.monotonic()\n",[14,2428,2429,2430,2433,2434,2437,2438,2441,2442,2445],{},"Wire ",[27,2431,2432],{},"before_call()"," at the top of your client method and ",[27,2435,2436],{},"record()"," after it, outside the tenacity loop. ",[27,2439,2440],{},"BreakerOpen"," must not be in your ",[27,2443,2444],{},"retry_if_exception_type"," list — a breaker you retry through is just a slower failure.",[32,2447,2449],{"id":2448},"when-tenacity-is-the-right-tool-and-when-it-is-not","When tenacity is the right tool, and when it is not",[47,2451,2452,2465],{},[50,2453,2454],{},[53,2455,2456,2459,2462],{},[56,2457,2458],{},"Option",[56,2460,2461],{},"Best for",[56,2463,2464],{},"Weakness",[66,2466,2467,2477,2491,2505],{},[53,2468,2469,2471,2474],{},[71,2470,29],{},[71,2472,2473],{},"Any policy with real rules",[71,2475,2476],{},"One more dependency",[53,2478,2479,2485,2488],{},[71,2480,2481,2482],{},"httpx ",[27,2483,2484],{},"transport retries",[71,2486,2487],{},"Connect errors only",[71,2489,2490],{},"Ignores status codes",[53,2492,2493,2499,2502],{},[71,2494,2495,2496],{},"urllib3 ",[27,2497,2498],{},"Retry",[71,2500,2501],{},"Sync requests sessions",[71,2503,2504],{},"Awkward async story",[53,2506,2507,2510,2513],{},[71,2508,2509],{},"Hand-rolled loop",[71,2511,2512],{},"A single throwaway script",[71,2514,2515],{},"Untestable, no deadline",[14,2517,2518,2519,2523,2524,2527,2528,2532],{},"Reach for tenacity as soon as the call is customer-facing or metered. Its value is that policy lives in one declarative object you can unit-test with ",[18,2520,2522],{"href":2521},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002Fmocking-external-apis-with-respx\u002F","respx mocking"," rather than scattered ",[27,2525,2526],{},"time.sleep()"," calls nobody dares touch. Skip it when retrying cannot help: a long-running job that already runs on a queue should let ",[18,2529,2531],{"href":2530},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","Celery"," handle redelivery, because an in-process retry there just holds a worker hostage. Skip it too on hot paths where the caller's own deadline is shorter than one backoff step — a 200ms budget has no room for a retry, so fail fast and serve a cached value instead.",[32,2534,2536],{"id":2535},"migrating-off-a-hand-rolled-retry-loop","Migrating off a hand-rolled retry loop",[14,2538,2539,2540,2542,2543,2545,2546,2549,2550,2553,2554,2557,2558,2561],{},"Five steps, and you can do them in one afternoon. First, add ",[27,2541,29],{}," and ",[27,2544,456],{}," and pull every retry constant into environment variables so staging and production can differ. Second, wrap your existing client call in ",[27,2547,2548],{},"AsyncRetrying"," with ",[27,2551,2552],{},"reraise=True"," and no retry predicate — behaviour is unchanged, which makes the diff safe to merge. Third, narrow the predicate to transport errors plus your status allowlist, and delete the old ",[27,2555,2556],{},"except Exception: continue",". Fourth, replace the attempt counter with ",[27,2559,2560],{},"stop_after_attempt(n) | stop_after_delay(budget)"," and tighten the per-attempt timeouts underneath it. Fifth, add the idempotency key to every mutating call and only then enable retries on those paths.",[14,2563,2564,2565,2568,2569,2573,2574,2578],{},"Instrument as you go. Emit ",[27,2566,2567],{},"attempt_number",", the exception class, and the chosen sleep on every retry through ",[18,2570,2572],{"href":2571},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002F","structured logging",", and put a counter on retries-per-successful-call. If that ratio drifts above roughly 0.1 you are not resilient, you are papering over an upstream problem — or over your own quota, in which case ",[18,2575,2577],{"href":2576},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fdebugging-429-too-many-requests-errors\u002F","debugging 429 errors"," is the more useful page.",[32,2580,2582],{"id":2581},"builder-verdict","Builder verdict",[14,2584,2585,2586,2590],{},"tenacity wins, and it is not close. The alternatives either cover a fraction of the problem or hand you a loop you will be too scared to modify six months from now. What actually matters is the shape of the policy, not the library: retry a narrow allowlist, wait with full jitter, bound the whole operation with a deadline instead of a count, make writes idempotent before you let them retry, and put a breaker in front so a supplier's bad day does not multiply your bill. That configuration is roughly forty lines and it converts the most common cause of paging incidents in small API businesses — a dependency wobbles, retries amplify it, everything falls over — into a bounded, observable, cheap failure. If you are integrating a metered upstream such as an LLM provider, pair it with the spend controls in ",[18,2587,2589],{"href":2588},"\u002Fautomating-side-hustle-operations-with-apis\u002Fautomating-ai-workflows-with-python-apis\u002Fcontrolling-llm-api-costs-in-production\u002F","controlling LLM API costs in production","; retries are a cost multiplier and deserve a budget line of their own.",[32,2592,2594],{"id":2593},"faq","FAQ",[14,2596,2597,2601],{},[2598,2599,2600],"strong",{},"How much does retrying add to my upstream bill?","\nModel it as your failure rate times your average attempt count. At a 1% failure rate with three attempts you pay for about 2% more calls, which is noise. At a 5% failure rate the same policy costs 10% more, and if the upstream charges per call that is a real margin hit — which is the point at which a circuit breaker stops being optional.",[14,2603,2604,2607],{},[2598,2605,2606],{},"Should I retry inside my API handler or push the work to a queue?","\nIf the caller is waiting on the response and your total budget fits inside their timeout, retry inline. If the operation can complete later, enqueue it: a background worker can afford minutes of backoff without holding a web worker, a database connection, and a customer's browser tab open.",[14,2609,2610,2613,2614,2616,2617,2620,2621,2623],{},[2598,2611,2612],{},"Does a retry policy break when I rotate API keys?","\nIt can, badly. A stale key produces ",[27,2615,149],{}," or ",[27,2618,2619],{},"403",", and if those are in your retry set you will hammer the upstream with doomed calls and possibly get the whole key blocked. Keep ",[27,2622,261],{}," auth errors out of the retry predicate and let them fail loudly during a rotation.",[14,2625,2626,2629],{},[2598,2627,2628],{},"What is the migration risk of adding tenacity to a live service?","\nLow if you stage it. Ship the wrapper with the old behaviour first, then narrow the predicate, then add the deadline. The one genuinely risky step is enabling retries on writes, so do that only after idempotency keys are live and verified against the upstream's test environment.",[14,2631,2632,2635],{},[2598,2633,2634],{},"How do I stop retries hiding a real outage from my alerts?","\nAlert on the retry rate, not just on final errors. A service that succeeds after four attempts is failing — it is just failing quietly and burning latency budget. Track retries per successful call and page when it crosses your threshold, before customers notice the slowdown.",[32,2637,2639],{"id":2638},"related","Related",[2641,2642,2643,2649,2655,2661,2667],"ul",{},[2644,2645,2646,2648],"li",{},[18,2647,21],{"href":20}," — the parent guide covering sessions, headers and connection reuse.",[2644,2650,2651,2654],{},[18,2652,2653],{"href":2576},"Debugging 429 Too Many Requests Errors"," — read this when your retries are the cause rather than the cure.",[2644,2656,2657,2660],{},[18,2658,2659],{"href":460},"httpx vs requests for Async"," — why the client in these snippets is async and what it costs to switch.",[2644,2662,2663,2666],{},[18,2664,2665],{"href":1708},"Building an Idempotent Webhook Receiver"," — the same idempotency contract applied to traffic you receive.",[2644,2668,2669,2672],{},[18,2670,2671],{"href":2521},"Mocking External APIs with respx"," — how to unit-test a retry policy without waiting on real backoff.",[2674,2675,2676],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":472,"searchDepth":489,"depth":489,"links":2678},[2679,2680,2681,2682,2683,2684,2685,2686,2687,2688],{"id":34,"depth":489,"text":35},{"id":318,"depth":489,"text":319},{"id":1322,"depth":489,"text":1323},{"id":1694,"depth":489,"text":1695},{"id":1989,"depth":489,"text":1990},{"id":2448,"depth":489,"text":2449},{"id":2535,"depth":489,"text":2536},{"id":2581,"depth":489,"text":2582},{"id":2593,"depth":489,"text":2594},{"id":2638,"depth":489,"text":2639},"Build a tenacity retry policy that survives an outage instead of amplifying it: status codes, jittered backoff, timeout budgets, idempotency keys, breakers.","md",{"pageTitle":2692,"type":2693,"datePublished":2694,"dateModified":2694},"tenacity Retry Patterns for Failed HTTP Requests in Python","article","2026-07-23","\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fretrying-failed-http-requests-with-tenacity",{"title":5,"description":2689},"getting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fretrying-failed-http-requests-with-tenacity\u002Findex","x8TK5gaihNVxLTdanh9xki42oh3PfMwguy9CruN3PZE",{"@context":2700,"@type":2701,"mainEntity":2702},"https:\u002F\u002Fschema.org","FAQPage",[2703,2708,2711,2714,2717],{"@type":2704,"name":2600,"acceptedAnswer":2705},"Question",{"@type":2706,"text":2707},"Answer","Model it as your failure rate times your average attempt count. At a 1% failure rate with three attempts you pay for about 2% more calls, which is noise. At a 5% failure rate the same policy costs 10% more, and if the upstream charges per call that is a real margin hit — which is the point at which a circuit breaker stops being optional.",{"@type":2704,"name":2606,"acceptedAnswer":2709},{"@type":2706,"text":2710},"If the caller is waiting on the response and your total budget fits inside their timeout, retry inline. If the operation can complete later, enqueue it: a background worker can afford minutes of backoff without holding a web worker, a database connection, and a customer's browser tab open.",{"@type":2704,"name":2612,"acceptedAnswer":2712},{"@type":2706,"text":2713},"It can, badly. A stale key produces 401 or 403, and if those are in your retry set you will hammer the upstream with doomed calls and possibly get the whole key blocked. Keep 4xx auth errors out of the retry predicate and let them fail loudly during a rotation.",{"@type":2704,"name":2628,"acceptedAnswer":2715},{"@type":2706,"text":2716},"Low if you stage it. Ship the wrapper with the old behaviour first, then narrow the predicate, then add the deadline. The one genuinely risky step is enabling retries on writes, so do that only after idempotency keys are live and verified against the upstream's test environment.",{"@type":2704,"name":2634,"acceptedAnswer":2718},{"@type":2706,"text":2719},"Alert on the retry rate, not just on final errors. A service that succeeds after four attempts is failing — it is just failing quietly and burning latency budget. Track retries per successful call and page when it crosses your threshold, before customers notice the slowdown.",1784887028359]