[{"data":1,"prerenderedAt":2033},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python":3,"surround:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python":2026},{"id":4,"title":5,"body":6,"dateModified":1997,"datePublished":1997,"description":1998,"extension":1999,"faq":2000,"meta":2009,"navigation":248,"path":2019,"seo":2020,"slug":2022,"stem":2023,"type":2024,"__hash__":2025},"docs\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002Findex.md","Retry a Failed Excel Report Job in Python",{"type":7,"value":8,"toc":1982},"minimark",[9,19,144,149,177,195,199,202,666,676,693,697,700,890,893,1007,1011,1022,1192,1199,1203,1206,1443,1446,1450,1453,1640,1643,1647,1650,1758,1765,1769,1879,1883,1886,1890,1893,1897,1900,1904,1911,1917,1923,1929,1933,1936,1945,1948,1978],[10,11,12,13,18],"p",{},"Retries turn a class of failures into a non-event: the share blinked, the file was open for thirty seconds, the mail server was busy. Applied indiscriminately they do the opposite, hiding real problems behind four identical failures and a delayed alert. This guide, part of ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",", covers what to retry, how to wait, and where the retry boundary belongs.",[20,21,29,30,29,34,29,38,29,45,29,55,29,62,29,66,29,75,29,80,29,86,29,91,29,95,29,100,29,107,29,113,29,118,29,122,29,126,29,130,29,135,29,138,29,141],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 740 240","img",[25,26],"rt-class-t","rt-class-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"Deciding whether an error is worth retrying",[35,36,37],"desc",{"id":26},"Ask whether the same call could succeed unchanged in ten seconds. If yes — a lock, a timeout, a temporary network failure — retry with backoff. If no — a missing column, a validation failure, bad credentials — stop and report, because every attempt will fail identically.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","740","240","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53,"style":54},"210","20","320","52","12","#ebebfd","var(--brand,#5b5cf0)","stroke-width:2px",[56,57,61],"text",{"x":58,"y":59,"style":60},"370","43","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Could the identical call succeed",[56,63,65],{"x":58,"y":64,"style":60},"62","in ten seconds, unchanged?",[67,68],"line",{"x1":69,"y1":70,"x2":71,"y2":72,"stroke":73,"style":74},"270","74","180","112","var(--teal,#0f9488)","stroke-width:1.5px",[76,77],"polygon",{"points":78,"fill":79},"176,114 188,108 186,118","#0f766e",[56,81,85],{"x":82,"y":83,"style":84},"212","98","font-size:11.5px;font-weight:700;fill:var(--teal,#0f9488)","yes",[67,87],{"x1":88,"y1":70,"x2":89,"y2":72,"stroke":90,"style":74},"470","560","var(--danger,#dc2626)",[76,92],{"points":93,"fill":94},"564,114 552,108 554,118","#d81b73",[56,96,99],{"x":97,"y":83,"style":98},"522","font-size:11.5px;font-weight:700;fill:var(--danger,#dc2626)","no",[39,101],{"x":102,"y":103,"width":49,"height":104,"rx":105,"fill":106,"stroke":73,"style":54},"24","118","104","14","#d9f4f1",[56,108,112],{"x":109,"y":110,"style":111},"184","146","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","retry with backoff",[56,114,117],{"x":109,"y":115,"style":116},"172","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","PermissionError · TimeoutError",[56,119,121],{"x":109,"y":120,"style":116},"192","ConnectionError · SMTP 4xx",[56,123,125],{"x":109,"y":82,"style":124},"font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","bounded attempts, then alert",[39,127],{"x":128,"y":103,"width":49,"height":104,"rx":105,"fill":129,"stroke":90,"style":54},"396","#fce9e9",[56,131,134],{"x":132,"y":110,"style":133},"556","font-size:13px;font-weight:700;fill:var(--danger,#dc2626);text-anchor:middle","stop and report",[56,136,137],{"x":132,"y":115,"style":116},"KeyError · ValueError",[56,139,140],{"x":132,"y":120,"style":116},"auth rejected · SMTP 5xx",[56,142,143],{"x":132,"y":82,"style":124},"retrying only delays the alert",[145,146,148],"h2",{"id":147},"prerequisites","Prerequisites",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[157,158,159],"code",{"__ignoreMap":155},[160,161,163,167,171,174],"span",{"class":67,"line":162},1,[160,164,166],{"class":165},"sMTad","pip",[160,168,170],{"class":169},"srMev"," install",[160,172,173],{"class":169}," pandas",[160,175,176],{"class":169}," openpyxl\n",[10,178,179,180,183,184,183,187,190,191,194],{},"Everything else is standard library: ",[157,181,182],{},"time",", ",[157,185,186],{},"random",[157,188,189],{},"functools"," and ",[157,192,193],{},"logging",".",[145,196,198],{"id":197},"a-reusable-retry-decorator","A reusable retry decorator",[10,200,201],{},"Put the policy in one place so every call site does not reinvent it:",[150,203,207],{"className":204,"code":205,"language":206,"meta":155,"style":155},"language-python shiki shiki-themes github-light github-dark-high-contrast","import functools\nimport logging\nimport random\nimport time\n\nlog = logging.getLogger(\"report\")\n\nTRANSIENT = (PermissionError, TimeoutError, ConnectionError, OSError)\n\ndef retry(attempts=4, base_delay=2.0, max_delay=30.0, exceptions=TRANSIENT):\n    \"\"\"Retry a callable on transient errors with exponential backoff and jitter.\"\"\"\n    def decorator(fn):\n        @functools.wraps(fn)\n        def wrapper(*args, **kwargs):\n            for attempt in range(1, attempts + 1):\n                try:\n                    return fn(*args, **kwargs)\n                except exceptions as exc:\n                    if attempt == attempts:\n                        log.error(\"%s failed after %d attempt(s): %s\",\n                                  fn.__name__, attempts, exc)\n                        raise\n                    delay = min(base_delay * (2 ** (attempt - 1)), max_delay)\n                    delay *= 0.5 + random.random()          # jitter: 50%–150%\n                    log.warning(\"%s attempt %d\u002F%d failed (%s) — retrying in %.1fs\",\n                                fn.__name__, attempt, attempts, exc, delay)\n                    time.sleep(delay)\n        return wrapper\n    return decorator\n","python",[157,208,209,219,227,235,243,250,268,273,306,311,355,361,373,382,406,437,446,464,479,493,522,534,540,575,596,631,642,648,657],{"__ignoreMap":155},[160,210,211,215],{"class":67,"line":162},[160,212,214],{"class":213},"s-kum","import",[160,216,218],{"class":217},"skGVy"," functools\n",[160,220,222,224],{"class":67,"line":221},2,[160,223,214],{"class":213},[160,225,226],{"class":217}," logging\n",[160,228,230,232],{"class":67,"line":229},3,[160,231,214],{"class":213},[160,233,234],{"class":217}," random\n",[160,236,238,240],{"class":67,"line":237},4,[160,239,214],{"class":213},[160,241,242],{"class":217}," time\n",[160,244,246],{"class":67,"line":245},5,[160,247,249],{"emptyLinePlaceholder":248},true,"\n",[160,251,253,256,259,262,265],{"class":67,"line":252},6,[160,254,255],{"class":217},"log ",[160,257,258],{"class":213},"=",[160,260,261],{"class":217}," logging.getLogger(",[160,263,264],{"class":169},"\"report\"",[160,266,267],{"class":217},")\n",[160,269,271],{"class":67,"line":270},7,[160,272,249],{"emptyLinePlaceholder":248},[160,274,276,280,283,286,289,291,294,296,299,301,304],{"class":67,"line":275},8,[160,277,279],{"class":278},"sP0c6","TRANSIENT",[160,281,282],{"class":213}," =",[160,284,285],{"class":217}," (",[160,287,288],{"class":278},"PermissionError",[160,290,183],{"class":217},[160,292,293],{"class":278},"TimeoutError",[160,295,183],{"class":217},[160,297,298],{"class":278},"ConnectionError",[160,300,183],{"class":217},[160,302,303],{"class":278},"OSError",[160,305,267],{"class":217},[160,307,309],{"class":67,"line":308},9,[160,310,249],{"emptyLinePlaceholder":248},[160,312,314,317,321,324,326,329,332,334,337,340,342,345,348,350,352],{"class":67,"line":313},10,[160,315,316],{"class":213},"def",[160,318,320],{"class":319},"s_Opv"," retry",[160,322,323],{"class":217},"(attempts",[160,325,258],{"class":213},[160,327,328],{"class":278},"4",[160,330,331],{"class":217},", base_delay",[160,333,258],{"class":213},[160,335,336],{"class":278},"2.0",[160,338,339],{"class":217},", max_delay",[160,341,258],{"class":213},[160,343,344],{"class":278},"30.0",[160,346,347],{"class":217},", exceptions",[160,349,258],{"class":213},[160,351,279],{"class":278},[160,353,354],{"class":217},"):\n",[160,356,358],{"class":67,"line":357},11,[160,359,360],{"class":169},"    \"\"\"Retry a callable on transient errors with exponential backoff and jitter.\"\"\"\n",[160,362,364,367,370],{"class":67,"line":363},12,[160,365,366],{"class":213},"    def",[160,368,369],{"class":319}," decorator",[160,371,372],{"class":217},"(fn):\n",[160,374,376,379],{"class":67,"line":375},13,[160,377,378],{"class":319},"        @functools.wraps",[160,380,381],{"class":217},"(fn)\n",[160,383,385,388,391,394,397,400,403],{"class":67,"line":384},14,[160,386,387],{"class":213},"        def",[160,389,390],{"class":319}," wrapper",[160,392,393],{"class":217},"(",[160,395,396],{"class":213},"*",[160,398,399],{"class":217},"args, ",[160,401,402],{"class":213},"**",[160,404,405],{"class":217},"kwargs):\n",[160,407,409,412,415,418,421,423,426,429,432,435],{"class":67,"line":408},15,[160,410,411],{"class":213},"            for",[160,413,414],{"class":217}," attempt ",[160,416,417],{"class":213},"in",[160,419,420],{"class":278}," range",[160,422,393],{"class":217},[160,424,425],{"class":278},"1",[160,427,428],{"class":217},", attempts ",[160,430,431],{"class":213},"+",[160,433,434],{"class":278}," 1",[160,436,354],{"class":217},[160,438,440,443],{"class":67,"line":439},16,[160,441,442],{"class":213},"                try",[160,444,445],{"class":217},":\n",[160,447,449,452,455,457,459,461],{"class":67,"line":448},17,[160,450,451],{"class":213},"                    return",[160,453,454],{"class":217}," fn(",[160,456,396],{"class":213},[160,458,399],{"class":217},[160,460,402],{"class":213},[160,462,463],{"class":217},"kwargs)\n",[160,465,467,470,473,476],{"class":67,"line":466},18,[160,468,469],{"class":213},"                except",[160,471,472],{"class":217}," exceptions ",[160,474,475],{"class":213},"as",[160,477,478],{"class":217}," exc:\n",[160,480,482,485,487,490],{"class":67,"line":481},19,[160,483,484],{"class":213},"                    if",[160,486,414],{"class":217},[160,488,489],{"class":213},"==",[160,491,492],{"class":217}," attempts:\n",[160,494,496,499,502,506,509,512,515,517,519],{"class":67,"line":495},20,[160,497,498],{"class":217},"                        log.error(",[160,500,501],{"class":169},"\"",[160,503,505],{"class":504},"sSjpA","%s",[160,507,508],{"class":169}," failed after ",[160,510,511],{"class":504},"%d",[160,513,514],{"class":169}," attempt(s): ",[160,516,505],{"class":504},[160,518,501],{"class":169},[160,520,521],{"class":217},",\n",[160,523,525,528,531],{"class":67,"line":524},21,[160,526,527],{"class":217},"                                  fn.",[160,529,530],{"class":278},"__name__",[160,532,533],{"class":217},", attempts, exc)\n",[160,535,537],{"class":67,"line":536},22,[160,538,539],{"class":213},"                        raise\n",[160,541,543,546,548,551,554,556,558,561,564,567,570,572],{"class":67,"line":542},23,[160,544,545],{"class":217},"                    delay ",[160,547,258],{"class":213},[160,549,550],{"class":278}," min",[160,552,553],{"class":217},"(base_delay ",[160,555,396],{"class":213},[160,557,285],{"class":217},[160,559,560],{"class":278},"2",[160,562,563],{"class":213}," **",[160,565,566],{"class":217}," (attempt ",[160,568,569],{"class":213},"-",[160,571,434],{"class":278},[160,573,574],{"class":217},")), max_delay)\n",[160,576,578,580,583,586,589,592],{"class":67,"line":577},24,[160,579,545],{"class":217},[160,581,582],{"class":213},"*=",[160,584,585],{"class":278}," 0.5",[160,587,588],{"class":213}," +",[160,590,591],{"class":217}," random.random()          ",[160,593,595],{"class":594},"s-wDw","# jitter: 50%–150%\n",[160,597,599,602,604,606,608,610,613,615,618,620,623,626,629],{"class":67,"line":598},25,[160,600,601],{"class":217},"                    log.warning(",[160,603,501],{"class":169},[160,605,505],{"class":504},[160,607,414],{"class":169},[160,609,511],{"class":504},[160,611,612],{"class":169},"\u002F",[160,614,511],{"class":504},[160,616,617],{"class":169}," failed (",[160,619,505],{"class":504},[160,621,622],{"class":169},") — retrying in ",[160,624,625],{"class":504},"%.1f",[160,627,628],{"class":169},"s\"",[160,630,521],{"class":217},[160,632,634,637,639],{"class":67,"line":633},26,[160,635,636],{"class":217},"                                fn.",[160,638,530],{"class":278},[160,640,641],{"class":217},", attempt, attempts, exc, delay)\n",[160,643,645],{"class":67,"line":644},27,[160,646,647],{"class":217},"                    time.sleep(delay)\n",[160,649,651,654],{"class":67,"line":650},28,[160,652,653],{"class":213},"        return",[160,655,656],{"class":217}," wrapper\n",[160,658,660,663],{"class":67,"line":659},29,[160,661,662],{"class":213},"    return",[160,664,665],{"class":217}," decorator\n",[10,667,668,671,672,675],{},[157,669,670],{},"functools.wraps"," keeps the wrapped function's name and docstring, which matters because the log messages use ",[157,673,674],{},"fn.__name__",". The jitter multiplier spreads retries that would otherwise collide — two regional jobs that both failed on the same locked file at 06:00 would, without it, retry at exactly the same instants and fail together every time.",[10,677,678,680,681,190,683,685,686,688,689,692],{},[157,679,303],{}," is in the transient list because ",[157,682,288],{},[157,684,298],{}," are both subclasses of it, and because network-share hiccups surface as bare ",[157,687,303],{}," on some platforms. That breadth is deliberate here, but it is also why the decorator takes an ",[157,690,691],{},"exceptions"," argument: narrow it wherever you can be specific.",[145,694,696],{"id":695},"apply-it-at-the-right-boundary","Apply it at the right boundary",[10,698,699],{},"The unit of retry should be the smallest step that can fail on its own — not the whole pipeline:",[150,701,703],{"className":204,"code":702,"language":206,"meta":155,"style":155},"from pathlib import Path\n\nimport pandas as pd\nfrom openpyxl import load_workbook\n\n@retry(attempts=4)\ndef read_source(path):\n    return pd.read_excel(path, sheet_name=\"Orders\")\n\n@retry(attempts=5, base_delay=2.0)\ndef save_report(wb, target):\n    wb.save(target)\n    return Path(target)\n\n@retry(attempts=3, base_delay=5.0)\ndef send_report(path, to):\n    deliver(path, to)                    # your SMTP call\n",[157,704,705,718,722,734,746,750,767,777,794,798,822,832,837,844,848,872,882],{"__ignoreMap":155},[160,706,707,710,713,715],{"class":67,"line":162},[160,708,709],{"class":213},"from",[160,711,712],{"class":217}," pathlib ",[160,714,214],{"class":213},[160,716,717],{"class":217}," Path\n",[160,719,720],{"class":67,"line":221},[160,721,249],{"emptyLinePlaceholder":248},[160,723,724,726,729,731],{"class":67,"line":229},[160,725,214],{"class":213},[160,727,728],{"class":217}," pandas ",[160,730,475],{"class":213},[160,732,733],{"class":217}," pd\n",[160,735,736,738,741,743],{"class":67,"line":237},[160,737,709],{"class":213},[160,739,740],{"class":217}," openpyxl ",[160,742,214],{"class":213},[160,744,745],{"class":217}," load_workbook\n",[160,747,748],{"class":67,"line":245},[160,749,249],{"emptyLinePlaceholder":248},[160,751,752,755,757,761,763,765],{"class":67,"line":252},[160,753,754],{"class":319},"@retry",[160,756,393],{"class":217},[160,758,760],{"class":759},"sa561","attempts",[160,762,258],{"class":213},[160,764,328],{"class":278},[160,766,267],{"class":217},[160,768,769,771,774],{"class":67,"line":270},[160,770,316],{"class":213},[160,772,773],{"class":319}," read_source",[160,775,776],{"class":217},"(path):\n",[160,778,779,781,784,787,789,792],{"class":67,"line":275},[160,780,662],{"class":213},[160,782,783],{"class":217}," pd.read_excel(path, ",[160,785,786],{"class":759},"sheet_name",[160,788,258],{"class":213},[160,790,791],{"class":169},"\"Orders\"",[160,793,267],{"class":217},[160,795,796],{"class":67,"line":308},[160,797,249],{"emptyLinePlaceholder":248},[160,799,800,802,804,806,808,811,813,816,818,820],{"class":67,"line":313},[160,801,754],{"class":319},[160,803,393],{"class":217},[160,805,760],{"class":759},[160,807,258],{"class":213},[160,809,810],{"class":278},"5",[160,812,183],{"class":217},[160,814,815],{"class":759},"base_delay",[160,817,258],{"class":213},[160,819,336],{"class":278},[160,821,267],{"class":217},[160,823,824,826,829],{"class":67,"line":357},[160,825,316],{"class":213},[160,827,828],{"class":319}," save_report",[160,830,831],{"class":217},"(wb, target):\n",[160,833,834],{"class":67,"line":363},[160,835,836],{"class":217},"    wb.save(target)\n",[160,838,839,841],{"class":67,"line":375},[160,840,662],{"class":213},[160,842,843],{"class":217}," Path(target)\n",[160,845,846],{"class":67,"line":384},[160,847,249],{"emptyLinePlaceholder":248},[160,849,850,852,854,856,858,861,863,865,867,870],{"class":67,"line":408},[160,851,754],{"class":319},[160,853,393],{"class":217},[160,855,760],{"class":759},[160,857,258],{"class":213},[160,859,860],{"class":278},"3",[160,862,183],{"class":217},[160,864,815],{"class":759},[160,866,258],{"class":213},[160,868,869],{"class":278},"5.0",[160,871,267],{"class":217},[160,873,874,876,879],{"class":67,"line":439},[160,875,316],{"class":213},[160,877,878],{"class":319}," send_report",[160,880,881],{"class":217},"(path, to):\n",[160,883,884,887],{"class":67,"line":448},[160,885,886],{"class":217},"    deliver(path, to)                    ",[160,888,889],{"class":594},"# your SMTP call\n",[10,891,892],{},"Wrapping the whole job instead would re-read the source, recompute everything and possibly re-send an email that already went out. Small boundaries also let each step carry its own policy: five patient attempts for a save that is probably blocked by a colleague, three for an SMTP send where a delay of a minute is already unhelpful.",[20,894,29,899,29,902,29,905,29,908,29,913,29,919,29,925,29,929,29,933,29,936,29,940,29,943,29,947,29,952,29,957,29,962,29,965,29,968,29,971,29,974,29,979,29,982,29,985,29,990,29,997,29,1002],{"viewBox":895,"role":23,"ariaLabelledBy":896,"xmlns":27,"style":28},"0 0 740 214",[897,898],"rt-scope-t","rt-scope-d",[31,900,901],{"id":897},"Retrying one step versus retrying the whole job",[35,903,904],{"id":898},"Retrying the whole pipeline repeats the read and the transform and risks sending a duplicate email. Retrying only the failing save repeats a few seconds of work and has no side effects.",[39,906],{"x":41,"y":41,"width":42,"height":907,"fill":44},"214",[56,909,912],{"x":71,"y":910,"style":911},"30","font-size:12.5px;font-weight:700;fill:var(--danger,#dc2626);text-anchor:middle","retry the whole job",[39,914],{"x":102,"y":915,"width":916,"height":917,"rx":918,"fill":129,"stroke":90},"44","70","40","8",[56,920,924],{"x":921,"y":922,"style":923},"59","69","font-size:11px;fill:var(--text,#172033);text-anchor:middle","read",[39,926],{"x":927,"y":915,"width":928,"height":917,"rx":918,"fill":129,"stroke":90},"100","80",[56,930,932],{"x":931,"y":922,"style":923},"140","transform",[39,934],{"x":935,"y":915,"width":916,"height":917,"rx":918,"fill":129,"stroke":90},"186",[56,937,939],{"x":938,"y":922,"style":923},"221","save ✗",[39,941],{"x":942,"y":915,"width":70,"height":917,"rx":918,"fill":129,"stroke":90},"262",[56,944,946],{"x":945,"y":922,"style":923},"299","email",[56,948,951],{"x":71,"y":949,"style":950},"106","font-size:11px;fill:var(--danger,#dc2626);text-anchor:middle","all four repeat — duplicate email risk",[56,953,956],{"x":954,"y":910,"style":955},"540","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","retry the failing step",[39,958],{"x":959,"y":915,"width":916,"height":917,"rx":918,"fill":960,"stroke":961},"390","#f0f2f5","var(--line,#cdd5e6)",[56,963,924],{"x":964,"y":922,"style":124},"425",[39,966],{"x":967,"y":915,"width":928,"height":917,"rx":918,"fill":960,"stroke":961},"466",[56,969,932],{"x":970,"y":922,"style":124},"506",[39,972],{"x":973,"y":915,"width":916,"height":917,"rx":918,"fill":106,"stroke":73,"style":54},"552",[56,975,978],{"x":976,"y":922,"style":977},"587","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","save ↻",[39,980],{"x":981,"y":915,"width":70,"height":917,"rx":918,"fill":960,"stroke":961},"628",[56,983,946],{"x":984,"y":922,"style":124},"665",[56,986,989],{"x":987,"y":949,"style":988},"546","font-size:11px;fill:var(--teal,#0f9488);text-anchor:middle","only the save repeats — no side effects",[39,991],{"x":992,"y":993,"width":994,"height":50,"rx":51,"fill":995,"stroke":996},"150","134","440","#fdefd8","var(--gold,#b4740a)",[56,998,1001],{"x":58,"y":999,"style":1000},"156","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Rule: the retry boundary is the smallest independently repeatable step",[56,1003,1006],{"x":58,"y":1004,"style":1005},"176","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","anything with a side effect gets its own boundary and its own guard",[145,1008,1010],{"id":1009},"do-not-retry-a-data-failure","Do not retry a data failure",[10,1012,1013,1014,1017,1018,1021],{},"The mistake that costs the most is catching too much. A ",[157,1015,1016],{},"KeyError"," for a missing column, a ",[157,1019,1020],{},"ValueError"," from a validation gate — these are facts about the file, and four attempts produce four identical failures and a four-minute delay before anyone is told:",[150,1023,1025],{"className":204,"code":1024,"language":206,"meta":155,"style":155},"class DataProblem(Exception):\n    \"\"\"Raised for anything that will fail identically on a second attempt.\"\"\"\n\n@retry(attempts=4)                       # only transient errors are caught\ndef load_and_check(path):\n    df = pd.read_excel(path, sheet_name=\"Orders\")     # may raise PermissionError → retry\n    required = {\"Order_ID\", \"Region\", \"Quantity\"}\n    missing = required - set(df.columns)\n    if missing:\n        raise DataProblem(f\"missing column(s): {sorted(missing)}\")   # not retried\n    return df\n",[157,1026,1027,1042,1047,1051,1069,1078,1099,1125,1143,1151,1185],{"__ignoreMap":155},[160,1028,1029,1032,1035,1037,1040],{"class":67,"line":162},[160,1030,1031],{"class":213},"class",[160,1033,1034],{"class":165}," DataProblem",[160,1036,393],{"class":217},[160,1038,1039],{"class":278},"Exception",[160,1041,354],{"class":217},[160,1043,1044],{"class":67,"line":221},[160,1045,1046],{"class":169},"    \"\"\"Raised for anything that will fail identically on a second attempt.\"\"\"\n",[160,1048,1049],{"class":67,"line":229},[160,1050,249],{"emptyLinePlaceholder":248},[160,1052,1053,1055,1057,1059,1061,1063,1066],{"class":67,"line":237},[160,1054,754],{"class":319},[160,1056,393],{"class":217},[160,1058,760],{"class":759},[160,1060,258],{"class":213},[160,1062,328],{"class":278},[160,1064,1065],{"class":217},")                       ",[160,1067,1068],{"class":594},"# only transient errors are caught\n",[160,1070,1071,1073,1076],{"class":67,"line":245},[160,1072,316],{"class":213},[160,1074,1075],{"class":319}," load_and_check",[160,1077,776],{"class":217},[160,1079,1080,1083,1085,1087,1089,1091,1093,1096],{"class":67,"line":252},[160,1081,1082],{"class":217},"    df ",[160,1084,258],{"class":213},[160,1086,783],{"class":217},[160,1088,786],{"class":759},[160,1090,258],{"class":213},[160,1092,791],{"class":169},[160,1094,1095],{"class":217},")     ",[160,1097,1098],{"class":594},"# may raise PermissionError → retry\n",[160,1100,1101,1104,1106,1109,1112,1114,1117,1119,1122],{"class":67,"line":270},[160,1102,1103],{"class":217},"    required ",[160,1105,258],{"class":213},[160,1107,1108],{"class":217}," {",[160,1110,1111],{"class":169},"\"Order_ID\"",[160,1113,183],{"class":217},[160,1115,1116],{"class":169},"\"Region\"",[160,1118,183],{"class":217},[160,1120,1121],{"class":169},"\"Quantity\"",[160,1123,1124],{"class":217},"}\n",[160,1126,1127,1130,1132,1135,1137,1140],{"class":67,"line":275},[160,1128,1129],{"class":217},"    missing ",[160,1131,258],{"class":213},[160,1133,1134],{"class":217}," required ",[160,1136,569],{"class":213},[160,1138,1139],{"class":278}," set",[160,1141,1142],{"class":217},"(df.columns)\n",[160,1144,1145,1148],{"class":67,"line":308},[160,1146,1147],{"class":213},"    if",[160,1149,1150],{"class":217}," missing:\n",[160,1152,1153,1156,1159,1162,1165,1168,1171,1174,1177,1179,1182],{"class":67,"line":313},[160,1154,1155],{"class":213},"        raise",[160,1157,1158],{"class":217}," DataProblem(",[160,1160,1161],{"class":213},"f",[160,1163,1164],{"class":169},"\"missing column(s): ",[160,1166,1167],{"class":504},"{",[160,1169,1170],{"class":278},"sorted",[160,1172,1173],{"class":217},"(missing)",[160,1175,1176],{"class":504},"}",[160,1178,501],{"class":169},[160,1180,1181],{"class":217},")   ",[160,1183,1184],{"class":594},"# not retried\n",[160,1186,1187,1189],{"class":67,"line":357},[160,1188,662],{"class":213},[160,1190,1191],{"class":217}," df\n",[10,1193,1194,1195,1198],{},"Because ",[157,1196,1197],{},"DataProblem"," is not in the transient tuple, it propagates immediately on the first attempt. Defining your own exception type for this is worth the three lines: it documents the intent, and it stops a future edit to the transient tuple accidentally making validation failures retryable.",[145,1200,1202],{"id":1201},"make-retried-steps-safe-to-repeat","Make retried steps safe to repeat",[10,1204,1205],{},"A retry runs the same code twice, so any step with a side effect needs a guard. For file writes, that means writing to a temporary name and renaming; for deliveries, a marker:",[150,1207,1209],{"className":204,"code":1208,"language":206,"meta":155,"style":155},"import os\nfrom pathlib import Path\n\n@retry(attempts=5)\ndef atomic_save(wb, target):\n    target = Path(target)\n    temp = target.with_name(f\".{target.stem}.tmp{target.suffix}\")\n    wb.save(temp)                 # a failed attempt leaves only a temp file\n    os.replace(temp, target)\n    return target\n\ndef send_once(path, to, marker_dir=\"state\"):\n    marker = Path(marker_dir) \u002F f\"{Path(path).stem}.sent\"\n    if marker.exists():\n        log.info(\"already delivered %s at %s\", path, marker.read_text().strip())\n        return False\n    send_report(path, to)         # itself retried\n    Path(marker_dir).mkdir(exist_ok=True)\n    marker.write_text(str(Path(path).stat().st_mtime))\n    return True\n",[157,1210,1211,1218,1228,1232,1246,1255,1264,1300,1308,1313,1320,1324,1341,1368,1375,1395,1402,1410,1425,1436],{"__ignoreMap":155},[160,1212,1213,1215],{"class":67,"line":162},[160,1214,214],{"class":213},[160,1216,1217],{"class":217}," os\n",[160,1219,1220,1222,1224,1226],{"class":67,"line":221},[160,1221,709],{"class":213},[160,1223,712],{"class":217},[160,1225,214],{"class":213},[160,1227,717],{"class":217},[160,1229,1230],{"class":67,"line":229},[160,1231,249],{"emptyLinePlaceholder":248},[160,1233,1234,1236,1238,1240,1242,1244],{"class":67,"line":237},[160,1235,754],{"class":319},[160,1237,393],{"class":217},[160,1239,760],{"class":759},[160,1241,258],{"class":213},[160,1243,810],{"class":278},[160,1245,267],{"class":217},[160,1247,1248,1250,1253],{"class":67,"line":245},[160,1249,316],{"class":213},[160,1251,1252],{"class":319}," atomic_save",[160,1254,831],{"class":217},[160,1256,1257,1260,1262],{"class":67,"line":252},[160,1258,1259],{"class":217},"    target ",[160,1261,258],{"class":213},[160,1263,843],{"class":217},[160,1265,1266,1269,1271,1274,1276,1279,1281,1284,1286,1289,1291,1294,1296,1298],{"class":67,"line":270},[160,1267,1268],{"class":217},"    temp ",[160,1270,258],{"class":213},[160,1272,1273],{"class":217}," target.with_name(",[160,1275,1161],{"class":213},[160,1277,1278],{"class":169},"\".",[160,1280,1167],{"class":504},[160,1282,1283],{"class":217},"target.stem",[160,1285,1176],{"class":504},[160,1287,1288],{"class":169},".tmp",[160,1290,1167],{"class":504},[160,1292,1293],{"class":217},"target.suffix",[160,1295,1176],{"class":504},[160,1297,501],{"class":169},[160,1299,267],{"class":217},[160,1301,1302,1305],{"class":67,"line":275},[160,1303,1304],{"class":217},"    wb.save(temp)                 ",[160,1306,1307],{"class":594},"# a failed attempt leaves only a temp file\n",[160,1309,1310],{"class":67,"line":308},[160,1311,1312],{"class":217},"    os.replace(temp, target)\n",[160,1314,1315,1317],{"class":67,"line":313},[160,1316,662],{"class":213},[160,1318,1319],{"class":217}," target\n",[160,1321,1322],{"class":67,"line":357},[160,1323,249],{"emptyLinePlaceholder":248},[160,1325,1326,1328,1331,1334,1336,1339],{"class":67,"line":363},[160,1327,316],{"class":213},[160,1329,1330],{"class":319}," send_once",[160,1332,1333],{"class":217},"(path, to, marker_dir",[160,1335,258],{"class":213},[160,1337,1338],{"class":169},"\"state\"",[160,1340,354],{"class":217},[160,1342,1343,1346,1348,1351,1353,1356,1358,1360,1363,1365],{"class":67,"line":375},[160,1344,1345],{"class":217},"    marker ",[160,1347,258],{"class":213},[160,1349,1350],{"class":217}," Path(marker_dir) ",[160,1352,612],{"class":213},[160,1354,1355],{"class":213}," f",[160,1357,501],{"class":169},[160,1359,1167],{"class":504},[160,1361,1362],{"class":217},"Path(path).stem",[160,1364,1176],{"class":504},[160,1366,1367],{"class":169},".sent\"\n",[160,1369,1370,1372],{"class":67,"line":384},[160,1371,1147],{"class":213},[160,1373,1374],{"class":217}," marker.exists():\n",[160,1376,1377,1380,1383,1385,1388,1390,1392],{"class":67,"line":408},[160,1378,1379],{"class":217},"        log.info(",[160,1381,1382],{"class":169},"\"already delivered ",[160,1384,505],{"class":504},[160,1386,1387],{"class":169}," at ",[160,1389,505],{"class":504},[160,1391,501],{"class":169},[160,1393,1394],{"class":217},", path, marker.read_text().strip())\n",[160,1396,1397,1399],{"class":67,"line":439},[160,1398,653],{"class":213},[160,1400,1401],{"class":278}," False\n",[160,1403,1404,1407],{"class":67,"line":448},[160,1405,1406],{"class":217},"    send_report(path, to)         ",[160,1408,1409],{"class":594},"# itself retried\n",[160,1411,1412,1415,1418,1420,1423],{"class":67,"line":466},[160,1413,1414],{"class":217},"    Path(marker_dir).mkdir(",[160,1416,1417],{"class":759},"exist_ok",[160,1419,258],{"class":213},[160,1421,1422],{"class":278},"True",[160,1424,267],{"class":217},[160,1426,1427,1430,1433],{"class":67,"line":481},[160,1428,1429],{"class":217},"    marker.write_text(",[160,1431,1432],{"class":278},"str",[160,1434,1435],{"class":217},"(Path(path).stat().st_mtime))\n",[160,1437,1438,1440],{"class":67,"line":495},[160,1439,662],{"class":213},[160,1441,1442],{"class":278}," True\n",[10,1444,1445],{},"The marker turns \"send the report\" into an idempotent operation, which matters as soon as anything above it retries. Storing the file's modification time in the marker also gives you a cheap way to detect a regenerated report that genuinely should be sent again.",[145,1447,1449],{"id":1448},"give-up-loudly","Give up loudly",[10,1451,1452],{},"A retry policy that ends in silence is worse than no retries at all. The final attempt should produce an alert that names the step, the target and the last error:",[150,1454,1456],{"className":204,"code":1455,"language":206,"meta":155,"style":155},"def run():\n    try:\n        df = load_and_check(SOURCE)\n        report = build(df)\n        atomic_save(report, TARGET)\n        send_once(TARGET, \"finance@example.com\")\n        return 0\n    except DataProblem as exc:\n        alert(\"Report blocked — data problem\", f\"{SOURCE}: {exc}\")\n        return 1\n    except TRANSIENT as exc:\n        alert(\"Report failed after retries\", f\"{type(exc).__name__}: {exc}\")\n        return 2\n",[157,1457,1458,1468,1475,1490,1500,1510,1524,1531,1543,1577,1584,1596,1633],{"__ignoreMap":155},[160,1459,1460,1462,1465],{"class":67,"line":162},[160,1461,316],{"class":213},[160,1463,1464],{"class":319}," run",[160,1466,1467],{"class":217},"():\n",[160,1469,1470,1473],{"class":67,"line":221},[160,1471,1472],{"class":213},"    try",[160,1474,445],{"class":217},[160,1476,1477,1480,1482,1485,1488],{"class":67,"line":229},[160,1478,1479],{"class":217},"        df ",[160,1481,258],{"class":213},[160,1483,1484],{"class":217}," load_and_check(",[160,1486,1487],{"class":278},"SOURCE",[160,1489,267],{"class":217},[160,1491,1492,1495,1497],{"class":67,"line":237},[160,1493,1494],{"class":217},"        report ",[160,1496,258],{"class":213},[160,1498,1499],{"class":217}," build(df)\n",[160,1501,1502,1505,1508],{"class":67,"line":245},[160,1503,1504],{"class":217},"        atomic_save(report, ",[160,1506,1507],{"class":278},"TARGET",[160,1509,267],{"class":217},[160,1511,1512,1515,1517,1519,1522],{"class":67,"line":252},[160,1513,1514],{"class":217},"        send_once(",[160,1516,1507],{"class":278},[160,1518,183],{"class":217},[160,1520,1521],{"class":169},"\"finance@example.com\"",[160,1523,267],{"class":217},[160,1525,1526,1528],{"class":67,"line":270},[160,1527,653],{"class":213},[160,1529,1530],{"class":278}," 0\n",[160,1532,1533,1536,1539,1541],{"class":67,"line":275},[160,1534,1535],{"class":213},"    except",[160,1537,1538],{"class":217}," DataProblem ",[160,1540,475],{"class":213},[160,1542,478],{"class":217},[160,1544,1545,1548,1551,1553,1555,1557,1559,1561,1563,1566,1568,1571,1573,1575],{"class":67,"line":308},[160,1546,1547],{"class":217},"        alert(",[160,1549,1550],{"class":169},"\"Report blocked — data problem\"",[160,1552,183],{"class":217},[160,1554,1161],{"class":213},[160,1556,501],{"class":169},[160,1558,1167],{"class":504},[160,1560,1487],{"class":278},[160,1562,1176],{"class":504},[160,1564,1565],{"class":169},": ",[160,1567,1167],{"class":504},[160,1569,1570],{"class":217},"exc",[160,1572,1176],{"class":504},[160,1574,501],{"class":169},[160,1576,267],{"class":217},[160,1578,1579,1581],{"class":67,"line":313},[160,1580,653],{"class":213},[160,1582,1583],{"class":278}," 1\n",[160,1585,1586,1588,1591,1594],{"class":67,"line":357},[160,1587,1535],{"class":213},[160,1589,1590],{"class":278}," TRANSIENT",[160,1592,1593],{"class":213}," as",[160,1595,478],{"class":217},[160,1597,1598,1600,1603,1605,1607,1609,1611,1614,1617,1619,1621,1623,1625,1627,1629,1631],{"class":67,"line":363},[160,1599,1547],{"class":217},[160,1601,1602],{"class":169},"\"Report failed after retries\"",[160,1604,183],{"class":217},[160,1606,1161],{"class":213},[160,1608,501],{"class":169},[160,1610,1167],{"class":504},[160,1612,1613],{"class":278},"type",[160,1615,1616],{"class":217},"(exc).",[160,1618,530],{"class":278},[160,1620,1176],{"class":504},[160,1622,1565],{"class":169},[160,1624,1167],{"class":504},[160,1626,1570],{"class":217},[160,1628,1176],{"class":504},[160,1630,501],{"class":169},[160,1632,267],{"class":217},[160,1634,1635,1637],{"class":67,"line":375},[160,1636,653],{"class":213},[160,1638,1639],{"class":278}," 2\n",[10,1641,1642],{},"Two exit codes, two different messages, and both say what a human should do next. The data problem needs the file fixed; the transient failure needs someone to check whether the share is still down.",[145,1644,1646],{"id":1645},"choosing-the-delay-curve","Choosing the delay curve",[10,1648,1649],{},"The shape of the wait matters as much as the number of attempts. Three curves cover almost every case:",[20,1651,29,1656,29,1659,29,1662,29,1665,29,1669,29,1673,29,1677,29,1679,29,1682,29,1684,29,1688,29,1691,29,1696,29,1699,29,1703,29,1708,29,1712,29,1716,29,1719,29,1722,29,1725,29,1728,29,1731,29,1735,29,1738,29,1741,29,1745,29,1748,29,1751,29,1755],{"viewBox":1652,"role":23,"ariaLabelledBy":1653,"xmlns":27,"style":28},"0 0 740 216",[1654,1655],"rt-curve-t","rt-curve-d",[31,1657,1658],{"id":1654},"Fixed, linear and exponential backoff compared",[35,1660,1661],{"id":1655},"A fixed delay retries at the same short interval and gives up quickly. Linear backoff grows steadily. Exponential backoff waits far longer by the last attempt, covering a slow-clearing problem within a bounded total time.",[39,1663],{"x":41,"y":41,"width":42,"height":1664,"fill":44},"216",[39,1666],{"x":48,"y":910,"width":1667,"height":1668,"rx":105,"fill":52,"stroke":961},"228","158",[56,1670,1672],{"x":993,"y":1671,"style":60},"58","fixed · 2s each",[39,1674],{"x":915,"y":1675,"width":102,"height":105,"rx":860,"fill":1676},"76","#5b5cf0",[39,1678],{"x":1675,"y":1675,"width":102,"height":105,"rx":860,"fill":1676},[39,1680],{"x":1681,"y":1675,"width":102,"height":105,"rx":860,"fill":1676},"108",[39,1683],{"x":931,"y":1675,"width":102,"height":105,"rx":860,"fill":1676},[56,1685,1687],{"x":993,"y":1686,"style":1005},"122","8s total",[56,1689,1690],{"x":993,"y":110,"style":116},"good for a fast flap",[56,1692,1695],{"x":993,"y":1693,"style":1694},"168","font-size:11.5px;fill:var(--danger,#dc2626);text-anchor:middle","gives up too soon on a lock",[39,1697],{"x":1698,"y":910,"width":1667,"height":1668,"rx":105,"fill":995,"stroke":996},"256",[56,1700,1702],{"x":58,"y":1671,"style":1701},"font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","linear · 2, 4, 6, 8s",[39,1704],{"x":1705,"y":1675,"width":1706,"height":105,"rx":860,"fill":1707},"280","16","#8a5808",[39,1709],{"x":1710,"y":1675,"width":1711,"height":105,"rx":860,"fill":1707},"304","28",[39,1713],{"x":1714,"y":1675,"width":1715,"height":105,"rx":860,"fill":1707},"340","42",[39,1717],{"x":959,"y":1675,"width":1718,"height":105,"rx":860,"fill":1707},"56",[56,1720,1721],{"x":58,"y":1686,"style":1005},"20s total",[56,1723,1724],{"x":58,"y":110,"style":116},"predictable, easy to reason about",[56,1726,1727],{"x":58,"y":1693,"style":1005},"a middle ground",[39,1729],{"x":1730,"y":910,"width":1667,"height":1668,"rx":105,"fill":106,"stroke":73,"style":54},"492",[56,1732,1734],{"x":1733,"y":1671,"style":955},"606","exponential · 2, 4, 8, 16s",[39,1736],{"x":1737,"y":1675,"width":51,"height":105,"rx":860,"fill":79},"516",[39,1739],{"x":1740,"y":1675,"width":102,"height":105,"rx":860,"fill":79},"536",[39,1742],{"x":1743,"y":1675,"width":1744,"height":105,"rx":860,"fill":79},"568","48",[39,1746],{"x":1747,"y":1675,"width":928,"height":105,"rx":860,"fill":79},"624",[56,1749,1750],{"x":1733,"y":1686,"style":1005},"30s total",[56,1752,1754],{"x":1733,"y":110,"style":1753},"font-size:11.5px;fill:var(--teal,#0f9488);text-anchor:middle","covers a slow-clearing lock",[56,1756,1757],{"x":1733,"y":1693,"style":1753},"the sensible default",[10,1759,1760,1761,1764],{},"Exponential is the default for a reason: the early attempts are cheap enough to catch a momentary blip, and the later ones wait long enough to outlast a colleague reading a spreadsheet. Cap it — without a ",[157,1762,1763],{},"max_delay"," a sixth attempt waits over a minute, by which point an alert would have been more useful than another try.",[145,1766,1768],{"id":1767},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1770,1771,1772,1788],"table",{},[1773,1774,1775],"thead",{},[1776,1777,1778,1782,1785],"tr",{},[1779,1780,1781],"th",{},"Symptom",[1779,1783,1784],{},"Cause",[1779,1786,1787],{},"Fix",[1789,1790,1791,1803,1814,1825,1839,1852,1866],"tbody",{},[1776,1792,1793,1797,1800],{},[1794,1795,1796],"td",{},"Same failure four times, alert delayed",[1794,1798,1799],{},"Retrying a data problem",[1794,1801,1802],{},"Keep validation errors out of the transient tuple",[1776,1804,1805,1808,1811],{},[1794,1806,1807],{},"Duplicate emails after a retry",[1794,1809,1810],{},"Retry boundary wrapped the delivery",[1794,1812,1813],{},"Guard delivery with a marker; retry the send alone",[1776,1815,1816,1819,1822],{},[1794,1817,1818],{},"Retries all collide again",[1794,1820,1821],{},"No jitter in the delay",[1794,1823,1824],{},"Randomise the wait by 50–150%",[1776,1826,1827,1830,1833],{},[1794,1828,1829],{},"Truncated output file",[1794,1831,1832],{},"Retried a save that writes in place",[1794,1834,1835,1836],{},"Write to a temp file and ",[157,1837,1838],{},"os.replace",[1776,1840,1841,1844,1849],{},[1794,1842,1843],{},"Job hangs for minutes",[1794,1845,1846,1847],{},"Too many attempts or no ",[157,1848,1763],{},[1794,1850,1851],{},"Three to five attempts, cap the delay",[1776,1853,1854,1857,1863],{},[1794,1855,1856],{},"Retry loop swallows the error",[1794,1858,1859,1862],{},[157,1860,1861],{},"except"," without a re-raise on the last attempt",[1794,1864,1865],{},"Re-raise when attempts are exhausted",[1776,1867,1868,1871,1876],{},[1794,1869,1870],{},"Wrapped function name lost in logs",[1794,1872,1873,1874],{},"Missing ",[157,1875,670],{},[1794,1877,1878],{},"Add it to the decorator",[145,1880,1882],{"id":1881},"performance-and-scale-notes","Performance and scale notes",[10,1884,1885],{},"Retries cost only waiting, and the waiting is bounded by design: four attempts with a two-second base and a thirty-second cap is under a minute in the worst case. The cost that surprises people is repeated work — a retry boundary around an expensive step re-does that step, so keep the boundary tight and put it after the expensive part where you can. In a batch job, retry per unit rather than per batch, so one locked regional file does not re-run eleven successful reports.",[145,1887,1889],{"id":1888},"record-every-attempt","Record every attempt",[10,1891,1892],{},"A retry that succeeds on the third try should still leave a trace. Logging each attempt with its error and delay makes a pattern visible — the same file locked every Monday, an SMTP server that always needs two tries — and that pattern is usually the real problem worth fixing. Retries that succeed silently hide exactly the information that would let you stop needing them.",[145,1894,1896],{"id":1895},"conclusion","Conclusion",[10,1898,1899],{},"Retry the failures that time can fix and nothing else. Put the policy in one decorator, wrap the smallest independently repeatable step, add jitter so parallel jobs do not collide, and make every retried step idempotent — atomic writes for files, markers for deliveries. Cap the attempts at three to five, and make the final failure an alert that names the step and the reason, so the retries buy resilience rather than delay.",[145,1901,1903],{"id":1902},"frequently-asked-questions","Frequently asked questions",[10,1905,1906,1910],{},[1907,1908,1909],"strong",{},"Which errors should be retried?","\nOnes caused by timing rather than by content — a locked file, an unreachable share, an SMTP timeout. A missing column or a failed validation will fail identically every time.",[10,1912,1913,1916],{},[1907,1914,1915],{},"Why add jitter to the delay?","\nSo several jobs that failed at the same moment do not retry in lockstep. Randomising the wait spreads the load and avoids a repeated collision on the same resource.",[10,1918,1919,1922],{},[1907,1920,1921],{},"Should I retry the whole job or one step?","\nOne step. Re-running an entire pipeline to recover from a locked save wastes the work already done and can duplicate side effects such as emails.",[10,1924,1925,1928],{},[1907,1926,1927],{},"How many attempts are sensible?","\nThree to five, with exponential backoff, so the total wait is tens of seconds. Beyond that you are delaying an alert someone needs to see.",[145,1930,1932],{"id":1931},"related","Related",[10,1934,1935],{},"Up to the parent guide:",[1937,1938,1939],"ul",{},[1940,1941,1942,1944],"li",{},[14,1943,17],{"href":16}," — the failure taxonomy behind these decisions.",[10,1946,1947],{},"Related guides:",[1937,1949,1950,1957,1964,1971],{},[1940,1951,1952,1956],{},[14,1953,1955],{"href":1954},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002F","Handle Permission Denied When Writing Excel in Python"," — the most commonly retried failure.",[1940,1958,1959,1963],{},[14,1960,1962],{"href":1961},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It"," — the check that must never be retried.",[1940,1965,1966,1970],{},[14,1967,1969],{"href":1968},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," — recording each attempt.",[1940,1972,1973,1977],{},[14,1974,1976],{"href":1975},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002F","Schedule Recurring Excel Reports with APScheduler"," — scheduler-level retries and misfire handling.",[1979,1980,1981],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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 .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":155,"searchDepth":221,"depth":221,"links":1983},[1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996],{"id":147,"depth":221,"text":148},{"id":197,"depth":221,"text":198},{"id":695,"depth":221,"text":696},{"id":1009,"depth":221,"text":1010},{"id":1201,"depth":221,"text":1202},{"id":1448,"depth":221,"text":1449},{"id":1645,"depth":221,"text":1646},{"id":1767,"depth":221,"text":1768},{"id":1881,"depth":221,"text":1882},{"id":1888,"depth":221,"text":1889},{"id":1895,"depth":221,"text":1896},{"id":1902,"depth":221,"text":1903},{"id":1931,"depth":221,"text":1932},"2026-08-01","Retry the failures worth retrying — exponential backoff with jitter, a decorator you can reuse, per-step retry boundaries, idempotent writes and a cap that ends in a real alert.","md",[2001,2003,2005,2007],{"q":1909,"a":2002},"Ones caused by timing rather than by content — a locked file, an unreachable share, an SMTP timeout. A missing column or a failed validation will fail identically every time.",{"q":1915,"a":2004},"So several jobs that failed at the same moment do not retry in lockstep. Randomising the wait spreads the load and avoids a repeated collision on the same resource.",{"q":1921,"a":2006},"One step. Re-running an entire pipeline to recover from a locked save wastes the work already done and can duplicate side effects such as emails.",{"q":1927,"a":2008},"Three to five, with exponential backoff, so the total wait is tens of seconds. Beyond that you are delaying an alert someone needs to see.",{"breadcrumb":2010},[2011,2013,2016,2017],{"name":2012,"item":612},"Home",{"name":2014,"item":2015},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},{"name":5,"item":2018},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python",{"title":5,"description":2021},"Build retry logic for Excel jobs: classify transient versus permanent errors, exponential backoff with jitter, a reusable decorator, checkpointing and safe re-runs.","retry-a-failed-excel-report-job-in-python","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002Findex","how-to","sNNH4ijJt-hjN-tq6soqxValkYsyU_8ZcyM99aRZdEg",[2027,2030],{"title":1969,"path":2028,"stem":2029,"children":-1},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002Findex",{"title":1962,"path":2031,"stem":2032,"children":-1},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002Findex",1785584462870]