[{"data":1,"prerenderedAt":970},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ffailing-fast-with-pytest-timeout\u002F":3},{"id":4,"title":5,"body":6,"description":934,"extension":935,"meta":936,"navigation":131,"path":966,"seo":967,"stem":968,"__hash__":969},"content\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ffailing-fast-with-pytest-timeout\u002Findex.md","Failing Fast with pytest-timeout",{"type":7,"value":8,"toc":922},"minimark",[9,18,23,43,47,50,115,205,345,349,356,363,367,415,419,422,485,492,495,503,507,510,556,562,569,659,663,666,673,738,741,750,754,757,827,834,841,845,860,873,879,883,913,918],[10,11,12,13,17],"p",{},"A suite without a per-test ceiling has one failure mode strictly worse than a red build: a job that runs until the platform kills it, with no traceback and no indication of which test was running. ",[14,15,16],"code",{},"pytest-timeout"," removes that, and the whole art of configuring it is choosing values generous enough never to fire on a healthy test and a method that actually works on the platform in question.",[19,20,22],"h2",{"id":21},"prerequisites","Prerequisites",[24,25,26,37,40],"ul",{},[27,28,29,32,33,36],"li",{},[14,30,31],{},"pytest >= 8.0"," and ",[14,34,35],{},"pytest-timeout >= 2.3",".",[27,38,39],{},"Knowledge of whether the suite uses worker threads, since that decides the method.",[27,41,42],{},"A measured picture of the suite's slowest tests under the configuration CI uses.",[19,44,46],{"id":45},"solution","Solution",[10,48,49],{},"Set a ceiling in configuration, pick the method deliberately, and override the exceptions.",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-toml shiki shiki-themes github-light github-dark","# pyproject.toml\n[tool.pytest.ini_options]\n# A ceiling, not an assertion: no healthy test should come near 60 s.\ntimeout = 60\n# \"thread\" dumps every thread's stack before killing the process, which is the\n# only useful output when the hang is a deadlock rather than a slow call.\ntimeout_method = \"thread\"\n# Dump stacks 15 s earlier so the evidence lands before the process ends.\nfaulthandler_timeout = 45\n","toml","",[14,58,59,67,73,79,85,91,97,103,109],{"__ignoreMap":56},[60,61,64],"span",{"class":62,"line":63},"line",1,[60,65,66],{},"# pyproject.toml\n",[60,68,70],{"class":62,"line":69},2,[60,71,72],{},"[tool.pytest.ini_options]\n",[60,74,76],{"class":62,"line":75},3,[60,77,78],{},"# A ceiling, not an assertion: no healthy test should come near 60 s.\n",[60,80,82],{"class":62,"line":81},4,[60,83,84],{},"timeout = 60\n",[60,86,88],{"class":62,"line":87},5,[60,89,90],{},"# \"thread\" dumps every thread's stack before killing the process, which is the\n",[60,92,94],{"class":62,"line":93},6,[60,95,96],{},"# only useful output when the hang is a deadlock rather than a slow call.\n",[60,98,100],{"class":62,"line":99},7,[60,101,102],{},"timeout_method = \"thread\"\n",[60,104,106],{"class":62,"line":105},8,[60,107,108],{},"# Dump stacks 15 s earlier so the evidence lands before the process ends.\n",[60,110,112],{"class":62,"line":111},9,[60,113,114],{},"faulthandler_timeout = 45\n",[51,116,120],{"className":117,"code":118,"language":119,"meta":56,"style":56},"language-python shiki shiki-themes github-light github-dark","import pytest\n\n\n@pytest.mark.timeout(300)               # a migration test against a real database\ndef test_full_migration_applies(engine_migrated):\n    assert engine_migrated.dialect.has_table(engine_migrated.connect(), \"widget\")\n\n\n@pytest.mark.timeout(5, method=\"signal\") # a tight bound, and a clean traceback\ndef test_parser_terminates_on_pathological_input(parser):\n    parser.parse(\"(\" * 10_000)\n\n\n@pytest.mark.timeout(0)                  # opt out entirely: an interactive helper\ndef test_manual_smoke_check():\n    ...\n","python",[14,121,122,127,133,137,142,147,152,156,160,165,171,177,182,187,193,199],{"__ignoreMap":56},[60,123,124],{"class":62,"line":63},[60,125,126],{},"import pytest\n",[60,128,129],{"class":62,"line":69},[60,130,132],{"emptyLinePlaceholder":131},true,"\n",[60,134,135],{"class":62,"line":75},[60,136,132],{"emptyLinePlaceholder":131},[60,138,139],{"class":62,"line":81},[60,140,141],{},"@pytest.mark.timeout(300)               # a migration test against a real database\n",[60,143,144],{"class":62,"line":87},[60,145,146],{},"def test_full_migration_applies(engine_migrated):\n",[60,148,149],{"class":62,"line":93},[60,150,151],{},"    assert engine_migrated.dialect.has_table(engine_migrated.connect(), \"widget\")\n",[60,153,154],{"class":62,"line":99},[60,155,132],{"emptyLinePlaceholder":131},[60,157,158],{"class":62,"line":105},[60,159,132],{"emptyLinePlaceholder":131},[60,161,162],{"class":62,"line":111},[60,163,164],{},"@pytest.mark.timeout(5, method=\"signal\") # a tight bound, and a clean traceback\n",[60,166,168],{"class":62,"line":167},10,[60,169,170],{},"def test_parser_terminates_on_pathological_input(parser):\n",[60,172,174],{"class":62,"line":173},11,[60,175,176],{},"    parser.parse(\"(\" * 10_000)\n",[60,178,180],{"class":62,"line":179},12,[60,181,132],{"emptyLinePlaceholder":131},[60,183,185],{"class":62,"line":184},13,[60,186,132],{"emptyLinePlaceholder":131},[60,188,190],{"class":62,"line":189},14,[60,191,192],{},"@pytest.mark.timeout(0)                  # opt out entirely: an interactive helper\n",[60,194,196],{"class":62,"line":195},15,[60,197,198],{},"def test_manual_smoke_check():\n",[60,200,202],{"class":62,"line":201},16,[60,203,204],{},"    ...\n",[206,207,210,337],"figure",{"className":208},[209],"diagram",[211,212,219,220,219,224,219,228,219,246,219,254,219,263,219,273,219,279,219,288,219,293,219,299,219,303,219,307,219,311,219,316,219,320,219,323,219,326,219,328,219,330,219,333],"svg",{"viewBox":213,"role":214,"ariaLabelledBy":215,"xmlns":218},"0 0 820 262","img",[216,217],"pt-t","pt-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[221,222,223],"title",{"id":216},"How each timeout method ends a hung test",[225,226,227],"desc",{"id":217},"Two paths. The signal method delivers SIGALRM to the main thread, which raises at the exact line and produces an ordinary pytest failure so the session continues. The thread method runs a watchdog that dumps every thread's stack and then terminates the process, ending the session but capturing complete evidence.",[229,230,231,232,219],"defs",{},"\n    ",[233,234,241],"marker",{"id":235,"viewBox":236,"refX":237,"refY":238,"markerWidth":239,"markerHeight":239,"orient":240},"pt-a","0 0 10 10","9","5","7","auto-start-reverse",[242,243],"path",{"d":244,"fill":245},"M0 0 L10 5 L0 10 z","#3d405b",[247,248],"rect",{"x":249,"y":249,"width":250,"height":251,"rx":252,"fill":253},"0","820","262","14","#fffdf8",[255,256,262],"text",{"x":257,"y":258,"textAnchor":259,"fontSize":260,"fontWeight":261,"fill":245},"410","28","middle","16","700","Two mechanisms, two kinds of evidence",[247,264],{"x":265,"y":266,"width":267,"height":268,"rx":269,"fill":270,"stroke":271,"strokeWidth":272},"26","52","368","186","12","#e6f0ea","#81b29a","2",[255,274,278],{"x":275,"y":276,"textAnchor":259,"fontSize":277,"fontWeight":261,"fill":245},"210","78","12.5","method = \"signal\"",[247,280],{"x":281,"y":282,"width":283,"height":284,"rx":285,"fill":253,"stroke":286,"strokeWidth":287},"48","92","324","34","8","rgba(61,64,91,0.35)","1.4",[255,289,292],{"x":275,"y":290,"textAnchor":259,"fontSize":291,"fill":245},"114","11","SIGALRM delivered to the main thread",[62,294],{"x1":275,"y1":295,"x2":275,"y2":296,"stroke":245,"strokeWidth":297,"markerEnd":298},"130","146","1.5","url(#pt-a)",[247,300],{"x":281,"y":301,"width":283,"height":284,"rx":285,"fill":253,"stroke":271,"strokeWidth":302},"150","1.6",[255,304,306],{"x":275,"y":305,"textAnchor":259,"fontSize":291,"fill":245},"172","raises at the exact line · real traceback",[255,308,310],{"x":275,"y":275,"textAnchor":259,"fontSize":291,"fill":309},"#2a5f49","session continues · Unix main thread only",[247,312],{"x":313,"y":266,"width":267,"height":268,"rx":269,"fill":314,"stroke":315,"strokeWidth":272},"426","#f7f0da","#f2cc8f",[255,317,319],{"x":318,"y":276,"textAnchor":259,"fontSize":277,"fontWeight":261,"fill":245},"610","method = \"thread\"",[247,321],{"x":322,"y":282,"width":283,"height":284,"rx":285,"fill":253,"stroke":286,"strokeWidth":287},"448",[255,324,325],{"x":318,"y":290,"textAnchor":259,"fontSize":291,"fill":245},"watchdog thread fires",[62,327],{"x1":318,"y1":295,"x2":318,"y2":296,"stroke":245,"strokeWidth":297,"markerEnd":298},[247,329],{"x":322,"y":301,"width":283,"height":284,"rx":285,"fill":253,"stroke":315,"strokeWidth":302},[255,331,332],{"x":318,"y":305,"textAnchor":259,"fontSize":291,"fill":245},"dumps every thread, then kills the process",[255,334,336],{"x":318,"y":275,"textAnchor":259,"fontSize":291,"fill":335},"#8a5a00","session ends · works everywhere",[338,339,340,341,344],"figcaption",{},"Prefer ",[14,342,343],{},"signal"," where the blocking code runs on the main thread of a Unix process, because it is the only method that produces a normal failure and lets the rest of the suite run.",[19,346,348],{"id":347},"why-this-works","Why this works",[10,350,351,352,355],{},"The signal method installs a ",[14,353,354],{},"SIGALRM"," handler and arms an alarm before each test. When it fires, the handler raises inside whatever frame the main thread is executing, which is why the traceback points at the exact blocking line. Signals in CPython are delivered only to the main thread, and only between bytecodes, so a main thread blocked inside a C call that does not release the GIL will not see it either.",[10,357,358,359,362],{},"The thread method sidesteps all of that with a watchdog that does not need cooperation from the test at all. It cannot raise into another thread — Python has no safe mechanism for that — so it dumps and terminates instead. That is a worse outcome for the session and a better one for the evidence, which is why it pairs naturally with ",[14,360,361],{},"faulthandler_timeout"," set slightly lower.",[19,364,366],{"id":365},"edge-cases-and-failure-modes","Edge cases and failure modes",[24,368,369,381,387,393,399],{},[27,370,371,377,378,36],{},[372,373,374,375,36],"strong",{},"Timeout never fires under ",[14,376,343],{}," The blocking code is off the main thread, or the platform is Windows. Switch to ",[14,379,380],{},"thread",[27,382,383,386],{},[372,384,385],{},"A bound tuned on an idle machine."," CI runners are shared, and a cold import can add a second. Set the ceiling an order of magnitude above the slowest observed test, not just above it.",[27,388,389,392],{},[372,390,391],{},"Raising the global ceiling for one slow test."," Every other test loses its containment. Use the marker.",[27,394,395,398],{},[372,396,397],{},"Interaction with fixtures."," The timeout covers setup, call and teardown together, so a test with a thirty-second container fixture needs a ceiling above that even if the body is instant.",[27,400,401,411,412,414],{},[372,402,403,406,407,410],{},[14,404,405],{},"method=\"signal\""," inside an ",[14,408,409],{},"xdist"," worker."," Workers run tests on their main thread, so signal generally still works — but any suite that also spawns threads should use ",[14,413,380],{}," for consistency rather than relying on which code blocks.",[19,416,418],{"id":417},"calibrating-the-numbers","Calibrating the numbers",[10,420,421],{},"The two mistakes are setting the ceiling too close to the observed duration and measuring under the wrong configuration. Both are avoidable in one pass.",[51,423,427],{"className":424,"code":425,"language":426,"meta":56,"style":56},"language-bash shiki shiki-themes github-light github-dark","# Measure under the configuration CI actually uses, not a serial local run.\npytest -n 8 --durations=0 -q > \u002Ftmp\u002Fdurations.txt\nsort -rn -k1 \u002Ftmp\u002Fdurations.txt | head -20\n","bash",[14,428,429,435,462],{"__ignoreMap":56},[60,430,431],{"class":62,"line":63},[60,432,434],{"class":433},"sJ8bj","# Measure under the configuration CI actually uses, not a serial local run.\n",[60,436,437,441,445,448,451,454,458],{"class":62,"line":69},[60,438,440],{"class":439},"sScJk","pytest",[60,442,444],{"class":443},"sj4cs"," -n",[60,446,447],{"class":443}," 8",[60,449,450],{"class":443}," --durations=0",[60,452,453],{"class":443}," -q",[60,455,457],{"class":456},"szBVR"," >",[60,459,461],{"class":460},"sZZnC"," \u002Ftmp\u002Fdurations.txt\n",[60,463,464,467,470,473,476,479,482],{"class":62,"line":75},[60,465,466],{"class":439},"sort",[60,468,469],{"class":443}," -rn",[60,471,472],{"class":443}," -k1",[60,474,475],{"class":460}," \u002Ftmp\u002Fdurations.txt",[60,477,478],{"class":456}," |",[60,480,481],{"class":439}," head",[60,483,484],{"class":443}," -20\n",[51,486,490],{"className":487,"code":489,"language":255,"meta":56},[488],"language-text","41.02s call     tests\u002Fintegration\u002Ftest_migration.py::test_full_migration_applies\n 8.71s setup    tests\u002Fintegration\u002Ftest_orders.py::test_create_order\n 3.10s call     tests\u002Fapi\u002Ftest_search.py::test_large_result_set\n 0.94s call     tests\u002Funit\u002Ftest_parser.py::test_deeply_nested\n",[14,491,489],{"__ignoreMap":56},[10,493,494],{},"With a slowest test of 41 seconds, a global ceiling of 60 is too tight: that test will occasionally take 55 on a loaded runner and fail for no reason. The correct arrangement is a global ceiling around 60 for the bulk of the suite plus a marker of 300 on the migration test, which keeps containment meaningful for the 99% while giving the genuine outlier room.",[10,496,497,498,502],{},"The reason to resist a single large ceiling is that the ceiling's value ",[499,500,501],"em",{},"is"," the detection latency. At 600 seconds, a deadlocked test costs ten minutes of every affected pipeline run before anyone learns anything; at 60 it costs one. Marking the three genuine outliers is worth the ten minutes it takes.",[19,504,506],{"id":505},"verifying-that-it-fires","Verifying that it fires",[10,508,509],{},"Configuration that has never been exercised is configuration that might not work. Prove it once:",[51,511,513],{"className":117,"code":512,"language":119,"meta":56,"style":56},"import time\n\nimport pytest\n\n\n@pytest.mark.skip(reason=\"run manually to verify the timeout configuration\")\n@pytest.mark.timeout(2)\ndef test_timeout_configuration_is_live():\n    time.sleep(30)\n",[14,514,515,520,524,528,532,536,541,546,551],{"__ignoreMap":56},[60,516,517],{"class":62,"line":63},[60,518,519],{},"import time\n",[60,521,522],{"class":62,"line":69},[60,523,132],{"emptyLinePlaceholder":131},[60,525,526],{"class":62,"line":75},[60,527,126],{},[60,529,530],{"class":62,"line":81},[60,531,132],{"emptyLinePlaceholder":131},[60,533,534],{"class":62,"line":87},[60,535,132],{"emptyLinePlaceholder":131},[60,537,538],{"class":62,"line":93},[60,539,540],{},"@pytest.mark.skip(reason=\"run manually to verify the timeout configuration\")\n",[60,542,543],{"class":62,"line":99},[60,544,545],{},"@pytest.mark.timeout(2)\n",[60,547,548],{"class":62,"line":105},[60,549,550],{},"def test_timeout_configuration_is_live():\n",[60,552,553],{"class":62,"line":111},[60,554,555],{},"    time.sleep(30)\n",[51,557,560],{"className":558,"code":559,"language":255,"meta":56},[488],"+++++++++++++++++++++++++++ Timeout ++++++++++++++++++++++++++++\n~~~~~~~~~~~~~~~ Stack of MainThread (140234...) ~~~~~~~~~~~~~~~~\n  File \"tests\u002Ftest_meta.py\", line 11, in test_timeout_configuration_is_live\n    time.sleep(30)\n+++++++++++++++++++++++++++ Timeout ++++++++++++++++++++++++++++\n",[14,561,559],{"__ignoreMap":56},[10,563,564,565,568],{},"Teams routinely discover at this point that ",[14,566,567],{},"timeout_method"," was left at its default in a threaded suite, where the signal never arrives and the ceiling has been doing nothing for months. Running this once after any change to the configuration takes ten seconds and is the only way to know.",[206,570,572,656],{"className":571},[209],[211,573,219,578,219,581,219,584,219,588,219,593,219,597,219,603,219,609,219,612,219,616,219,619,219,623,219,629,219,633,219,637,219,640,219,644,219,647,219,652],{"viewBox":574,"role":214,"ariaLabelledBy":575,"xmlns":218},"0 0 800 234",[576,577],"cal-t","cal-d",[221,579,580],{"id":576},"Choosing a ceiling from measured durations",[225,582,583],{"id":577},"A scale of test durations. Most tests sit under one second, a handful reach three to nine seconds, and one migration test takes forty-one. A global ceiling of sixty seconds covers everything except the outlier, which gets its own marker of three hundred seconds, keeping detection latency low for the bulk of the suite.",[247,585],{"x":249,"y":249,"width":586,"height":587,"rx":252,"fill":253},"800","234",[255,589,592],{"x":590,"y":258,"textAnchor":259,"fontSize":591,"fontWeight":261,"fill":245},"400","15.5","One ceiling for the suite, one marker for the outlier",[62,594],{"x1":595,"y1":301,"x2":596,"y2":301,"stroke":245,"strokeWidth":287},"60","760",[247,598],{"x":599,"y":600,"width":301,"height":601,"rx":239,"fill":270,"stroke":271,"strokeWidth":602},"70","118","30","1.7",[255,604,608],{"x":605,"y":606,"textAnchor":259,"fontSize":607,"fill":245},"145","138","10.5","most tests \u003C 1 s",[247,610],{"x":611,"y":600,"width":295,"height":601,"rx":239,"fill":270,"stroke":271,"strokeWidth":602},"240",[255,613,615],{"x":614,"y":606,"textAnchor":259,"fontSize":607,"fill":245},"305","3–9 s integration",[247,617],{"x":590,"y":600,"width":618,"height":601,"rx":239,"fill":314,"stroke":315,"strokeWidth":602},"120",[255,620,622],{"x":621,"y":606,"textAnchor":259,"fontSize":607,"fill":245},"460","41 s migration",[62,624],{"x1":625,"y1":626,"x2":625,"y2":627,"stroke":271,"strokeWidth":628},"560","96","176","3",[255,630,632],{"x":625,"y":631,"textAnchor":259,"fontSize":291,"fontWeight":261,"fill":309},"88","timeout = 60",[255,634,636],{"x":625,"y":635,"textAnchor":259,"fontSize":607,"fill":245},"194","suite ceiling",[62,638],{"x1":261,"y1":626,"x2":261,"y2":627,"stroke":639,"strokeWidth":628},"#e07a5f",[255,641,643],{"x":261,"y":631,"textAnchor":259,"fontSize":291,"fontWeight":261,"fill":642},"#8f3d22","marker = 300",[255,645,646],{"x":261,"y":635,"textAnchor":259,"fontSize":607,"fill":245},"one test only",[247,648],{"x":595,"y":649,"width":261,"height":650,"rx":239,"fill":253,"stroke":286,"strokeWidth":651},"206","22","1.3",[255,653,655],{"x":257,"y":654,"textAnchor":259,"fontSize":607,"fill":245},"222","Detection latency equals the ceiling, so keeping it low for the bulk of the suite matters.",[338,657,658],{},"Raising the global ceiling to 300 to accommodate one test would multiply every hang's cost by five for no benefit to the other tests.",[19,660,662],{"id":661},"what-the-ceiling-does-not-tell-you","What the ceiling does not tell you",[10,664,665],{},"It is worth being precise about the limits of a suite-wide timeout, because teams sometimes treat it as coverage of the code's own timeout behaviour and it is nothing of the kind.",[10,667,668,669,672],{},"A ceiling firing says only that a test did not finish. It does not say whether the code under test ",[499,670,671],{},"should"," have given up earlier, whether a retry loop is spinning, or whether a client's own deadline is configured correctly. Those are behavioural claims and need assertions inside the test:",[51,674,676],{"className":117,"code":675,"language":119,"meta":56,"style":56},"import asyncio\n\nimport pytest\n\n\nasync def test_client_gives_up_on_a_stalled_server(stalling_server):\n    # This asserts the CLIENT's behaviour. Without it, the test would \"pass\"\n    # only in the sense that pytest-timeout eventually stopped it.\n    with pytest.raises(TimeoutError):\n        async with asyncio.timeout(0.5):\n            await stalling_server.client.fetch(\"\u002Fnever-responds\")\n\n    assert stalling_server.client.open_connections == 0\n",[14,677,678,683,687,691,695,699,704,709,714,719,724,729,733],{"__ignoreMap":56},[60,679,680],{"class":62,"line":63},[60,681,682],{},"import asyncio\n",[60,684,685],{"class":62,"line":69},[60,686,132],{"emptyLinePlaceholder":131},[60,688,689],{"class":62,"line":75},[60,690,126],{},[60,692,693],{"class":62,"line":81},[60,694,132],{"emptyLinePlaceholder":131},[60,696,697],{"class":62,"line":87},[60,698,132],{"emptyLinePlaceholder":131},[60,700,701],{"class":62,"line":93},[60,702,703],{},"async def test_client_gives_up_on_a_stalled_server(stalling_server):\n",[60,705,706],{"class":62,"line":99},[60,707,708],{},"    # This asserts the CLIENT's behaviour. Without it, the test would \"pass\"\n",[60,710,711],{"class":62,"line":105},[60,712,713],{},"    # only in the sense that pytest-timeout eventually stopped it.\n",[60,715,716],{"class":62,"line":111},[60,717,718],{},"    with pytest.raises(TimeoutError):\n",[60,720,721],{"class":62,"line":167},[60,722,723],{},"        async with asyncio.timeout(0.5):\n",[60,725,726],{"class":62,"line":173},[60,727,728],{},"            await stalling_server.client.fetch(\"\u002Fnever-responds\")\n",[60,730,731],{"class":62,"line":179},[60,732,132],{"emptyLinePlaceholder":131},[60,734,735],{"class":62,"line":184},[60,736,737],{},"    assert stalling_server.client.open_connections == 0\n",[10,739,740],{},"The distinction shows up sharply in a regression. If a change removes a client's internal deadline, the per-operation assertion above fails immediately and names the client; the suite ceiling fails sixty seconds later and names nothing in particular. Both are worth having, and only the first is a test.",[10,742,743,744,749],{},"A related confusion is treating the ceiling as a performance budget. It is not: a test that normally takes 200 ms and now takes 40 seconds is a serious regression that a 60-second ceiling happily allows. Performance claims belong in a benchmark with a distribution, as described in ",[745,746,748],"a",{"href":747},"\u002Fsystematic-debugging-performance-profiling\u002Fcpu-profiling-with-cprofile-and-py-spy\u002Fbenchmarking-with-pytest-benchmark\u002F","benchmarking with pytest-benchmark",", and the ceiling remains what it is — containment.",[19,751,753],{"id":752},"timeouts-and-fixtures","Timeouts and fixtures",[10,755,756],{},"One behaviour surprises people often enough to state plainly: the timeout covers setup, call and teardown as a single budget.",[206,758,760,824],{"className":759},[209],[211,761,219,766,219,769,219,772,219,775,219,778,219,783,219,788,219,791,219,796,219,799,219,803,219,808,219,812,219,816,219,821],{"viewBox":762,"role":214,"ariaLabelledBy":763,"xmlns":218},"0 0 800 226",[764,765],"scope2-t","scope2-d",[221,767,768],{"id":764},"What the per-test timeout budget covers",[225,770,771],{"id":765},"A single timeout budget spans fixture setup, the test body and teardown. A test with a thirty-second container fixture and a one-second body consumes thirty-one seconds of a sixty-second ceiling, leaving less headroom than the body alone suggests.",[247,773],{"x":249,"y":249,"width":586,"height":774,"rx":252,"fill":253},"226",[255,776,777],{"x":590,"y":258,"textAnchor":259,"fontSize":591,"fontWeight":261,"fill":245},"One budget for the whole item",[247,779],{"x":284,"y":595,"width":780,"height":599,"rx":269,"fill":781,"stroke":245,"strokeWidth":782},"732","#f4f1de","1.8",[255,784,787],{"x":785,"y":786,"fontSize":269,"fontWeight":261,"fill":245},"54","84","timeout = 60 — the whole bar",[247,789],{"x":785,"y":790,"width":590,"height":265,"rx":239,"fill":314,"stroke":315,"strokeWidth":302},"94",[255,792,795],{"x":793,"y":794,"textAnchor":259,"fontSize":607,"fill":245},"254","112","setup: container start, migrations — 30 s",[247,797],{"x":621,"y":790,"width":798,"height":265,"rx":239,"fill":270,"stroke":271,"strokeWidth":302},"90",[255,800,802],{"x":801,"y":794,"textAnchor":259,"fontSize":607,"fill":245},"505","call 1 s",[247,804],{"x":805,"y":790,"width":806,"height":265,"rx":239,"fill":807,"stroke":639,"strokeWidth":302},"556","190","#fbe9e3",[255,809,811],{"x":810,"y":794,"textAnchor":259,"fontSize":607,"fill":245},"651","teardown + headroom",[247,813],{"x":284,"y":301,"width":780,"height":814,"rx":815,"fill":253,"stroke":286,"strokeWidth":297},"56","10",[255,817,820],{"x":590,"y":818,"textAnchor":259,"fontSize":819,"fill":245},"174","11.5","A one-second test body can still trip a sixty-second ceiling when its fixtures are slow.",[255,822,823],{"x":590,"y":635,"textAnchor":259,"fontSize":819,"fill":245},"Widen the fixture's scope, or mark the test — do not raise the ceiling for everyone.",[338,825,826],{},"Session-scoped fixtures charge their setup to whichever test triggered it, which is why the first test in a module can time out while identical later ones pass.",[10,828,829,830,833],{},"That last point is the one that produces the confusing report: the first test to request a session-scoped container pays its thirty seconds, and every test after it pays nothing. If the ceiling is tight, the ",[499,831,832],{},"first"," test in a file fails and the rest pass, which looks like a problem with that test and is not. Widening the fixture's scope so the cost is paid once per session, or marking that one test, both fix it; raising the global ceiling hides it.",[10,835,836,837,840],{},"The same effect appears in reverse under ",[14,838,839],{},"pytest-xdist",". Each worker is a separate process with its own session fixtures, so the expensive setup is paid once per worker rather than once per run — eight workers means eight container starts, and eight tests charged for them. A ceiling calibrated on a serial run, where only one test pays, will then fire on whichever test happened to be first in each worker. Measuring with the worker count CI uses is what makes this visible before it becomes an intermittent red build. It also usually argues for making the expensive fixture cheaper rather than the ceiling larger.",[19,842,844],{"id":843},"frequently-asked-questions","Frequently Asked Questions",[10,846,847,850,851,853,854,856,857,859],{},[372,848,849],{},"Why does my timeout never fire?","\nAlmost always because the signal method is in use and the blocked code is not on the main thread, or the platform is Windows where ",[14,852,354],{}," does not exist. Switch ",[14,855,567],{}," to ",[14,858,380],{},", which uses a watchdog rather than a signal and works everywhere, at the cost of ending the session when it fires.",[10,861,862,865,866,32,869,872],{},[372,863,864],{},"Does pytest-timeout interfere with a debugger?","\nIt detects an attached debugger and disables itself, so stepping through a test does not trip the ceiling. That detection covers ",[14,867,868],{},"pdb",[14,870,871],{},"debugpy","; with an unusual debugger, set the timeout to zero for the session rather than removing the configuration.",[10,874,875,878],{},[372,876,877],{},"How does the timeout interact with pytest-xdist?","\nEach worker applies the timeout to its own tests independently, which is what you want. The complication is calibration: with eight workers on four cores every test is slower, so a bound tuned on a serial run will fire spuriously. Measure under the parallel configuration CI actually uses.",[19,880,882],{"id":881},"related","Related",[24,884,885,892,899,906],{},[27,886,887,891],{},[745,888,890],{"href":889},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002F","Timeouts, Cancellation & Deadlines"," — how this ceiling relates to the per-operation deadlines inside tests.",[27,893,894,898],{},[745,895,897],{"href":896},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Fdumping-stacks-on-deadlock-with-faulthandler\u002F","Dumping Stacks on Deadlock with faulthandler"," — the layer that produces evidence before this one fires.",[27,900,901,905],{},[745,902,904],{"href":903},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ftesting-cancellation-and-cleanup-paths\u002F","Testing Cancellation and Cleanup Paths"," — asserting the code's own timeout behaviour rather than the runner's.",[27,907,908,912],{},[745,909,911],{"href":910},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-tests-in-ci-and-containers\u002Fdebugging-a-test-that-only-fails-under-xdist\u002F","Debugging a Test That Only Fails Under xdist"," — when the spurious timeout turns out to be contention.",[10,914,915,916],{},"← Back to ",[745,917,890],{"href":889},[919,920,921],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":56,"searchDepth":69,"depth":69,"links":923},[924,925,926,927,928,929,930,931,932,933],{"id":21,"depth":69,"text":22},{"id":45,"depth":69,"text":46},{"id":347,"depth":69,"text":348},{"id":365,"depth":69,"text":366},{"id":417,"depth":69,"text":418},{"id":505,"depth":69,"text":506},{"id":661,"depth":69,"text":662},{"id":752,"depth":69,"text":753},{"id":843,"depth":69,"text":844},{"id":881,"depth":69,"text":882},"Configure pytest-timeout properly: signal versus thread methods, per-test overrides, xdist interactions, debugger exemptions, and values that survive a loaded CI runner.","md",{"slug":937,"type":938,"breadcrumb":16,"datePublished":939,"dateModified":939,"faq":940,"howto":947},"failing-fast-with-pytest-timeout","article","2026-09-18",[941,943,945],{"q":849,"a":942},"Almost always because the signal method is in use and the blocked code is not on the main thread, or the platform is Windows where SIGALRM does not exist. Switch timeout_method to thread, which uses a watchdog rather than a signal and works everywhere, at the cost of ending the session when it fires.",{"q":864,"a":944},"It detects an attached debugger and disables itself, so stepping through a test does not trip the ceiling. That detection covers pdb and debugpy; with an unusual debugger, set the timeout to zero for the session rather than removing the configuration.",{"q":877,"a":946},"Each worker applies the timeout to its own tests independently, which is what you want. The complication is calibration: with eight workers on four cores every test is slower, so a bound tuned on a serial run will fire spuriously. Measure under the parallel configuration CI actually uses.",{"name":948,"description":949,"steps":950},"How to configure pytest-timeout so it helps rather than flakes","Set a generous suite ceiling, choose the method for the platform, and override only the tests that genuinely need longer.",[951,954,957,960,963],{"name":952,"text":953},"Set a suite-wide ceiling","Add timeout to the pytest configuration at a value no healthy test approaches.",{"name":955,"text":956},"Choose the method","Use signal on Unix when the blocking happens on the main thread, thread otherwise and on Windows.",{"name":958,"text":959},"Override individual slow tests","Apply the timeout marker to the few tests that legitimately exceed the ceiling rather than raising it for everyone.",{"name":961,"text":962},"Calibrate under the CI configuration","Measure durations with the same worker count CI uses, and set the ceiling an order of magnitude above the slowest.",{"name":964,"text":965},"Verify it fires","Run a deliberately hanging test once and read the output so the configuration is known to work.","\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ffailing-fast-with-pytest-timeout",{"title":5,"description":934},"testing-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ffailing-fast-with-pytest-timeout\u002Findex","rRiFIKeKGaXiB9UHaanWdcwFNlY_Lp4wM77mnTV2Pzo",1789718767525]