[{"data":1,"prerenderedAt":1966},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat":1933,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat":1941,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat":1946,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat":1947},{"id":4,"title":5,"body":6,"dateModified":1900,"datePublished":1900,"description":1901,"extension":1902,"faq":1903,"keyword":1912,"kind":1913,"meta":1914,"navigation":261,"path":1922,"seo":1923,"slug":1926,"stem":1927,"topics":1928,"type":1931,"__hash__":1932},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat\u002Findex.md","Schedule Excel Reports with Celery Beat",{"type":7,"value":8,"toc":1887},"minimark",[9,24,29,68,79,223,227,234,375,401,405,408,837,868,872,878,1052,1063,1166,1170,1180,1546,1564,1568,1571,1615,1631,1742,1746,1753,1757,1798,1802,1822,1826,1832,1838,1844,1850,1854,1883],[10,11,12,13,17,18,23],"p",{},"Web applications built with Django or Flask often already run Celery for background work — sending emails, processing uploads. When the same application needs a weekly Excel report, adding it to Celery keeps everything in one place and avoids a second scheduling system: the report uses the application's models and settings, runs on the workers that are already monitored, and is scheduled by ",[14,15,16],"em",{},"Celery beat",", the component that puts tasks on the queue at set times. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",", sets up the task and the schedule, tests them with a real worker, and covers the operational rule that catches most teams: only one beat may run.",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install celery pandas openpyxl\n# plus a broker in production, usually Redis or RabbitMQ\n","bash","",[37,38,39,61],"code",{"__ignoreMap":35},[40,41,44,48,52,55,58],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," celery",[40,56,57],{"class":50}," pandas",[40,59,60],{"class":50}," openpyxl\n",[40,62,64],{"class":42,"line":63},2,[40,65,67],{"class":66},"s-wDw","# plus a broker in production, usually Redis or RabbitMQ\n",[10,69,70,71,74,75,78],{},"In production Celery needs a message broker, and the examples would use ",[37,72,73],{},"redis:\u002F\u002F…",". Here the app uses Celery's in-memory broker and result backend, which work within a single process — enough to run a real worker thread in a test and exercise the task exactly as a deployed worker would. ",[37,76,77],{},"sales_source.csv"," stands in for the application's database.",[80,81,89,90,89,94,89,98,89,106,89,116,89,123,89,128,89,133,89,136,89,140,89,145,89,147,89,149,89,154,89,159,89,167,89,173,89,178,89,181,89,186,89,191,89,196,89,199,89,204,89,208,89,211,89,214,89,219],"svg",{"viewBox":82,"role":83,"ariaLabelledBy":84,"xmlns":87,"style":88},"4 0 752 292","img",[85,86],"cb-seq-t","cb-seq-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[91,92,93],"title",{"id":85},"How beat, the broker and a worker cooperate",[95,96,97],"desc",{"id":86},"A sequence diagram. Celery beat checks its schedule and at Monday 07:00 sends the weekly report task to the broker; a worker takes the task, builds and saves the workbook, and stores the result; on a transient error the worker schedules a retry.",[99,100],"rect",{"x":101,"y":102,"width":103,"height":104,"fill":105},"4","0","752","292","#ffffff",[99,107],{"x":108,"y":109,"width":110,"height":111,"rx":112,"fill":113,"stroke":114,"style":115},"20.0","16.0","180.0","48.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[117,118,122],"text",{"x":119,"y":120,"style":121},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","celery beat",[99,124],{"x":125,"y":109,"width":110,"height":111,"rx":112,"fill":126,"stroke":127,"style":115},"290.0","#e7ebef","var(--line,#cdd5e6)",[117,129,132],{"x":130,"y":120,"style":131},"380.0","font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","broker",[99,134],{"x":135,"y":109,"width":110,"height":111,"rx":112,"fill":126,"stroke":127,"style":115},"560.0",[117,137,139],{"x":138,"y":120,"style":131},"650.0","worker",[42,141],{"x1":119,"y1":142,"x2":119,"y2":143,"stroke":127,"style":144},"64.0","276.0","stroke-width:1.5px;stroke-dasharray:5 4",[42,146],{"x1":130,"y1":142,"x2":130,"y2":143,"stroke":127,"style":144},[42,148],{"x1":138,"y1":142,"x2":138,"y2":143,"stroke":127,"style":144},[42,150],{"x1":151,"y1":152,"x2":153,"y2":152,"stroke":114,"style":115},"115.0","104.0","371.0",[155,156],"polygon",{"points":157,"fill":158},"375.0,104.0 366.0,109.0 366.0,99.0","#5b5cf0",[99,160],{"x":161,"y":162,"width":163,"height":164,"rx":165,"fill":105,"stroke":127,"style":166},"113.4","75.0","263.2","21.0","10","stroke-width:1.2px",[117,168,172],{"x":169,"y":170,"style":171},"245.0","89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. Mon 07:00 → send build_weekly_report",[42,174],{"x1":175,"y1":176,"x2":177,"y2":176,"stroke":114,"style":115},"385.0","152.0","641.0",[155,179],{"points":180,"fill":158},"645.0,152.0 636.0,157.0 636.0,147.0",[99,182],{"x":183,"y":184,"width":185,"height":164,"rx":165,"fill":105,"stroke":127,"style":166},"457.9","123.0","114.2",[117,187,190],{"x":188,"y":189,"style":171},"515.0","137.5","2. task message",[42,192],{"x1":193,"y1":194,"x2":195,"y2":194,"stroke":114,"style":115},"645.0","200.0","389.0",[155,197],{"points":198,"fill":158},"385.0,200.0 394.0,195.0 394.0,205.0",[99,200],{"x":201,"y":202,"width":203,"height":164,"rx":165,"fill":105,"stroke":127,"style":166},"389.9","171.0","250.3",[117,205,207],{"x":188,"y":206,"style":171},"185.5","3. retry in 1 s, 2 s … on ConnectionError",[42,209],{"x1":193,"y1":210,"x2":195,"y2":210,"stroke":114,"style":115},"248.0",[155,212],{"points":213,"fill":158},"385.0,248.0 394.0,243.0 394.0,253.0",[99,215],{"x":216,"y":217,"width":218,"height":164,"rx":165,"fill":105,"stroke":127,"style":166},"415.4","219.0","199.1",[117,220,222],{"x":188,"y":221,"style":171},"233.5","4. result: path to the workbook",[25,224,226],{"id":225},"step-1-the-celery-app-and-its-time-zone","Step 1 — The Celery app and its time zone",[10,228,229,230,233],{},"Configure the time zone once on the app. Beat evaluates every ",[37,231,232],{},"crontab"," entry in that zone:",[30,235,239],{"className":236,"code":237,"language":238,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from celery import Celery\n\napp = Celery(\"reports\", broker=\"memory:\u002F\u002F\", backend=\"cache+memory:\u002F\u002F\")\napp.conf.update(\n    timezone=\"Europe\u002FLondon\",\n    enable_utc=True,\n    task_acks_late=True,\n    worker_prefetch_multiplier=1,\n)\nprint(app.conf.timezone, app.conf.enable_utc)\n","python",[37,240,241,257,263,302,308,322,336,348,361,366],{"__ignoreMap":35},[40,242,243,247,251,254],{"class":42,"line":43},[40,244,246],{"class":245},"s-kum","from",[40,248,250],{"class":249},"skGVy"," celery ",[40,252,253],{"class":245},"import",[40,255,256],{"class":249}," Celery\n",[40,258,259],{"class":42,"line":63},[40,260,262],{"emptyLinePlaceholder":261},true,"\n",[40,264,266,269,272,275,278,281,284,286,289,291,294,296,299],{"class":42,"line":265},3,[40,267,268],{"class":249},"app ",[40,270,271],{"class":245},"=",[40,273,274],{"class":249}," Celery(",[40,276,277],{"class":50},"\"reports\"",[40,279,280],{"class":249},", ",[40,282,132],{"class":283},"sa561",[40,285,271],{"class":245},[40,287,288],{"class":50},"\"memory:\u002F\u002F\"",[40,290,280],{"class":249},[40,292,293],{"class":283},"backend",[40,295,271],{"class":245},[40,297,298],{"class":50},"\"cache+memory:\u002F\u002F\"",[40,300,301],{"class":249},")\n",[40,303,305],{"class":42,"line":304},4,[40,306,307],{"class":249},"app.conf.update(\n",[40,309,311,314,316,319],{"class":42,"line":310},5,[40,312,313],{"class":283},"    timezone",[40,315,271],{"class":245},[40,317,318],{"class":50},"\"Europe\u002FLondon\"",[40,320,321],{"class":249},",\n",[40,323,325,328,330,334],{"class":42,"line":324},6,[40,326,327],{"class":283},"    enable_utc",[40,329,271],{"class":245},[40,331,333],{"class":332},"sP0c6","True",[40,335,321],{"class":249},[40,337,339,342,344,346],{"class":42,"line":338},7,[40,340,341],{"class":283},"    task_acks_late",[40,343,271],{"class":245},[40,345,333],{"class":332},[40,347,321],{"class":249},[40,349,351,354,356,359],{"class":42,"line":350},8,[40,352,353],{"class":283},"    worker_prefetch_multiplier",[40,355,271],{"class":245},[40,357,358],{"class":332},"1",[40,360,321],{"class":249},[40,362,364],{"class":42,"line":363},9,[40,365,301],{"class":249},[40,367,369,372],{"class":42,"line":368},10,[40,370,371],{"class":332},"print",[40,373,374],{"class":249},"(app.conf.timezone, app.conf.enable_utc)\n",[10,376,377,380,381,384,385,388,389,392,393,396,397,400],{},[37,378,379],{},"enable_utc=True"," keeps internal timestamps in UTC while ",[37,382,383],{},"timezone"," decides how schedules are read, so \"07:00\" means 07:00 in London all year. ",[37,386,387],{},"task_acks_late"," makes the worker acknowledge a task only after it finishes, so a report interrupted by a worker restart is delivered again rather than lost; ",[37,390,391],{},"worker_prefetch_multiplier=1"," stops one worker reserving several long reports at once. In Django these settings live in ",[37,394,395],{},"settings.py"," with a ",[37,398,399],{},"CELERY_"," prefix.",[25,402,404],{"id":403},"step-2-a-report-task-that-retries-and-stops","Step 2 — A report task that retries and stops",[10,406,407],{},"The task builds the workbook and returns where it saved it. Transient failures — the database restarting, a network share briefly unavailable — are retried automatically with exponential backoff; time limits stop a stuck run:",[30,409,411],{"className":236,"code":410,"language":238,"meta":35,"style":35},"from datetime import date\nfrom pathlib import Path\nimport pandas as pd\n\n@app.task(bind=True, autoretry_for=(ConnectionError, TimeoutError), retry_backoff=1,\n          retry_backoff_max=300, retry_jitter=False, max_retries=4,\n          soft_time_limit=600, time_limit=660)\ndef build_weekly_report(self, report_date: str | None = None) -> str:\n    report_date = date.fromisoformat(report_date) if report_date else date.today()\n    rows = load_sales(report_date)\n    out = Path(\"reports\") \u002F f\"weekly_sales_{report_date:%Y-%m-%d}.xlsx\"\n    out.parent.mkdir(exist_ok=True)\n    rows.to_excel(out, index=False)\n    return str(out)\n\nattempts = {\"n\": 0}\n\ndef load_sales(report_date):\n    attempts[\"n\"] += 1\n    if attempts[\"n\"] \u003C 3:\n        raise ConnectionError(\"database is restarting\")\n    return pd.read_csv(\"sales_source.csv\")\n\nprint(build_weekly_report.name, build_weekly_report.max_retries)\n",[37,412,413,425,437,450,454,499,530,552,585,607,617,672,687,702,714,719,741,746,757,774,795,811,824,829],{"__ignoreMap":35},[40,414,415,417,420,422],{"class":42,"line":43},[40,416,246],{"class":245},[40,418,419],{"class":249}," datetime ",[40,421,253],{"class":245},[40,423,424],{"class":249}," date\n",[40,426,427,429,432,434],{"class":42,"line":63},[40,428,246],{"class":245},[40,430,431],{"class":249}," pathlib ",[40,433,253],{"class":245},[40,435,436],{"class":249}," Path\n",[40,438,439,441,444,447],{"class":42,"line":265},[40,440,253],{"class":245},[40,442,443],{"class":249}," pandas ",[40,445,446],{"class":245},"as",[40,448,449],{"class":249}," pd\n",[40,451,452],{"class":42,"line":304},[40,453,262],{"emptyLinePlaceholder":261},[40,455,456,460,463,466,468,470,472,475,477,479,482,484,487,490,493,495,497],{"class":42,"line":310},[40,457,459],{"class":458},"s_Opv","@app.task",[40,461,462],{"class":249},"(",[40,464,465],{"class":283},"bind",[40,467,271],{"class":245},[40,469,333],{"class":332},[40,471,280],{"class":249},[40,473,474],{"class":283},"autoretry_for",[40,476,271],{"class":245},[40,478,462],{"class":249},[40,480,481],{"class":332},"ConnectionError",[40,483,280],{"class":249},[40,485,486],{"class":332},"TimeoutError",[40,488,489],{"class":249},"), ",[40,491,492],{"class":283},"retry_backoff",[40,494,271],{"class":245},[40,496,358],{"class":332},[40,498,321],{"class":249},[40,500,501,504,506,509,511,514,516,519,521,524,526,528],{"class":42,"line":324},[40,502,503],{"class":283},"          retry_backoff_max",[40,505,271],{"class":245},[40,507,508],{"class":332},"300",[40,510,280],{"class":249},[40,512,513],{"class":283},"retry_jitter",[40,515,271],{"class":245},[40,517,518],{"class":332},"False",[40,520,280],{"class":249},[40,522,523],{"class":283},"max_retries",[40,525,271],{"class":245},[40,527,101],{"class":332},[40,529,321],{"class":249},[40,531,532,535,537,540,542,545,547,550],{"class":42,"line":338},[40,533,534],{"class":283},"          soft_time_limit",[40,536,271],{"class":245},[40,538,539],{"class":332},"600",[40,541,280],{"class":249},[40,543,544],{"class":283},"time_limit",[40,546,271],{"class":245},[40,548,549],{"class":332},"660",[40,551,301],{"class":249},[40,553,554,557,560,563,566,569,572,575,577,580,582],{"class":42,"line":350},[40,555,556],{"class":245},"def",[40,558,559],{"class":458}," build_weekly_report",[40,561,562],{"class":249},"(self, report_date: ",[40,564,565],{"class":332},"str",[40,567,568],{"class":245}," |",[40,570,571],{"class":332}," None",[40,573,574],{"class":245}," =",[40,576,571],{"class":332},[40,578,579],{"class":249},") -> ",[40,581,565],{"class":332},[40,583,584],{"class":249},":\n",[40,586,587,590,592,595,598,601,604],{"class":42,"line":363},[40,588,589],{"class":249},"    report_date ",[40,591,271],{"class":245},[40,593,594],{"class":249}," date.fromisoformat(report_date) ",[40,596,597],{"class":245},"if",[40,599,600],{"class":249}," report_date ",[40,602,603],{"class":245},"else",[40,605,606],{"class":249}," date.today()\n",[40,608,609,612,614],{"class":42,"line":368},[40,610,611],{"class":249},"    rows ",[40,613,271],{"class":245},[40,615,616],{"class":249}," load_sales(report_date)\n",[40,618,620,623,625,628,630,633,636,639,642,646,649,652,655,658,661,663,666,669],{"class":42,"line":619},11,[40,621,622],{"class":249},"    out ",[40,624,271],{"class":245},[40,626,627],{"class":249}," Path(",[40,629,277],{"class":50},[40,631,632],{"class":249},") ",[40,634,635],{"class":245},"\u002F",[40,637,638],{"class":245}," f",[40,640,641],{"class":50},"\"weekly_sales_",[40,643,645],{"class":644},"sSjpA","{",[40,647,648],{"class":249},"report_date:",[40,650,651],{"class":245},"%",[40,653,654],{"class":249},"Y",[40,656,657],{"class":245},"-%",[40,659,660],{"class":249},"m",[40,662,657],{"class":245},[40,664,665],{"class":249},"d",[40,667,668],{"class":644},"}",[40,670,671],{"class":50},".xlsx\"\n",[40,673,675,678,681,683,685],{"class":42,"line":674},12,[40,676,677],{"class":249},"    out.parent.mkdir(",[40,679,680],{"class":283},"exist_ok",[40,682,271],{"class":245},[40,684,333],{"class":332},[40,686,301],{"class":249},[40,688,690,693,696,698,700],{"class":42,"line":689},13,[40,691,692],{"class":249},"    rows.to_excel(out, ",[40,694,695],{"class":283},"index",[40,697,271],{"class":245},[40,699,518],{"class":332},[40,701,301],{"class":249},[40,703,705,708,711],{"class":42,"line":704},14,[40,706,707],{"class":245},"    return",[40,709,710],{"class":332}," str",[40,712,713],{"class":249},"(out)\n",[40,715,717],{"class":42,"line":716},15,[40,718,262],{"emptyLinePlaceholder":261},[40,720,722,725,727,730,733,736,738],{"class":42,"line":721},16,[40,723,724],{"class":249},"attempts ",[40,726,271],{"class":245},[40,728,729],{"class":249}," {",[40,731,732],{"class":50},"\"n\"",[40,734,735],{"class":249},": ",[40,737,102],{"class":332},[40,739,740],{"class":249},"}\n",[40,742,744],{"class":42,"line":743},17,[40,745,262],{"emptyLinePlaceholder":261},[40,747,749,751,754],{"class":42,"line":748},18,[40,750,556],{"class":245},[40,752,753],{"class":458}," load_sales",[40,755,756],{"class":249},"(report_date):\n",[40,758,760,763,765,768,771],{"class":42,"line":759},19,[40,761,762],{"class":249},"    attempts[",[40,764,732],{"class":50},[40,766,767],{"class":249},"] ",[40,769,770],{"class":245},"+=",[40,772,773],{"class":332}," 1\n",[40,775,777,780,783,785,787,790,793],{"class":42,"line":776},20,[40,778,779],{"class":245},"    if",[40,781,782],{"class":249}," attempts[",[40,784,732],{"class":50},[40,786,767],{"class":249},[40,788,789],{"class":245},"\u003C",[40,791,792],{"class":332}," 3",[40,794,584],{"class":249},[40,796,798,801,804,806,809],{"class":42,"line":797},21,[40,799,800],{"class":245},"        raise",[40,802,803],{"class":332}," ConnectionError",[40,805,462],{"class":249},[40,807,808],{"class":50},"\"database is restarting\"",[40,810,301],{"class":249},[40,812,814,816,819,822],{"class":42,"line":813},22,[40,815,707],{"class":245},[40,817,818],{"class":249}," pd.read_csv(",[40,820,821],{"class":50},"\"sales_source.csv\"",[40,823,301],{"class":249},[40,825,827],{"class":42,"line":826},23,[40,828,262],{"emptyLinePlaceholder":261},[40,830,832,834],{"class":42,"line":831},24,[40,833,371],{"class":332},[40,835,836],{"class":249},"(build_weekly_report.name, build_weekly_report.max_retries)\n",[10,838,839,841,842,845,846,849,850,853,854,856,857,860,861,863,864,867],{},[37,840,474],{}," lists the exceptions worth retrying; anything else — a ",[37,843,844],{},"KeyError"," from a renamed column — fails at once, because retrying cannot fix it. ",[37,847,848],{},"retry_backoff=1"," waits 1, 2, 4, 8 seconds between attempts, capped at ",[37,851,852],{},"retry_backoff_max","; in production leave ",[37,855,513],{}," on so many failing tasks do not retry in lockstep. ",[37,858,859],{},"soft_time_limit"," raises an exception inside the task after ten minutes so it can clean up, and ",[37,862,544],{}," kills it a minute later if it does not. The task takes the report date as an optional ISO string — task arguments travel as JSON, and a string survives that where a ",[37,865,866],{},"date"," object would not.",[25,869,871],{"id":870},"step-3-test-it-with-a-real-worker","Step 3 — Test it with a real worker",[10,873,874,877],{},[37,875,876],{},"celery.contrib.testing.worker.start_worker"," runs a worker in a background thread against the app's broker, so the test goes through serialisation, the queue and the retry machinery — not just a function call:",[30,879,881],{"className":236,"code":880,"language":238,"meta":35,"style":35},"import time\nfrom celery.contrib.testing.worker import start_worker\n\nstarted = time.perf_counter()\nwith start_worker(app, perform_ping_check=False, pool=\"solo\", loglevel=\"WARNING\"):\n    result = build_weekly_report.delay(\"2026-09-28\")\n    path = result.get(timeout=30)\nprint(path, \"after\", attempts[\"n\"], \"attempts in\", f\"{time.perf_counter() - started:.1f}s\")\nprint(pd.read_excel(path)[\"Region\"].tolist())\n",[37,882,883,890,902,906,916,954,969,989,1039],{"__ignoreMap":35},[40,884,885,887],{"class":42,"line":43},[40,886,253],{"class":245},[40,888,889],{"class":249}," time\n",[40,891,892,894,897,899],{"class":42,"line":63},[40,893,246],{"class":245},[40,895,896],{"class":249}," celery.contrib.testing.worker ",[40,898,253],{"class":245},[40,900,901],{"class":249}," start_worker\n",[40,903,904],{"class":42,"line":265},[40,905,262],{"emptyLinePlaceholder":261},[40,907,908,911,913],{"class":42,"line":304},[40,909,910],{"class":249},"started ",[40,912,271],{"class":245},[40,914,915],{"class":249}," time.perf_counter()\n",[40,917,918,921,924,927,929,931,933,936,938,941,943,946,948,951],{"class":42,"line":310},[40,919,920],{"class":245},"with",[40,922,923],{"class":249}," start_worker(app, ",[40,925,926],{"class":283},"perform_ping_check",[40,928,271],{"class":245},[40,930,518],{"class":332},[40,932,280],{"class":249},[40,934,935],{"class":283},"pool",[40,937,271],{"class":245},[40,939,940],{"class":50},"\"solo\"",[40,942,280],{"class":249},[40,944,945],{"class":283},"loglevel",[40,947,271],{"class":245},[40,949,950],{"class":50},"\"WARNING\"",[40,952,953],{"class":249},"):\n",[40,955,956,959,961,964,967],{"class":42,"line":324},[40,957,958],{"class":249},"    result ",[40,960,271],{"class":245},[40,962,963],{"class":249}," build_weekly_report.delay(",[40,965,966],{"class":50},"\"2026-09-28\"",[40,968,301],{"class":249},[40,970,971,974,976,979,982,984,987],{"class":42,"line":338},[40,972,973],{"class":249},"    path ",[40,975,271],{"class":245},[40,977,978],{"class":249}," result.get(",[40,980,981],{"class":283},"timeout",[40,983,271],{"class":245},[40,985,986],{"class":332},"30",[40,988,301],{"class":249},[40,990,991,993,996,999,1002,1004,1007,1010,1012,1015,1018,1020,1023,1026,1029,1032,1034,1037],{"class":42,"line":350},[40,992,371],{"class":332},[40,994,995],{"class":249},"(path, ",[40,997,998],{"class":50},"\"after\"",[40,1000,1001],{"class":249},", attempts[",[40,1003,732],{"class":50},[40,1005,1006],{"class":249},"], ",[40,1008,1009],{"class":50},"\"attempts in\"",[40,1011,280],{"class":249},[40,1013,1014],{"class":245},"f",[40,1016,1017],{"class":50},"\"",[40,1019,645],{"class":644},[40,1021,1022],{"class":249},"time.perf_counter() ",[40,1024,1025],{"class":245},"-",[40,1027,1028],{"class":249}," started",[40,1030,1031],{"class":245},":.1f",[40,1033,668],{"class":644},[40,1035,1036],{"class":50},"s\"",[40,1038,301],{"class":249},[40,1040,1041,1043,1046,1049],{"class":42,"line":363},[40,1042,371],{"class":332},[40,1044,1045],{"class":249},"(pd.read_excel(path)[",[40,1047,1048],{"class":50},"\"Region\"",[40,1050,1051],{"class":249},"].tolist())\n",[10,1053,1054,1055,1058,1059,1062],{},"The first two attempts failed with the simulated database error, Celery retried after one and then two seconds, and the third attempt built the workbook — a few seconds in all, three of them spent in the backoff waits. This is the behaviour a deployed worker shows, which a plain function call cannot demonstrate. In a pytest suite the same pattern works with the ",[37,1056,1057],{},"celery_app"," and ",[37,1060,1061],{},"celery_worker"," fixtures that Celery provides.",[80,1064,89,1069,89,1072,89,1075,89,1078,89,1085,89,1091,89,1102,89,1108,89,1113,89,1117,89,1121,89,1125,89,1130,89,1134,89,1138,89,1140,89,1142,89,1147,89,1154,89,1159,89,1162,89,1164],{"viewBox":1065,"role":83,"ariaLabelledBy":1066,"xmlns":87,"style":88},"4 0 752 279",[1067,1068],"cb-retries-t","cb-retries-d",[91,1070,1071],{"id":1067},"One task, three attempts",[95,1073,1074],{"id":1068},"A timeline of the tested task. The first attempt fails at 0 seconds, the second after a 1 second backoff fails again, and the third after a further 2 seconds succeeds and writes the workbook.",[99,1076],{"x":101,"y":102,"width":103,"height":1077,"fill":105},"279",[42,1079],{"x1":1080,"y1":1081,"x2":1082,"y2":1081,"stroke":1083,"style":1084},"96.0","139.5","664.0","var(--muted,#5b6780)","stroke-width:2.5px",[99,1086],{"x":108,"y":109,"width":1087,"height":1088,"rx":112,"fill":1089,"stroke":1090,"style":115},"176.0","91.5","#fce9e9","var(--danger,#dc2626)",[1092,1093,1095],"g",{"transform":1094},"translate(96.0,29.0) scale(1.000)",[1096,1097],"path",{"d":1098,"fill":1099,"stroke":1100,"style":1101},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","none","var(--accent-ink,#be185d)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[117,1103,1107],{"x":1104,"y":1105,"style":1106},"108.0","74.5","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","attempt 1",[117,1109,1112],{"x":1104,"y":1110,"style":1111},"93.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","0 s — ConnectionError",[42,1114],{"x1":1104,"y1":1115,"x2":1104,"y2":1081,"stroke":1083,"style":1116},"107.5","stroke-width:1.5px",[1118,1119],"circle",{"cx":1104,"cy":1081,"r":1120,"fill":1090,"stroke":105,"style":115},"7.0",[99,1122],{"x":1123,"y":1124,"width":1087,"height":1088,"rx":112,"fill":1089,"stroke":1090,"style":115},"292.0","171.5",[1092,1126,1128],{"transform":1127},"translate(368.0,184.5) scale(1.000)",[1096,1129],{"d":1098,"fill":1099,"stroke":1100,"style":1101},[117,1131,1133],{"x":130,"y":1132,"style":1106},"230.0","attempt 2",[117,1135,1137],{"x":130,"y":1136,"style":1111},"248.5","+1 s — ConnectionError",[42,1139],{"x1":130,"y1":1081,"x2":130,"y2":1124,"stroke":1083,"style":1116},[1118,1141],{"cx":130,"cy":1081,"r":1120,"fill":1090,"stroke":105,"style":115},[99,1143],{"x":1144,"y":109,"width":1087,"height":1088,"rx":112,"fill":1145,"stroke":1146,"style":115},"564.0","#d9f4f1","var(--teal,#0f9488)",[1092,1148,1150],{"transform":1149},"translate(640.0,29.0) scale(1.000)",[1096,1151],{"d":1152,"fill":1099,"stroke":1153,"style":1101},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[117,1155,1158],{"x":1156,"y":1105,"style":1157},"652.0","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","attempt 3",[117,1160,1161],{"x":1156,"y":1110,"style":1111},"+2 s — workbook written",[42,1163],{"x1":1156,"y1":1115,"x2":1156,"y2":1081,"stroke":1083,"style":1116},[1118,1165],{"cx":1156,"cy":1081,"r":1120,"fill":1146,"stroke":105,"style":115},[25,1167,1169],{"id":1168},"step-4-the-beat-schedule","Step 4 — The beat schedule",[10,1171,1172,1173,1176,1177,1179],{},"Beat reads ",[37,1174,1175],{},"beat_schedule",": a dictionary of entries, each naming a task, a schedule and optional arguments. ",[37,1178,232],{}," takes the familiar fields by name:",[30,1181,1183],{"className":236,"code":1182,"language":238,"meta":35,"style":35},"from celery.schedules import crontab\n\napp.conf.beat_schedule = {\n    \"weekly-sales-monday-0700\": {\n        \"task\": build_weekly_report.name,\n        \"schedule\": crontab(minute=0, hour=7, day_of_week=\"mon\"),\n        \"options\": {\"expires\": 3 * 3600},\n    },\n    \"daily-stock-weekdays-0630\": {\n        \"task\": build_weekly_report.name,\n        \"schedule\": crontab(minute=30, hour=6, day_of_week=\"mon-fri\"),\n        \"args\": [None],\n    },\n}\n\nfrom datetime import datetime\nfrom zoneinfo import ZoneInfo\n\nschedule = app.conf.beat_schedule[\"weekly-sales-monday-0700\"][\"schedule\"]\nschedule.app = app\nlast = datetime(2026, 10, 19, 7, 0, tzinfo=ZoneInfo(\"Europe\u002FLondon\"))\nfor _ in range(3):\n    start, delta, _now = schedule.remaining_delta(last)\n    last = start + delta\n    print(last.astimezone(ZoneInfo(\"Europe\u002FLondon\")).strftime(\"%a %d %b %H:%M %Z\"))\n",[37,1184,1185,1197,1201,1211,1219,1227,1265,1290,1295,1302,1308,1340,1354,1358,1362,1366,1377,1389,1393,1415,1425,1470,1490,1500,1516],{"__ignoreMap":35},[40,1186,1187,1189,1192,1194],{"class":42,"line":43},[40,1188,246],{"class":245},[40,1190,1191],{"class":249}," celery.schedules ",[40,1193,253],{"class":245},[40,1195,1196],{"class":249}," crontab\n",[40,1198,1199],{"class":42,"line":63},[40,1200,262],{"emptyLinePlaceholder":261},[40,1202,1203,1206,1208],{"class":42,"line":265},[40,1204,1205],{"class":249},"app.conf.beat_schedule ",[40,1207,271],{"class":245},[40,1209,1210],{"class":249}," {\n",[40,1212,1213,1216],{"class":42,"line":304},[40,1214,1215],{"class":50},"    \"weekly-sales-monday-0700\"",[40,1217,1218],{"class":249},": {\n",[40,1220,1221,1224],{"class":42,"line":310},[40,1222,1223],{"class":50},"        \"task\"",[40,1225,1226],{"class":249},": build_weekly_report.name,\n",[40,1228,1229,1232,1235,1238,1240,1242,1244,1247,1249,1252,1254,1257,1259,1262],{"class":42,"line":324},[40,1230,1231],{"class":50},"        \"schedule\"",[40,1233,1234],{"class":249},": crontab(",[40,1236,1237],{"class":283},"minute",[40,1239,271],{"class":245},[40,1241,102],{"class":332},[40,1243,280],{"class":249},[40,1245,1246],{"class":283},"hour",[40,1248,271],{"class":245},[40,1250,1251],{"class":332},"7",[40,1253,280],{"class":249},[40,1255,1256],{"class":283},"day_of_week",[40,1258,271],{"class":245},[40,1260,1261],{"class":50},"\"mon\"",[40,1263,1264],{"class":249},"),\n",[40,1266,1267,1270,1273,1276,1278,1281,1284,1287],{"class":42,"line":338},[40,1268,1269],{"class":50},"        \"options\"",[40,1271,1272],{"class":249},": {",[40,1274,1275],{"class":50},"\"expires\"",[40,1277,735],{"class":249},[40,1279,1280],{"class":332},"3",[40,1282,1283],{"class":245}," *",[40,1285,1286],{"class":332}," 3600",[40,1288,1289],{"class":249},"},\n",[40,1291,1292],{"class":42,"line":350},[40,1293,1294],{"class":249},"    },\n",[40,1296,1297,1300],{"class":42,"line":363},[40,1298,1299],{"class":50},"    \"daily-stock-weekdays-0630\"",[40,1301,1218],{"class":249},[40,1303,1304,1306],{"class":42,"line":368},[40,1305,1223],{"class":50},[40,1307,1226],{"class":249},[40,1309,1310,1312,1314,1316,1318,1320,1322,1324,1326,1329,1331,1333,1335,1338],{"class":42,"line":619},[40,1311,1231],{"class":50},[40,1313,1234],{"class":249},[40,1315,1237],{"class":283},[40,1317,271],{"class":245},[40,1319,986],{"class":332},[40,1321,280],{"class":249},[40,1323,1246],{"class":283},[40,1325,271],{"class":245},[40,1327,1328],{"class":332},"6",[40,1330,280],{"class":249},[40,1332,1256],{"class":283},[40,1334,271],{"class":245},[40,1336,1337],{"class":50},"\"mon-fri\"",[40,1339,1264],{"class":249},[40,1341,1342,1345,1348,1351],{"class":42,"line":674},[40,1343,1344],{"class":50},"        \"args\"",[40,1346,1347],{"class":249},": [",[40,1349,1350],{"class":332},"None",[40,1352,1353],{"class":249},"],\n",[40,1355,1356],{"class":42,"line":689},[40,1357,1294],{"class":249},[40,1359,1360],{"class":42,"line":704},[40,1361,740],{"class":249},[40,1363,1364],{"class":42,"line":716},[40,1365,262],{"emptyLinePlaceholder":261},[40,1367,1368,1370,1372,1374],{"class":42,"line":721},[40,1369,246],{"class":245},[40,1371,419],{"class":249},[40,1373,253],{"class":245},[40,1375,1376],{"class":249}," datetime\n",[40,1378,1379,1381,1384,1386],{"class":42,"line":743},[40,1380,246],{"class":245},[40,1382,1383],{"class":249}," zoneinfo ",[40,1385,253],{"class":245},[40,1387,1388],{"class":249}," ZoneInfo\n",[40,1390,1391],{"class":42,"line":748},[40,1392,262],{"emptyLinePlaceholder":261},[40,1394,1395,1398,1400,1403,1406,1409,1412],{"class":42,"line":759},[40,1396,1397],{"class":249},"schedule ",[40,1399,271],{"class":245},[40,1401,1402],{"class":249}," app.conf.beat_schedule[",[40,1404,1405],{"class":50},"\"weekly-sales-monday-0700\"",[40,1407,1408],{"class":249},"][",[40,1410,1411],{"class":50},"\"schedule\"",[40,1413,1414],{"class":249},"]\n",[40,1416,1417,1420,1422],{"class":42,"line":776},[40,1418,1419],{"class":249},"schedule.app ",[40,1421,271],{"class":245},[40,1423,1424],{"class":249}," app\n",[40,1426,1427,1430,1432,1435,1438,1440,1442,1444,1447,1449,1451,1453,1455,1457,1460,1462,1465,1467],{"class":42,"line":797},[40,1428,1429],{"class":249},"last ",[40,1431,271],{"class":245},[40,1433,1434],{"class":249}," datetime(",[40,1436,1437],{"class":332},"2026",[40,1439,280],{"class":249},[40,1441,165],{"class":332},[40,1443,280],{"class":249},[40,1445,1446],{"class":332},"19",[40,1448,280],{"class":249},[40,1450,1251],{"class":332},[40,1452,280],{"class":249},[40,1454,102],{"class":332},[40,1456,280],{"class":249},[40,1458,1459],{"class":283},"tzinfo",[40,1461,271],{"class":245},[40,1463,1464],{"class":249},"ZoneInfo(",[40,1466,318],{"class":50},[40,1468,1469],{"class":249},"))\n",[40,1471,1472,1475,1478,1481,1484,1486,1488],{"class":42,"line":813},[40,1473,1474],{"class":245},"for",[40,1476,1477],{"class":249}," _ ",[40,1479,1480],{"class":245},"in",[40,1482,1483],{"class":332}," range",[40,1485,462],{"class":249},[40,1487,1280],{"class":332},[40,1489,953],{"class":249},[40,1491,1492,1495,1497],{"class":42,"line":826},[40,1493,1494],{"class":249},"    start, delta, _now ",[40,1496,271],{"class":245},[40,1498,1499],{"class":249}," schedule.remaining_delta(last)\n",[40,1501,1502,1505,1507,1510,1513],{"class":42,"line":831},[40,1503,1504],{"class":249},"    last ",[40,1506,271],{"class":245},[40,1508,1509],{"class":249}," start ",[40,1511,1512],{"class":245},"+",[40,1514,1515],{"class":249}," delta\n",[40,1517,1519,1522,1525,1527,1530,1532,1535,1538,1541,1544],{"class":42,"line":1518},25,[40,1520,1521],{"class":332},"    print",[40,1523,1524],{"class":249},"(last.astimezone(ZoneInfo(",[40,1526,318],{"class":50},[40,1528,1529],{"class":249},")).strftime(",[40,1531,1017],{"class":50},[40,1533,1534],{"class":644},"%a",[40,1536,1537],{"class":644}," %d",[40,1539,1540],{"class":644}," %b",[40,1542,1543],{"class":50}," %H:%M %Z\"",[40,1545,1469],{"class":249},[10,1547,1548,1551,1552,1555,1556,1559,1560,1563],{},[37,1549,1550],{},"remaining_delta"," is what beat uses to decide when an entry is next due; computing a few runs from it confirms the entry means Monday at 07:00 London time, before and after the clocks change on 25 October. ",[37,1553,1554],{},"expires"," discards the message if no worker has picked it up within three hours, so a backlog after an outage does not produce a stale Monday report on Wednesday. Start beat as its own process in production with ",[37,1557,1558],{},"celery -A reports beat",", next to the workers started with ",[37,1561,1562],{},"celery -A reports worker",". Beat only sends messages; it never runs the report itself, so it needs almost no resources.",[25,1565,1567],{"id":1566},"step-5-run-exactly-one-beat","Step 5 — Run exactly one beat",[10,1569,1570],{},"Beat has no built-in coordination: every beat process that is running sends every scheduled task. Two beats — one per web container, or an old one left running after a deployment — means every report is built and emailed twice. Run beat as a single dedicated process or container, separate from the scalable workers, and make sure deployments stop the old one before starting the new one:",[30,1572,1574],{"className":32,"code":1573,"language":34,"meta":35,"style":35},"celery -A reports worker --loglevel=INFO --concurrency=2     # scale these as needed\ncelery -A reports beat --loglevel=INFO                        # exactly one of these\n",[37,1575,1576,1599],{"__ignoreMap":35},[40,1577,1578,1581,1584,1587,1590,1593,1596],{"class":42,"line":43},[40,1579,1580],{"class":46},"celery",[40,1582,1583],{"class":332}," -A",[40,1585,1586],{"class":50}," reports",[40,1588,1589],{"class":50}," worker",[40,1591,1592],{"class":332}," --loglevel=INFO",[40,1594,1595],{"class":332}," --concurrency=2",[40,1597,1598],{"class":66},"     # scale these as needed\n",[40,1600,1601,1603,1605,1607,1610,1612],{"class":42,"line":63},[40,1602,1580],{"class":46},[40,1604,1583],{"class":332},[40,1606,1586],{"class":50},[40,1608,1609],{"class":50}," beat",[40,1611,1592],{"class":332},[40,1613,1614],{"class":66},"                        # exactly one of these\n",[10,1616,1617,1618,1621,1622,1625,1626,1630],{},"For schedules that non-developers should change without a deployment, ",[37,1619,1620],{},"django-celery-beat"," stores entries in the database with an admin interface; beat is started with its ",[37,1623,1624],{},"DatabaseScheduler",", and the one-beat rule still applies. Making the report task idempotent — writing to a dated file name and checking whether today's report already went out before emailing — turns an accidental duplicate into a harmless no-op, as described in ",[19,1627,1629],{"href":1628},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002F","Prevent Overlapping Runs of an Excel Report Job",".",[80,1632,89,1637,89,1640,89,1643,89,1646,89,1651,89,1657,89,1663,89,1669,89,1675,89,1681,89,1685,89,1690,89,1694,89,1699,89,1701,89,1706,89,1711,89,1715,89,1719,89,1726,89,1729,89,1734,89,1737],{"viewBox":1633,"role":83,"ariaLabelledBy":1634,"xmlns":87,"style":88},"4 0 752 207",[1635,1636],"cb-beats-t","cb-beats-d",[91,1638,1639],{"id":1635},"One beat or two",[95,1641,1642],{"id":1636},"Two panels. With one beat process every scheduled task is sent once. With two beat processes, for example one per web container, every scheduled task is sent twice and reports are duplicated.",[99,1644],{"x":101,"y":102,"width":103,"height":1645,"fill":105},"207",[99,1647],{"x":108,"y":109,"width":1648,"height":1649,"rx":1650,"fill":1145,"stroke":1146,"style":115},"351.0","175.0","14",[1092,1652,1654],{"transform":1653},"translate(182.5,29.0) scale(1.083)",[1096,1655],{"d":1152,"fill":1099,"stroke":1153,"style":1656},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[117,1658,1662],{"x":1659,"y":1660,"style":1661},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","One beat",[42,1664],{"x1":1665,"y1":1666,"x2":1667,"y2":1666,"stroke":1146,"style":1668},"36.0","91.0","355.0","stroke-width:1px",[117,1670,1674],{"x":1671,"y":1672,"style":1673},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","each entry sent once",[1092,1676,1678],{"transform":1677},"translate(38.5,99.5) scale(0.625)",[1096,1679],{"d":1152,"fill":1099,"stroke":1146,"style":1680},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[117,1682,1684],{"x":1671,"y":1683,"style":1673},"137.0","workers scale freely",[1092,1686,1688],{"transform":1687},"translate(38.5,125.5) scale(0.625)",[1096,1689],{"d":1152,"fill":1099,"stroke":1146,"style":1680},[117,1691,1693],{"x":1671,"y":1692,"style":1673},"163.0","duplicates impossible by design",[1092,1695,1697],{"transform":1696},"translate(38.5,151.5) scale(0.625)",[1096,1698],{"d":1152,"fill":1099,"stroke":1146,"style":1680},[99,1700],{"x":195,"y":109,"width":1648,"height":1649,"rx":1650,"fill":1089,"stroke":1090,"style":115},[1092,1702,1704],{"transform":1703},"translate(551.5,29.0) scale(1.083)",[1096,1705],{"d":1098,"fill":1099,"stroke":1100,"style":1656},[117,1707,1710],{"x":1708,"y":1660,"style":1709},"564.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","Two beats",[42,1712],{"x1":1713,"y1":1666,"x2":1714,"y2":1666,"stroke":1090,"style":1668},"405.0","724.0",[117,1716,1718],{"x":1717,"y":1672,"style":1673},"429.0","each entry sent twice",[1092,1720,1722],{"transform":1721},"translate(407.5,99.5) scale(0.625)",[1096,1723],{"d":1724,"fill":1099,"stroke":1725,"style":1680},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)",[117,1727,1728],{"x":1717,"y":1683,"style":1673},"reports emailed twice",[1092,1730,1732],{"transform":1731},"translate(407.5,125.5) scale(0.625)",[1096,1733],{"d":1724,"fill":1099,"stroke":1725,"style":1680},[117,1735,1736],{"x":1717,"y":1692,"style":1673},"hard to spot in logs",[1092,1738,1740],{"transform":1739},"translate(407.5,151.5) scale(0.625)",[1096,1741],{"d":1724,"fill":1099,"stroke":1725,"style":1680},[25,1743,1745],{"id":1744},"performance-notes","Performance notes",[10,1747,1748,1749,1752],{},"Workbook building is CPU-bound and can take minutes for large reports, which is long for a worker that also serves quick web tasks. Route report tasks to their own queue — ",[37,1750,1751],{},"task_routes = {\"reports.build_weekly_report\": {\"queue\": \"reports\"}}"," — and run a separate worker for that queue with a concurrency of one or two, so a Monday-morning batch of reports cannot delay password-reset emails. Keep the result small: return the path or the object-storage key of the workbook, never the workbook bytes, which would be stored in the result backend for every run.",[25,1754,1756],{"id":1755},"common-pitfalls","Common pitfalls",[1758,1759,1760,1768,1777,1783,1789],"ul",{},[1761,1762,1763,1767],"li",{},[1764,1765,1766],"strong",{},"More than one beat."," Every beat sends every task; run exactly one.",[1761,1769,1770,1773,1774,1776],{},[1764,1771,1772],{},"Retrying every exception."," List transient exceptions in ",[37,1775,474],{}," and let data errors fail immediately.",[1761,1778,1779,1782],{},[1764,1780,1781],{},"Passing dates and DataFrames as arguments."," Task arguments are serialised; pass ISO strings and IDs.",[1761,1784,1785,1788],{},[1764,1786,1787],{},"No time limits."," A hung report holds a worker forever; set soft and hard limits.",[1761,1790,1791,1794,1795,1797],{},[1764,1792,1793],{},"Forgetting the time zone."," Without ",[37,1796,383],{},", crontab entries are read in UTC.",[25,1799,1801],{"id":1800},"conclusion","Conclusion",[10,1803,1804,1805,1807,1808,1810,1811,1814,1815,1817,1818,1821],{},"Set the Celery app's ",[37,1806,383],{},", write the report as a task with ",[37,1809,474],{}," on transient errors, exponential backoff, soft and hard time limits and a date string argument, and test it through a real in-process worker with ",[37,1812,1813],{},"start_worker",". Declare the schedule in ",[37,1816,1175],{}," with ",[37,1819,1820],{},"crontab(minute=0, hour=7, day_of_week=\"mon\")",", check its next runs, give messages an expiry, and run exactly one beat process alongside the workers.",[25,1823,1825],{"id":1824},"frequently-asked-questions","Frequently asked questions",[10,1827,1828,1831],{},[1764,1829,1830],{},"How do I schedule a task every Monday at 7am with Celery?","\nAdd an entry to app.conf.beat_schedule with \"schedule\": crontab(minute=0, hour=7, day_of_week=\"mon\") and the task's name, set app.conf.timezone to your zone, and run celery beat alongside the workers.",[10,1833,1834,1837],{},[1764,1835,1836],{},"Why is my Celery beat task running twice?","\nMore than one beat process is running — for example one started in every web container. Each beat sends every scheduled task, so run exactly one.",[10,1839,1840,1843],{},[1764,1841,1842],{},"How do I make a Celery task retry automatically?","\nPass autoretry_for with the exception types to retry, plus retry_backoff and max_retries, to the task decorator. Celery then re-queues the task with increasing delays when one of those exceptions is raised.",[10,1845,1846,1849],{},[1764,1847,1848],{},"How can I test a Celery task without a broker?","\nUse Celery's in-memory broker and the start_worker helper from celery.contrib.testing.worker, which runs a real worker thread in the test process. In pytest, the celery_app and celery_worker fixtures do the same.",[25,1851,1853],{"id":1852},"related","Related",[1758,1855,1856,1862,1869,1876],{},[1761,1857,1858,1859,1861],{},"Up one level: ",[19,1860,22],{"href":21}," — every way to run reports on a schedule.",[1761,1863,1864,1868],{},[19,1865,1867],{"href":1866},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fgenerate-large-excel-exports-in-a-background-job\u002F","Generate Large Excel Exports in a Background Job"," — the same Celery set-up for on-demand exports.",[1761,1870,1871,1875],{},[19,1872,1874],{"href":1873},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002F","Schedule Recurring Excel Reports with APScheduler"," — scheduling inside a single process.",[1761,1877,1878,1882],{},[19,1879,1881],{"href":1880},"\u002Fautomating-reporting-workflows\u002Fserving-excel-files-from-python-web-apps\u002Fexport-a-django-queryset-to-excel\u002F","Export a Django Queryset to Excel"," — building the workbook from application data.",[1884,1885,1886],"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 pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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}",{"title":35,"searchDepth":63,"depth":63,"links":1888},[1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899],{"id":27,"depth":63,"text":28},{"id":225,"depth":63,"text":226},{"id":403,"depth":63,"text":404},{"id":870,"depth":63,"text":871},{"id":1168,"depth":63,"text":1169},{"id":1566,"depth":63,"text":1567},{"id":1744,"depth":63,"text":1745},{"id":1755,"depth":63,"text":1756},{"id":1800,"depth":63,"text":1801},{"id":1824,"depth":63,"text":1825},{"id":1852,"depth":63,"text":1853},"2026-09-29","Run recurring Excel reports inside an application that already uses Celery: a report task with automatic retries and time limits, a beat schedule with crontab entries in the right time zone, the rule that exactly one beat process may run, testing the task with a real in-process worker, and database-backed schedules people can edit.","md",[1904,1906,1908,1910],{"q":1830,"a":1905},"Add an entry to app.conf.beat_schedule with \"schedule\": crontab(minute=0, hour=7, day_of_week=\"mon\") and the task's name, set app.conf.timezone to your zone, and run celery beat alongside the workers.",{"q":1836,"a":1907},"More than one beat process is running — for example one started in every web container. Each beat sends every scheduled task, so run exactly one.",{"q":1842,"a":1909},"Pass autoretry_for with the exception types to retry, plus retry_backoff and max_retries, to the task decorator. Celery then re-queues the task with increasing delays when one of those exceptions is raised.",{"q":1848,"a":1911},"Use Celery's in-memory broker and the start_worker helper from celery.contrib.testing.worker, which runs a real worker thread in the test process. In pytest, the celery_app and celery_worker fixtures do the same.","schedule excel reports with celery beat",null,{"breadcrumb":1915},[1916,1918,1921],{"name":1917,"item":635},"Home",{"name":1919,"item":1920},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat",{"title":1924,"description":1925},"Schedule Python Excel Reports with Celery Beat and crontab","Schedule Excel report tasks with Celery beat: beat_schedule and crontab(day_of_week=\"mon\"), timezone and enable_utc, autoretry_for with retry_backoff, soft and hard time limits, acks_late, one beat instance only, and testing with celery.contrib.testing start_worker.","schedule-excel-reports-with-celery-beat","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-excel-reports-with-celery-beat\u002Findex",[1929,1930],"scheduling","web-apps","how-to","10ZoV_YM8oqRAdmjk0sGJ6lsNWIVevQ9yLokMTUpw1A",[1934,1938],{"title":1935,"path":1936,"stem":1937,"children":-1},"Schedule an Excel Report with systemd Timers","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-an-excel-report-with-systemd-timers\u002Findex",{"title":1874,"path":1939,"stem":1940,"children":-1},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fschedule-recurring-excel-reports-with-apscheduler\u002Findex",[1942,1944],{"path":1943,"title":1919},"\u002Fautomating-reporting-workflows",{"path":1945,"title":22},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[1948,1951,1954,1957,1960,1963],{"path":1949,"title":1950,"kind":1931},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server","Download New Excel Files from an SFTP Server",{"path":1952,"title":1953,"kind":1931},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fread-excel-attachments-into-a-database-nightly","Read Excel Attachments into a Database Nightly",{"path":1955,"title":1956,"kind":1931},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule","Refresh an Excel Report from a Database on a Schedule",{"path":1958,"title":1959,"kind":1931},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frun-a-report-when-an-excel-file-changes-in-onedrive","Run a Report When an Excel File Changes in OneDrive",{"path":1961,"title":1962,"kind":1931},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-report-emails-at-each-recipients-local-time","Send Report Emails at Each Recipient's Local Time",{"path":1964,"title":1965,"kind":1931},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",1790972140122]