[{"data":1,"prerenderedAt":1071},["ShallowReactive",2],{"page-\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002Fwaiting-for-container-readiness-without-sleep\u002F":3},{"id":4,"title":5,"body":6,"description":1034,"extension":1035,"meta":1036,"navigation":83,"path":1067,"seo":1068,"stem":1069,"__hash__":1070},"content\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002Fwaiting-for-container-readiness-without-sleep\u002Findex.md","Waiting for Container Readiness Without sleep",{"type":7,"value":8,"toc":1023},"minimark",[9,21,26,54,58,61,284,378,382,388,391,395,448,452,455,557,560,614,618,621,711,714,721,838,842,848,936,943,950,953,957,963,975,981,985,1014,1019],[10,11,12,16,17,20],"p",{},[13,14,15],"code",{},"time.sleep(10)"," after ",[13,18,19],{},"container.start()"," is the most common line in a Testcontainers fixture and the source of most of their flakiness. It is too long on a warm laptop, where the service was ready in two seconds, and too short on a loaded CI runner, where it needed twelve. Every service has a better signal than elapsed time, and waiting on it makes start-up both faster and reliable.",[22,23,25],"h2",{"id":24},"prerequisites","Prerequisites",[27,28,29,36,43],"ul",{},[30,31,32,35],"li",{},[13,33,34],{},"testcontainers >= 4.0",".",[30,37,38,39,42],{},"Knowledge of what the service prints or answers when it is genuinely ready — its documentation or one manual ",[13,40,41],{},"docker logs"," run is enough.",[30,44,45,48,49,35],{},[13,46,47],{},"pytest >= 8.0",", and the lifecycle model from ",[50,51,53],"a",{"href":52},"\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002F","spinning up services with Testcontainers",[22,55,57],{"id":56},"solution","Solution",[10,59,60],{},"Layer two signals: a log line the service prints when it believes it is ready, then one real request that proves it.",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"language-python shiki shiki-themes github-light github-dark","import time\n\nimport pytest\nimport redis\nfrom testcontainers.core.container import DockerContainer\nfrom testcontainers.core.waiting_utils import wait_for_logs\n\n\ndef wait_for_request(check, *, timeout=30.0, interval=0.2, what=\"service\"):\n    \"\"\"Retry a real request until it succeeds or the deadline passes.\"\"\"\n    deadline = time.monotonic() + timeout\n    last_error = None\n    while time.monotonic() \u003C deadline:\n        try:\n            check()\n            return\n        except Exception as exc:                 # connection refused, reset, …\n            last_error = exc\n            time.sleep(interval)\n    raise TimeoutError(f\"{what} not ready after {timeout}s: {last_error!r}\")\n\n\n@pytest.fixture(scope=\"session\")\ndef redis_client():\n    container = DockerContainer(\"redis:7.4-alpine\").with_exposed_ports(6379)\n    with container:\n        # 1. The service's own \"I am ready\" message.\n        wait_for_logs(container, \"Ready to accept connections\", timeout=30)\n\n        client = redis.Redis(\n            host=container.get_container_host_ip(),\n            port=int(container.get_exposed_port(6379)),\n        )\n        # 2. Belt and braces: one real round trip before any test runs.\n        wait_for_request(client.ping, what=\"redis\")\n        yield client\n","python","",[13,69,70,78,85,91,97,103,109,114,119,125,131,137,143,149,155,161,167,173,179,185,191,196,201,207,213,219,225,231,237,242,248,254,260,266,272,278],{"__ignoreMap":67},[71,72,75],"span",{"class":73,"line":74},"line",1,[71,76,77],{},"import time\n",[71,79,81],{"class":73,"line":80},2,[71,82,84],{"emptyLinePlaceholder":83},true,"\n",[71,86,88],{"class":73,"line":87},3,[71,89,90],{},"import pytest\n",[71,92,94],{"class":73,"line":93},4,[71,95,96],{},"import redis\n",[71,98,100],{"class":73,"line":99},5,[71,101,102],{},"from testcontainers.core.container import DockerContainer\n",[71,104,106],{"class":73,"line":105},6,[71,107,108],{},"from testcontainers.core.waiting_utils import wait_for_logs\n",[71,110,112],{"class":73,"line":111},7,[71,113,84],{"emptyLinePlaceholder":83},[71,115,117],{"class":73,"line":116},8,[71,118,84],{"emptyLinePlaceholder":83},[71,120,122],{"class":73,"line":121},9,[71,123,124],{},"def wait_for_request(check, *, timeout=30.0, interval=0.2, what=\"service\"):\n",[71,126,128],{"class":73,"line":127},10,[71,129,130],{},"    \"\"\"Retry a real request until it succeeds or the deadline passes.\"\"\"\n",[71,132,134],{"class":73,"line":133},11,[71,135,136],{},"    deadline = time.monotonic() + timeout\n",[71,138,140],{"class":73,"line":139},12,[71,141,142],{},"    last_error = None\n",[71,144,146],{"class":73,"line":145},13,[71,147,148],{},"    while time.monotonic() \u003C deadline:\n",[71,150,152],{"class":73,"line":151},14,[71,153,154],{},"        try:\n",[71,156,158],{"class":73,"line":157},15,[71,159,160],{},"            check()\n",[71,162,164],{"class":73,"line":163},16,[71,165,166],{},"            return\n",[71,168,170],{"class":73,"line":169},17,[71,171,172],{},"        except Exception as exc:                 # connection refused, reset, …\n",[71,174,176],{"class":73,"line":175},18,[71,177,178],{},"            last_error = exc\n",[71,180,182],{"class":73,"line":181},19,[71,183,184],{},"            time.sleep(interval)\n",[71,186,188],{"class":73,"line":187},20,[71,189,190],{},"    raise TimeoutError(f\"{what} not ready after {timeout}s: {last_error!r}\")\n",[71,192,194],{"class":73,"line":193},21,[71,195,84],{"emptyLinePlaceholder":83},[71,197,199],{"class":73,"line":198},22,[71,200,84],{"emptyLinePlaceholder":83},[71,202,204],{"class":73,"line":203},23,[71,205,206],{},"@pytest.fixture(scope=\"session\")\n",[71,208,210],{"class":73,"line":209},24,[71,211,212],{},"def redis_client():\n",[71,214,216],{"class":73,"line":215},25,[71,217,218],{},"    container = DockerContainer(\"redis:7.4-alpine\").with_exposed_ports(6379)\n",[71,220,222],{"class":73,"line":221},26,[71,223,224],{},"    with container:\n",[71,226,228],{"class":73,"line":227},27,[71,229,230],{},"        # 1. The service's own \"I am ready\" message.\n",[71,232,234],{"class":73,"line":233},28,[71,235,236],{},"        wait_for_logs(container, \"Ready to accept connections\", timeout=30)\n",[71,238,240],{"class":73,"line":239},29,[71,241,84],{"emptyLinePlaceholder":83},[71,243,245],{"class":73,"line":244},30,[71,246,247],{},"        client = redis.Redis(\n",[71,249,251],{"class":73,"line":250},31,[71,252,253],{},"            host=container.get_container_host_ip(),\n",[71,255,257],{"class":73,"line":256},32,[71,258,259],{},"            port=int(container.get_exposed_port(6379)),\n",[71,261,263],{"class":73,"line":262},33,[71,264,265],{},"        )\n",[71,267,269],{"class":73,"line":268},34,[71,270,271],{},"        # 2. Belt and braces: one real round trip before any test runs.\n",[71,273,275],{"class":73,"line":274},35,[71,276,277],{},"        wait_for_request(client.ping, what=\"redis\")\n",[71,279,281],{"class":73,"line":280},36,[71,282,283],{},"        yield client\n",[285,286,289,374],"figure",{"className":287},[288],"diagram",[290,291,298,299,298,303,298,307,298,315,298,325,298,335,298,341,298,346,298,351,298,355,298,360,298,365,298,369],"svg",{"viewBox":292,"role":293,"ariaLabelledBy":294,"xmlns":297},"0 0 820 262","img",[295,296],"rdy-t","rdy-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[300,301,302],"title",{"id":295},"Three readiness signals ranked by reliability",[304,305,306],"desc",{"id":296},"Three horizontal bands. An open port is the weakest signal because many services bind before they can serve. A log line is stronger because the service emits it when it believes it is ready. A real request such as a query or a ping is the strongest because it proves the service can do the work tests will ask of it.",[308,309],"rect",{"x":310,"y":310,"width":311,"height":312,"rx":313,"fill":314},"0","820","262","14","#fffdf8",[316,317,324],"text",{"x":318,"y":319,"textAnchor":320,"fontSize":321,"fontWeight":322,"fill":323},"410","28","middle","16","700","#3d405b","Stronger signals prove more about readiness",[308,326],{"x":327,"y":328,"width":329,"height":330,"rx":331,"fill":332,"stroke":333,"strokeWidth":334},"26","50","768","56","11","#fbe9e3","#e07a5f","2",[316,336,340],{"x":337,"y":338,"fontSize":339,"fontWeight":322,"fill":323},"46","74","12","port is open",[316,342,345],{"x":337,"y":343,"fontSize":331,"fill":344},"94","#8f3d22","bound but still initialising — the first connection is reset",[308,347],{"x":327,"y":348,"width":329,"height":330,"rx":331,"fill":349,"stroke":350,"strokeWidth":334},"116","#f7f0da","#f2cc8f",[316,352,354],{"x":337,"y":353,"fontSize":339,"fontWeight":322,"fill":323},"140","log line matched",[316,356,359],{"x":337,"y":357,"fontSize":331,"fill":358},"160","#8a5a00","the service believes it is ready — occasionally premature",[308,361],{"x":327,"y":362,"width":329,"height":330,"rx":331,"fill":363,"stroke":364,"strokeWidth":334},"182","#e6f0ea","#81b29a",[316,366,368],{"x":337,"y":367,"fontSize":339,"fontWeight":322,"fill":323},"206","real request succeeded",[316,370,373],{"x":337,"y":371,"fontSize":331,"fill":372},"226","#2a5f49","SELECT 1, PING, a metadata call — proven able to serve",[375,376,377],"figcaption",{},"The fixture above uses the middle signal to avoid hammering a service that is still booting, and the bottom one to be certain.",[22,379,381],{"id":380},"why-this-works","Why this works",[10,383,384,387],{},[13,385,386],{},"wait_for_logs"," tails the container's output and returns the moment a regex matches, so it costs exactly as long as start-up takes. The subsequent real request closes the small window where a service logs readiness slightly before it can serve — Postgres, for instance, prints \"ready to accept connections\" twice during initialisation, once for a temporary server used by the entrypoint scripts and once for the real one.",[10,389,390],{},"Both waits are bounded. A service that never becomes ready fails the session within thirty seconds with a message naming the service and the last error, instead of running until the CI job's global limit and dying silently.",[22,392,394],{"id":393},"edge-cases-and-failure-modes","Edge cases and failure modes",[27,396,397,409,415,428,438],{},[30,398,399,403,404,408],{},[400,401,402],"strong",{},"A log line printed twice."," Postgres's double \"ready\" message is the classic case; matching the first one connects to a server about to restart. The official module handles this; a hand-rolled fixture must match the ",[405,406,407],"em",{},"second"," occurrence or confirm with a query.",[30,410,411,414],{},[400,412,413],{},"A pattern that never matches after an upgrade."," Services reword their startup messages between versions. Pin the image tag and keep the real-request confirmation as a backstop.",[30,416,417,423,424,427],{},[400,418,419,420,35],{},"Readiness checks against ",[13,421,422],{},"localhost"," Under Docker-in-Docker the container's host differs. ",[13,425,426],{},"get_container_host_ip()"," is correct everywhere.",[30,429,430,433,434,437],{},[400,431,432],{},"Health checks defined in the image."," Some images declare a Docker ",[13,435,436],{},"HEALTHCHECK","; polling the container's health status is a good signal where it exists, and it reflects whatever the image author considered ready.",[30,439,440,443,444,447],{},[400,441,442],{},"Retrying on every exception."," Catching ",[13,445,446],{},"Exception"," in the retry loop also hides configuration errors such as a wrong password. Retry on connection errors only, and let authentication failures surface immediately.",[22,449,451],{"id":450},"diagnosing-a-service-that-never-becomes-ready","Diagnosing a service that never becomes ready",[10,453,454],{},"When the wait times out, the most important evidence is the container's own output, which disappears with the container unless the fixture captures it first.",[62,456,458],{"className":64,"code":457,"language":66,"meta":67,"style":67},"import pytest\nfrom testcontainers.core.container import DockerContainer\nfrom testcontainers.core.waiting_utils import wait_for_logs\n\n\n@pytest.fixture(scope=\"session\")\ndef search_service():\n    container = DockerContainer(\"ghcr.io\u002Facme\u002Fsearch:3.2.0\").with_exposed_ports(9200)\n    with container:\n        try:\n            wait_for_logs(container, r\"started \\[node=\", timeout=90)\n        except Exception:\n            stdout, stderr = container.get_logs()\n            # Put the reason in the test output before the container is removed.\n            pytest.fail(\n                \"search service never became ready.\\n\"\n                f\"--- stdout ---\\n{stdout.decode()[-4000:]}\\n\"\n                f\"--- stderr ---\\n{stderr.decode()[-4000:]}\",\n                pytrace=False,\n            )\n        yield container\n",[13,459,460,464,468,472,476,480,484,489,494,498,502,507,512,517,522,527,532,537,542,547,552],{"__ignoreMap":67},[71,461,462],{"class":73,"line":74},[71,463,90],{},[71,465,466],{"class":73,"line":80},[71,467,102],{},[71,469,470],{"class":73,"line":87},[71,471,108],{},[71,473,474],{"class":73,"line":93},[71,475,84],{"emptyLinePlaceholder":83},[71,477,478],{"class":73,"line":99},[71,479,84],{"emptyLinePlaceholder":83},[71,481,482],{"class":73,"line":105},[71,483,206],{},[71,485,486],{"class":73,"line":111},[71,487,488],{},"def search_service():\n",[71,490,491],{"class":73,"line":116},[71,492,493],{},"    container = DockerContainer(\"ghcr.io\u002Facme\u002Fsearch:3.2.0\").with_exposed_ports(9200)\n",[71,495,496],{"class":73,"line":121},[71,497,224],{},[71,499,500],{"class":73,"line":127},[71,501,154],{},[71,503,504],{"class":73,"line":133},[71,505,506],{},"            wait_for_logs(container, r\"started \\[node=\", timeout=90)\n",[71,508,509],{"class":73,"line":139},[71,510,511],{},"        except Exception:\n",[71,513,514],{"class":73,"line":145},[71,515,516],{},"            stdout, stderr = container.get_logs()\n",[71,518,519],{"class":73,"line":151},[71,520,521],{},"            # Put the reason in the test output before the container is removed.\n",[71,523,524],{"class":73,"line":157},[71,525,526],{},"            pytest.fail(\n",[71,528,529],{"class":73,"line":163},[71,530,531],{},"                \"search service never became ready.\\n\"\n",[71,533,534],{"class":73,"line":169},[71,535,536],{},"                f\"--- stdout ---\\n{stdout.decode()[-4000:]}\\n\"\n",[71,538,539],{"class":73,"line":175},[71,540,541],{},"                f\"--- stderr ---\\n{stderr.decode()[-4000:]}\",\n",[71,543,544],{"class":73,"line":181},[71,545,546],{},"                pytrace=False,\n",[71,548,549],{"class":73,"line":187},[71,550,551],{},"            )\n",[71,553,554],{"class":73,"line":193},[71,555,556],{},"        yield container\n",[10,558,559],{},"The last four thousand characters of the logs almost always contain the answer: an out-of-memory kill, a missing environment variable, a port conflict inside the container, a licence check. Without them the failure is \"timed out after 90s\" and a re-run with extra instrumentation; with them it is usually a one-line fix.",[285,561,563,611],{"className":562},[288],[290,564,298,569,298,572,298,575,298,579,298,584,298,588,298,591,298,595,298,598,298,600,298,604,298,608],{"viewBox":565,"role":293,"ariaLabelledBy":566,"xmlns":297},"0 0 800 234",[567,568],"dx-t","dx-d",[300,570,571],{"id":567},"What a readiness failure should leave behind",[304,573,574],{"id":568},"Two outcomes of the same timeout. Without log capture, the report says only that readiness timed out and the container is removed with its evidence. With log capture, the report includes the tail of the container's stdout and stderr, which typically names the cause directly.",[308,576],{"x":310,"y":310,"width":577,"height":578,"rx":313,"fill":314},"800","234",[316,580,583],{"x":581,"y":319,"textAnchor":320,"fontSize":582,"fontWeight":322,"fill":323},"400","15.5","The container's logs are the diagnosis — keep them",[308,585],{"x":327,"y":328,"width":586,"height":587,"rx":331,"fill":332,"stroke":333,"strokeWidth":334},"748","76",[316,589,590],{"x":337,"y":338,"fontSize":339,"fontWeight":322,"fill":323},"no capture",[316,592,594],{"x":337,"y":593,"fontSize":331,"fill":323},"96","\"TimeoutError: not ready after 90s\" · container removed",[316,596,597],{"x":337,"y":348,"fontSize":331,"fill":344},"re-run with instrumentation to find out why",[308,599],{"x":327,"y":353,"width":586,"height":587,"rx":331,"fill":363,"stroke":364,"strokeWidth":334},[316,601,603],{"x":337,"y":602,"fontSize":339,"fontWeight":322,"fill":323},"164","logs captured before removal",[316,605,607],{"x":337,"y":606,"fontSize":331,"fill":323},"186","\"max virtual memory areas vm.max_map_count [65530] is too low\"",[316,609,610],{"x":337,"y":367,"fontSize":331,"fill":372},"the fix is in the message",[375,612,613],{},"Capturing the logs costs three lines in the fixture and turns the most common Testcontainers failure from an investigation into a lookup.",[22,615,617],{"id":616},"readiness-for-services-that-depend-on-each-other","Readiness for services that depend on each other",[10,619,620],{},"A single container is simple. A stack — an application that needs a database, a broker and a cache — adds ordering, and the temptation is a sleep between each start. The better arrangement starts everything in parallel and waits on each dependency's own signal before starting whatever needs it.",[62,622,624],{"className":64,"code":623,"language":66,"meta":67,"style":67},"import concurrent.futures\n\nimport pytest\n\n\n@pytest.fixture(scope=\"session\")\ndef stack(postgres, redis_client, kafka):\n    # postgres, redis_client and kafka are independent session fixtures,\n    # each waiting on its own readiness signal. pytest starts them in\n    # dependency order; the app fixture below only runs once all three yield.\n    return {\"db\": postgres, \"cache\": redis_client, \"broker\": kafka}\n\n\n@pytest.fixture(scope=\"session\")\ndef app_container(stack):\n    container = build_app_container(env=connection_env(stack))\n    with container:\n        wait_for_request(lambda: http_get(container, \"\u002Fhealth\"), what=\"app\", timeout=60)\n        yield container\n",[13,625,626,631,635,639,643,647,651,656,661,666,671,676,680,684,688,693,698,702,707],{"__ignoreMap":67},[71,627,628],{"class":73,"line":74},[71,629,630],{},"import concurrent.futures\n",[71,632,633],{"class":73,"line":80},[71,634,84],{"emptyLinePlaceholder":83},[71,636,637],{"class":73,"line":87},[71,638,90],{},[71,640,641],{"class":73,"line":93},[71,642,84],{"emptyLinePlaceholder":83},[71,644,645],{"class":73,"line":99},[71,646,84],{"emptyLinePlaceholder":83},[71,648,649],{"class":73,"line":105},[71,650,206],{},[71,652,653],{"class":73,"line":111},[71,654,655],{},"def stack(postgres, redis_client, kafka):\n",[71,657,658],{"class":73,"line":116},[71,659,660],{},"    # postgres, redis_client and kafka are independent session fixtures,\n",[71,662,663],{"class":73,"line":121},[71,664,665],{},"    # each waiting on its own readiness signal. pytest starts them in\n",[71,667,668],{"class":73,"line":127},[71,669,670],{},"    # dependency order; the app fixture below only runs once all three yield.\n",[71,672,673],{"class":73,"line":133},[71,674,675],{},"    return {\"db\": postgres, \"cache\": redis_client, \"broker\": kafka}\n",[71,677,678],{"class":73,"line":139},[71,679,84],{"emptyLinePlaceholder":83},[71,681,682],{"class":73,"line":145},[71,683,84],{"emptyLinePlaceholder":83},[71,685,686],{"class":73,"line":151},[71,687,206],{},[71,689,690],{"class":73,"line":157},[71,691,692],{},"def app_container(stack):\n",[71,694,695],{"class":73,"line":163},[71,696,697],{},"    container = build_app_container(env=connection_env(stack))\n",[71,699,700],{"class":73,"line":169},[71,701,224],{},[71,703,704],{"class":73,"line":175},[71,705,706],{},"        wait_for_request(lambda: http_get(container, \"\u002Fhealth\"), what=\"app\", timeout=60)\n",[71,708,709],{"class":73,"line":181},[71,710,556],{},[10,712,713],{},"Fixture dependencies already express the ordering: the application fixture requests the stack, the stack requests each service, and pytest resolves them depth-first. Each service fixture owns its own readiness wait, so by the time the application starts every dependency is proven usable — no sleeps, no guessing, and each failure attributed to the service that caused it.",[10,715,716,717,720],{},"One refinement is worth making once the stack grows past three or four services: start the independent containers concurrently rather than one after another. pytest resolves session fixtures sequentially, so three services with four-second start-ups cost twelve seconds in series. Starting them together from a single fixture with a thread pool — each thread calling ",[13,718,719],{},"start()"," and then its own readiness wait — brings that down to roughly the slowest single start-up, and every readiness failure is still reported against the service it belongs to because each wait raises its own named error. It is a small change with a large effect on the feedback loop.",[285,722,724,835],{"className":723},[288],[290,725,298,730,298,733,298,736,298,753,298,756,298,759,298,766,298,770,298,774,298,776,298,779,298,783,298,786,298,790,298,794,298,802,298,805,298,809,298,814,298,818,298,821,298,827,298,831],{"viewBox":726,"role":293,"ariaLabelledBy":727,"xmlns":297},"0 0 800 246",[728,729],"dep-t","dep-d",[300,731,732],{"id":728},"Dependency-ordered readiness for a service stack",[304,734,735],{"id":729},"Three independent service fixtures — database, cache and broker — each wait on their own readiness signal. The application fixture depends on all three and starts only after they yield, then waits on its own health endpoint. A failure is attributed to whichever fixture's readiness wait timed out.",[737,738,739,740,298],"defs",{},"\n    ",[741,742,749],"marker",{"id":743,"viewBox":744,"refX":745,"refY":746,"markerWidth":747,"markerHeight":747,"orient":748},"dep-a","0 0 10 10","9","5","7","auto-start-reverse",[750,751],"path",{"d":752,"fill":364},"M0 0 L10 5 L0 10 z",[308,754],{"x":310,"y":310,"width":577,"height":755,"rx":313,"fill":314},"246",[316,757,758],{"x":581,"y":319,"textAnchor":320,"fontSize":582,"fontWeight":322,"fill":323},"Each fixture proves its own readiness",[308,760],{"x":761,"y":762,"width":763,"height":764,"rx":765,"fill":363,"stroke":364,"strokeWidth":334},"34","52","200","54","10",[316,767,769],{"x":768,"y":587,"textAnchor":320,"fontSize":339,"fontWeight":322,"fill":323},"134","postgres",[316,771,773],{"x":768,"y":772,"textAnchor":320,"fontSize":331,"fill":323},"95","SELECT 1",[308,775],{"x":761,"y":348,"width":763,"height":764,"rx":765,"fill":363,"stroke":364,"strokeWidth":334},[316,777,778],{"x":768,"y":353,"textAnchor":320,"fontSize":339,"fontWeight":322,"fill":323},"redis",[316,780,782],{"x":768,"y":781,"textAnchor":320,"fontSize":331,"fill":323},"159","PING",[308,784],{"x":761,"y":785,"width":763,"height":764,"rx":765,"fill":363,"stroke":364,"strokeWidth":334},"180",[316,787,789],{"x":768,"y":788,"textAnchor":320,"fontSize":339,"fontWeight":322,"fill":323},"204","kafka",[316,791,793],{"x":768,"y":792,"textAnchor":320,"fontSize":331,"fill":323},"223","metadata request",[73,795],{"x1":796,"y1":797,"x2":798,"y2":799,"stroke":364,"strokeWidth":800,"markerEnd":801},"238","79","320","130","1.8","url(#dep-a)",[73,803],{"x1":796,"y1":804,"x2":798,"y2":804,"stroke":364,"strokeWidth":800,"markerEnd":801},"143",[73,806],{"x1":796,"y1":807,"x2":798,"y2":808,"stroke":364,"strokeWidth":800,"markerEnd":801},"207","156",[308,810],{"x":811,"y":812,"width":763,"height":813,"rx":331,"fill":349,"stroke":350,"strokeWidth":334},"326","104","78",[316,815,817],{"x":816,"y":768,"textAnchor":320,"fontSize":339,"fontWeight":322,"fill":323},"426","app container",[316,819,820],{"x":816,"y":808,"textAnchor":320,"fontSize":331,"fill":323},"GET \u002Fhealth",[308,822],{"x":823,"y":812,"width":824,"height":813,"rx":331,"fill":314,"stroke":825,"strokeWidth":826},"552","220","rgba(61,64,91,0.35)","1.5",[316,828,830],{"x":829,"y":768,"textAnchor":320,"fontSize":331,"fill":323},"662","starts only once all",[316,832,834],{"x":829,"y":833,"textAnchor":320,"fontSize":331,"fill":323},"154","three dependencies yield",[375,836,837],{},"No fixture sleeps and no fixture assumes another is ready. The dependency graph pytest already builds is the ordering mechanism.",[22,839,841],{"id":840},"using-the-images-own-health-check","Using the image's own health check",[10,843,844,845,847],{},"Many images ship a Docker ",[13,846,436],{}," — a command the daemon runs periodically inside the container, whose result appears as the container's health status. Where it exists it is an excellent readiness signal, because it encodes the image author's definition of ready and it keeps working after the service's log messages change.",[62,849,851],{"className":64,"code":850,"language":66,"meta":67,"style":67},"import time\n\nimport docker\n\n\ndef wait_until_healthy(container_id: str, *, timeout: float = 60.0) -> None:\n    client = docker.from_env()\n    deadline = time.monotonic() + timeout\n    status = \"unknown\"\n    while time.monotonic() \u003C deadline:\n        state = client.api.inspect_container(container_id)[\"State\"]\n        status = state.get(\"Health\", {}).get(\"Status\", \"none\")\n        if status == \"healthy\":\n            return\n        if status == \"unhealthy\" or not state[\"Running\"]:\n            break                          # fail fast; waiting longer is pointless\n        time.sleep(0.5)\n    raise TimeoutError(f\"container never became healthy (last status: {status})\")\n",[13,852,853,857,861,866,870,874,879,884,888,893,897,902,907,912,916,921,926,931],{"__ignoreMap":67},[71,854,855],{"class":73,"line":74},[71,856,77],{},[71,858,859],{"class":73,"line":80},[71,860,84],{"emptyLinePlaceholder":83},[71,862,863],{"class":73,"line":87},[71,864,865],{},"import docker\n",[71,867,868],{"class":73,"line":93},[71,869,84],{"emptyLinePlaceholder":83},[71,871,872],{"class":73,"line":99},[71,873,84],{"emptyLinePlaceholder":83},[71,875,876],{"class":73,"line":105},[71,877,878],{},"def wait_until_healthy(container_id: str, *, timeout: float = 60.0) -> None:\n",[71,880,881],{"class":73,"line":111},[71,882,883],{},"    client = docker.from_env()\n",[71,885,886],{"class":73,"line":116},[71,887,136],{},[71,889,890],{"class":73,"line":121},[71,891,892],{},"    status = \"unknown\"\n",[71,894,895],{"class":73,"line":127},[71,896,148],{},[71,898,899],{"class":73,"line":133},[71,900,901],{},"        state = client.api.inspect_container(container_id)[\"State\"]\n",[71,903,904],{"class":73,"line":139},[71,905,906],{},"        status = state.get(\"Health\", {}).get(\"Status\", \"none\")\n",[71,908,909],{"class":73,"line":145},[71,910,911],{},"        if status == \"healthy\":\n",[71,913,914],{"class":73,"line":151},[71,915,166],{},[71,917,918],{"class":73,"line":157},[71,919,920],{},"        if status == \"unhealthy\" or not state[\"Running\"]:\n",[71,922,923],{"class":73,"line":163},[71,924,925],{},"            break                          # fail fast; waiting longer is pointless\n",[71,927,928],{"class":73,"line":169},[71,929,930],{},"        time.sleep(0.5)\n",[71,932,933],{"class":73,"line":175},[71,934,935],{},"    raise TimeoutError(f\"container never became healthy (last status: {status})\")\n",[10,937,938,939,942],{},"The early exit on ",[13,940,941],{},"unhealthy"," or a stopped container is the refinement worth copying. A service that has crashed will never become ready, and a readiness loop that keeps polling until its timeout wastes a minute of every failing run to report something that was knowable after two seconds.",[10,944,945,946,949],{},"For images without a declared health check, adding one in a small test-only ",[13,947,948],{},"Dockerfile"," is sometimes cleaner than encoding readiness in Python — particularly when several suites use the same image. The health command then lives with the image, and every consumer gets the same definition of ready without each writing its own poll.",[10,951,952],{},"Whichever signal is chosen, write it down next to the fixture with the reason. \"Waits for the second 'ready to accept connections' line because the first comes from the init server\" is the comment that stops the next person from simplifying the pattern and reintroducing the flake it was written to remove.",[22,954,956],{"id":955},"frequently-asked-questions","Frequently Asked Questions",[10,958,959,962],{},[400,960,961],{},"Why is 'the container is running' not the same as 'the service is ready'?","\nDocker reports a container as running once its entry process has started. The service inside may still be initialising a data directory, replaying a log, or waiting for a cluster to form, and connections during that window are refused or reset. Readiness has to be checked against the service itself.",[10,964,965,968,969,971,972,974],{},[400,966,967],{},"Which readiness signal is most reliable?","\nA real request that exercises the service: ",[13,970,773],{}," for a database, a ",[13,973,782],{}," for Redis, a metadata request for Kafka. A log line is a good second choice because it is emitted by the service when it believes it is ready. An open port is the weakest signal, since many services bind before they can serve.",[10,976,977,980],{},[400,978,979],{},"How long should the readiness timeout be?","\nLong enough for a cold start on a loaded runner — thirty to sixty seconds for most databases, longer for Kafka or Elasticsearch — because it only matters when something is wrong. The poll returns as soon as the service is ready, so a generous timeout costs nothing on healthy runs.",[22,982,984],{"id":983},"related","Related",[27,986,987,993,1000,1007],{},[30,988,989,992],{},[50,990,991],{"href":52},"Spinning Up Services with Testcontainers"," — lifecycles and reaping around these waits.",[30,994,995,999],{},[50,996,998],{"href":997},"\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002Fstarting-postgres-with-testcontainers-python\u002F","Starting Postgres with testcontainers-python"," — a fixture where the official module already handles readiness.",[30,1001,1002,1006],{},[50,1003,1005],{"href":1004},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Freplacing-sleep-based-waits-with-polling-assertions\u002F","Replacing Sleep-Based Waits with Polling Assertions"," — the same principle inside test bodies.",[30,1008,1009,1013],{},[50,1010,1012],{"href":1011},"\u002Fsystematic-debugging-performance-profiling\u002Fdebugging-tests-in-ci-and-containers\u002Fcapturing-artifacts-from-a-failed-ci-test-run\u002F","Capturing Artifacts from a Failed CI Test Run"," — keeping container logs beyond the fixture.",[10,1015,1016,1017],{},"← Back to ",[50,1018,991],{"href":52},[1020,1021,1022],"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);}",{"title":67,"searchDepth":80,"depth":80,"links":1024},[1025,1026,1027,1028,1029,1030,1031,1032,1033],{"id":24,"depth":80,"text":25},{"id":56,"depth":80,"text":57},{"id":380,"depth":80,"text":381},{"id":393,"depth":80,"text":394},{"id":450,"depth":80,"text":451},{"id":616,"depth":80,"text":617},{"id":840,"depth":80,"text":841},{"id":955,"depth":80,"text":956},{"id":983,"depth":80,"text":984},"Replace startup sleeps in Testcontainers fixtures with real readiness signals: log lines, port probes, health queries, Docker health checks, and bounded retries.","md",{"slug":1037,"type":1038,"breadcrumb":1039,"datePublished":1040,"dateModified":1040,"faq":1041,"howto":1048},"waiting-for-container-readiness-without-sleep","article","Container Readiness","2026-09-18",[1042,1044,1046],{"q":961,"a":1043},"Docker reports a container as running once its entry process has started. The service inside may still be initialising a data directory, replaying a log, or waiting for a cluster to form, and connections during that window are refused or reset. Readiness has to be checked against the service itself.",{"q":967,"a":1045},"A real request that exercises the service: SELECT 1 for a database, a PING for Redis, a metadata request for Kafka. A log line is a good second choice because it is emitted by the service when it believes it is ready. An open port is the weakest signal, since many services bind before they can serve.",{"q":979,"a":1047},"Long enough for a cold start on a loaded runner — thirty to sixty seconds for most databases, longer for Kafka or Elasticsearch — because it only matters when something is wrong. The poll returns as soon as the service is ready, so a generous timeout costs nothing on healthy runs.",{"name":1049,"description":1050,"steps":1051},"How to wait for a container to become ready","Pick the strongest available readiness signal, poll it with a bounded retry, and fail with the container's logs when it never arrives.",[1052,1055,1058,1061,1064],{"name":1053,"text":1054},"Prefer the module's built-in wait strategy","Use the official Testcontainers module for the service where one exists, since its readiness check is already correct.",{"name":1056,"text":1057},"Otherwise wait for a specific log line","Use wait_for_logs with a pattern the service prints only when it can accept work.",{"name":1059,"text":1060},"Confirm with one real request","After the log line, make a single real query or request so a premature log message cannot cause a flaky first test.",{"name":1062,"text":1063},"Bound every wait","Give each readiness check an explicit timeout so a service that never starts fails the session promptly.",{"name":1065,"text":1066},"Attach the container logs on failure","When readiness fails, print the container's logs so the reason is in the test output rather than lost with the container.","\u002Fintegration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002Fwaiting-for-container-readiness-without-sleep",{"title":5,"description":1034},"integration-database-and-service-testing\u002Fspinning-up-services-with-testcontainers\u002Fwaiting-for-container-readiness-without-sleep\u002Findex","Lc7C8DjMlTw7CrpzlNgd6Hr_GAinklsYvDWL6oRbGr0",1789718767481]