{"componentChunkName":"component---src-templates-blog-post-js","path":"/blog/2026-07-10-fastapi-blocking-event-loop/","result":{"data":{"site":{"siteMetadata":{"title":"M.Hassan Ahmed","author":"Hassan11196"}},"markdownRemark":{"id":"4d38fdf2-a5e1-5a29-9e92-2a8df98b7b8b","excerpt":"The bug looks impossible the first time you see it. Your FastAPI service handles hundreds of requests a second in load tests. Then someone adds one endpoint…","html":"<p>The bug looks impossible the first time you see it. Your FastAPI service handles hundreds of requests a second in load tests. Then someone adds one endpoint that resizes an image or verifies a JWT with a slow library, and suddenly <em>every</em> request gets slow, not just that one. Health checks time out. A streaming chat that was landing tokens in 200 ms now hangs for seconds. Nothing changed in the other routes, but they all got worse together.</p>\n<p>The cause is almost always the same: one blocking call sitting on the event loop. This post is for engineers running an <a href=\"https://fastapi.tiangolo.com/async/\">async FastAPI</a> backend who have hit that “why is the whole server slow” moment, or want to avoid it. It covers how the event loop works, how FastAPI decides where your route runs, a repro you can run on your laptop, and the right fix for each kind of slow work.</p>\n<p>I hit this building the FastAPI backends behind <a href=\"/project/cloud-canvas-ai/\">CloudCanvasAI</a> and <a href=\"/project/archi/\">Archi</a>. There, a stalled event loop does not just slow one request down; it freezes the live <a href=\"/blog/2026-06-30-fastapi-sse-streaming-llm/\">SSE stream</a> (Server-Sent Events) that every user is watching.</p>\n<h2>One thread runs every request</h2>\n<p>FastAPI is async, and async in Python means one event loop running on one thread per worker process. That loop is not doing many things at once. It does one thing at a time and switches between them very fast, at the points where your code says <code class=\"language-text\">await</code>.</p>\n<p>That is the whole contract. When an <code class=\"language-text\">async def</code> route hits <code class=\"language-text\">await client.get(url)</code>, it tells the loop “I am waiting on the network; go run someone else.” The loop parks that request, picks up another, and comes back when the network call is ready. A hundred requests can be in flight because, at any instant, ninety-nine of them are parked at an <code class=\"language-text\">await</code> and only one is actually executing.</p>\n<p>\n  <a\n    class=\"gatsby-resp-image-link\"\n    href=\"/static/22a8e5bdaa8cd775a2a3f469fa07341d/df13e/event-loop-blocking.png\"\n    style=\"display: block\"\n    target=\"_blank\"\n    rel=\"noopener\"\n  >\n  \n  <span\n    class=\"gatsby-resp-image-wrapper\"\n    style=\"position: relative; display: block; margin: 7vw 0; max-width: 1360px; margin-left: auto; margin-right: auto;\"\n  >\n    <span\n      class=\"gatsby-resp-image-background-image\"\n      style=\"padding-bottom: 40%; position: relative; bottom: 0; left: 0; background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAIAAAB2/0i6AAAACXBIWXMAAAsSAAALEgHS3X78AAABiElEQVQY002Qa0+jQBSG66q72jIUkLYDw62wyrUOA5RLS1ptixa8JIqf/P8/xJk1m5g8ZzJzTp6cNzMQp7ZqhooRqFYIdV81A3rONA/qHm0qBhshK6LPmeaOZesngyv4N6uapNgXmzapDqQ8pFXDKJu8PtJLtnpYrh9JvnOjUpjMKeNvqExLpC3Z4gVjLJpjSacMBfU3P70AcMShEUBDHg4B/AOmFwIcCSrgdSDoTBZnju2vrpM6bPbe3T3efER1T5KXCr8H8U4vM5RmDnk08cMt7irce/heKzM1TmiEgTC1oUWQl9r1Ss8LN+sc0ibpa036MN6Z5VJLs+vkaOEG43Yd90G8N6rlf3kylxVHosl5UxStS8m4vDLPBTTgJidAOeXQGYd+cfAUwBOGfD6ShpwCeI3Flv7Fdsg6aPb+dtsXm8/17i1dtT7u3EXrLVqXcXQXT2609YucvJt2LiGfbab/JqNg5tzqRY7i+ODhLkrbRdaFpLsJn5kcffPkBhs/Z/J8Kakelb8AbQ5H3YgTG7YAAAAASUVORK5CYII='); background-size: cover; display: block;\"\n    >\n      <picture>\n        <source\n          srcset=\"/static/22a8e5bdaa8cd775a2a3f469fa07341d/51a8e/event-loop-blocking.webp 340w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/713b7/event-loop-blocking.webp 680w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/54376/event-loop-blocking.webp 1360w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/0be89/event-loop-blocking.webp 2040w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/71194/event-loop-blocking.webp 2720w\"\n          sizes=\"(max-width: 1360px) 100vw, 1360px\"\n          type=\"image/webp\"\n        />\n        <source\n          srcset=\"/static/22a8e5bdaa8cd775a2a3f469fa07341d/ad208/event-loop-blocking.png 340w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/a5a26/event-loop-blocking.png 680w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/60356/event-loop-blocking.png 1360w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/bb4cf/event-loop-blocking.png 2040w,\n/static/22a8e5bdaa8cd775a2a3f469fa07341d/df13e/event-loop-blocking.png 2720w\"\n          sizes=\"(max-width: 1360px) 100vw, 1360px\"\n          type=\"image/png\"\n        />\n        <img\n          class=\"gatsby-resp-image-image\"\n          style=\"width: 100%; height: 100%; margin: 0; vertical-align: middle; position: absolute; top: 0; left: 0; box-shadow: inset 0px 0px 0px 400px white;\"\n          src=\"/static/22a8e5bdaa8cd775a2a3f469fa07341d/60356/event-loop-blocking.png\"\n          alt=\"A single event loop serves every request by interleaving them at await points; when one request makes a blocking call with no await, the loop cannot switch and every other request, including live SSE streams, stalls until it returns\"\n          title=\"\"\n          src=\"/static/22a8e5bdaa8cd775a2a3f469fa07341d/60356/event-loop-blocking.png\"\n        />\n      </picture>\n      </span>\n  </span>\n  \n  </a>\n    </p>\n<p>The failure mode falls straight out of that design. If one request runs code that takes 500 ms and never yields, the loop cannot switch away from it, because there is no <code class=\"language-text\">await</code> to hand control back. For that half second the single thread is busy, and every other request, no matter how cheap, waits in line behind it. You did not slow down one endpoint. You paused the server.</p>\n<h2>The signature you write decides where code runs</h2>\n<p>FastAPI gives you a way out of this, and most people use it without knowing. It checks whether your path operation (the route function) is <code class=\"language-text\">def</code> or <code class=\"language-text\">async def</code> and schedules the two differently. <a href=\"https://www.starlette.io/\">Starlette</a>, the framework FastAPI sits on, does the actual work. There are three cases:</p>\n<p><img src=\"/4499d8d853fa089f8c8c1084004d2520/route-scheduling.svg\" alt=\"Three ways a FastAPI route is scheduled: async def with await runs on the event loop and is safe; a plain def route is offloaded to a threadpool and is safe until the pool fills; an async def containing a blocking call runs on the loop and holds it, which is the trap\"></p>\n<ul>\n<li><strong><code class=\"language-text\">async def</code> + real <code class=\"language-text\">await</code>.</strong> Runs directly on the event loop. This is correct and concurrent, as long as every slow thing inside it is actually awaited.</li>\n<li><strong>Plain <code class=\"language-text\">def</code>.</strong> FastAPI runs it in a threadpool instead of on the loop, using <a href=\"https://anyio.readthedocs.io/\"><code class=\"language-text\">anyio</code></a> under the hood. Your blocking <code class=\"language-text\">requests.get()</code> still blocks, but it blocks a worker thread, not the loop, so other requests keep flowing. The catch is that the pool is bounded: Starlette’s default limiter is <strong>40 threads</strong>, and once they are all busy, new sync requests queue.</li>\n<li><strong><code class=\"language-text\">async def</code> with a blocking call inside.</strong> This is the trap. You wrote <code class=\"language-text\">async def</code>, so FastAPI trusts you and runs it on the loop. Then you called something synchronous, like <code class=\"language-text\">requests.get()</code> or a CPU-heavy function, with no <code class=\"language-text\">await</code>. Now you have the worst of both: on the loop, and holding it.</li>\n</ul>\n<p>The rule that falls out of this: <code class=\"language-text\">async def</code> is a promise that you will not block. If you cannot keep that promise for a given call, either change the route to plain <code class=\"language-text\">def</code> or offload the slow part explicitly.</p>\n<h2>A repro you can run</h2>\n<p>Here is the trap in about twenty lines. <code class=\"language-text\">time.sleep</code> stands in for any blocking work: a synchronous DB driver, a hashing round, a PIL resize, a <code class=\"language-text\">requests</code> call.</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">import</span> time\n<span class=\"token keyword\">from</span> fastapi <span class=\"token keyword\">import</span> FastAPI\n\napp <span class=\"token operator\">=</span> FastAPI<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/blocking\"</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">def</span> <span class=\"token function\">blocking</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    time<span class=\"token punctuation\">.</span>sleep<span class=\"token punctuation\">(</span><span class=\"token number\">1</span><span class=\"token punctuation\">)</span>          <span class=\"token comment\"># blocks the event loop for a full second</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span><span class=\"token string\">\"ok\"</span><span class=\"token punctuation\">:</span> <span class=\"token boolean\">True</span><span class=\"token punctuation\">}</span>\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/health\"</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">def</span> <span class=\"token function\">health</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span><span class=\"token string\">\"status\"</span><span class=\"token punctuation\">:</span> <span class=\"token string\">\"up\"</span><span class=\"token punctuation\">}</span></code></pre></div>\n<p>Run it with <code class=\"language-text\">uvicorn main:app</code>, then fire ten <code class=\"language-text\">/blocking</code> requests at once while timing <code class=\"language-text\">/health</code>:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\"><span class=\"token comment\"># hammer the blocking route</span>\n<span class=\"token keyword\">for</span> <span class=\"token for-or-select variable\">i</span> <span class=\"token keyword\">in</span> <span class=\"token variable\"><span class=\"token variable\">$(</span><span class=\"token function\">seq</span> <span class=\"token number\">10</span><span class=\"token variable\">)</span></span><span class=\"token punctuation\">;</span> <span class=\"token keyword\">do</span> <span class=\"token function\">curl</span> -s localhost:8000/blocking <span class=\"token operator\">&amp;</span> <span class=\"token keyword\">done</span>\n\n<span class=\"token comment\"># meanwhile, time a trivial health check</span>\n<span class=\"token function\">time</span> <span class=\"token function\">curl</span> -s localhost:8000/health</code></pre></div>\n<p><code class=\"language-text\">/health</code> does nothing and should answer instantly. Instead it waits behind the queue of <code class=\"language-text\">time.sleep</code> calls, because all of them are stacked on the one loop thread. With ten blocking requests, <code class=\"language-text\">/health</code> can take several seconds. That is the exact shape of the production incident, reproduced on your laptop.</p>\n<p>Now change one word on the blocking route, <code class=\"language-text\">async def</code> to <code class=\"language-text\">def</code>, and run it again. <code class=\"language-text\">/health</code> stays fast under the same load, because FastAPI pushed the <code class=\"language-text\">def</code> route onto the threadpool and left the loop free. Same blocking <code class=\"language-text\">sleep</code>, completely different blast radius.</p>\n<h2>Fixing it: match the tool to the kind of work</h2>\n<p>Dropping to <code class=\"language-text\">def</code> is the quick patch, but it is not always the right one. It also does not help if the blocking call lives deep inside an <code class=\"language-text\">async def</code> you cannot easily convert. The durable fix depends on <em>what kind</em> of slow work it is.</p>\n<p><img src=\"/442e0a57abaaf2ef3e616a4a0f9ca42d/offload-decision.svg\" alt=\"A decision guide: network or DB calls with an async client should stay on the loop and use await; a sync-only library that is mostly I/O should be pushed to the threadpool with asyncio.to_thread; CPU-bound work such as parsing or embeddings should go to a process pool via run_in_executor\"></p>\n<h3>I/O with an async client available</h3>\n<p>This is the clean case. Swap the synchronous library for its async counterpart and <code class=\"language-text\">await</code> it: <a href=\"https://www.python-httpx.org/async/\"><code class=\"language-text\">httpx</code></a> instead of <code class=\"language-text\">requests</code>, and <code class=\"language-text\">asyncpg</code> or SQLAlchemy’s async engine instead of a sync driver. The work stays on the loop, and the loop stays free. In this route, the <code class=\"language-text\">await</code> on <code class=\"language-text\">client.get</code> is where the loop gets control back:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">import</span> httpx\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/fetch\"</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">def</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    <span class=\"token keyword\">async</span> <span class=\"token keyword\">with</span> httpx<span class=\"token punctuation\">.</span>AsyncClient<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token keyword\">as</span> client<span class=\"token punctuation\">:</span>\n        r <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> client<span class=\"token punctuation\">.</span>get<span class=\"token punctuation\">(</span><span class=\"token string\">\"https://example.com/data\"</span><span class=\"token punctuation\">)</span>  <span class=\"token comment\"># yields at the await</span>\n    <span class=\"token keyword\">return</span> r<span class=\"token punctuation\">.</span>json<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span></code></pre></div>\n<h3>A sync-only library doing I/O</h3>\n<p>Sometimes there is no async version, or rewriting is not worth it. Push that one call to the threadpool with <a href=\"https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread\"><code class=\"language-text\">asyncio.to_thread</code></a> (Python 3.9+) or Starlette’s <code class=\"language-text\">run_in_threadpool</code>. This is safe for I/O. CPython’s <a href=\"https://docs.python.org/3/glossary.html#term-global-interpreter-lock\">GIL</a> (global interpreter lock) lets only one thread run Python code at a time, but CPython releases it while a thread waits on a socket, so the loop keeps running. Here <code class=\"language-text\">requests.get</code> runs on a worker thread while the route awaits its result:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">import</span> asyncio\n<span class=\"token keyword\">import</span> requests\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>get</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/legacy\"</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">def</span> <span class=\"token function\">legacy</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    <span class=\"token comment\"># requests has no async API; run it off the loop</span>\n    data <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> asyncio<span class=\"token punctuation\">.</span>to_thread<span class=\"token punctuation\">(</span>requests<span class=\"token punctuation\">.</span>get<span class=\"token punctuation\">,</span> <span class=\"token string\">\"https://example.com/data\"</span><span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">return</span> data<span class=\"token punctuation\">.</span>json<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span></code></pre></div>\n<h3>CPU-bound work</h3>\n<p>This is the one people get wrong: parsing a big file, hashing, resizing an image, running a local embedding model. Threads do <em>not</em> save you here, because CPU work holds the GIL and one busy thread starves the loop anyway. Send it to a separate process instead. Below, <code class=\"language-text\">run_in_executor</code> hands <code class=\"language-text\">expensive_embed</code> to a process pool and awaits the result:</p>\n<div class=\"gatsby-highlight\" data-language=\"python\"><pre class=\"language-python\"><code class=\"language-python\"><span class=\"token keyword\">import</span> asyncio\n<span class=\"token keyword\">from</span> concurrent<span class=\"token punctuation\">.</span>futures <span class=\"token keyword\">import</span> ProcessPoolExecutor\n\npool <span class=\"token operator\">=</span> ProcessPoolExecutor<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n\n<span class=\"token decorator annotation punctuation\">@app<span class=\"token punctuation\">.</span>post</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"/embed\"</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">async</span> <span class=\"token keyword\">def</span> <span class=\"token function\">embed</span><span class=\"token punctuation\">(</span>text<span class=\"token punctuation\">:</span> <span class=\"token builtin\">str</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">:</span>\n    loop <span class=\"token operator\">=</span> asyncio<span class=\"token punctuation\">.</span>get_running_loop<span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n    vector <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> loop<span class=\"token punctuation\">.</span>run_in_executor<span class=\"token punctuation\">(</span>pool<span class=\"token punctuation\">,</span> expensive_embed<span class=\"token punctuation\">,</span> text<span class=\"token punctuation\">)</span>\n    <span class=\"token keyword\">return</span> <span class=\"token punctuation\">{</span><span class=\"token string\">\"dim\"</span><span class=\"token punctuation\">:</span> <span class=\"token builtin\">len</span><span class=\"token punctuation\">(</span>vector<span class=\"token punctuation\">)</span><span class=\"token punctuation\">}</span></code></pre></div>\n<h2>Failure modes that hide the real cause</h2>\n<p><strong>It passes every load test, then falls over in prod.</strong> A single-user test never has two requests competing for the loop, so a blocked loop looks fast; you only see the problem under concurrency. Test with at least a handful of parallel connections, and keep a trivial <code class=\"language-text\">/health</code> route in the mix as your canary.</p>\n<p><strong>The threadpool is a ceiling, not a fix.</strong> Moving everything to <code class=\"language-text\">def</code> routes feels like it solved the problem, but the <a href=\"https://anyio.readthedocs.io/en/stable/threads.html\">anyio</a> pool defaults to 40 threads. Under enough concurrent slow requests, request 41 waits for a free thread. For genuinely high-throughput blocking work you need real async, not a bigger pool.</p>\n<p><strong>More <code class=\"language-text\">--workers</code> masks it.</strong> Running <code class=\"language-text\">uvicorn --workers 4</code> gives you four processes, so a blocked loop in one worker affects only a quarter of traffic. That raises your ceiling and hides the bug at low load, but the endpoint still blocks; you have just spread the pain. See the <a href=\"https://www.uvicorn.org/deployment/\">uvicorn deployment docs</a> for what workers actually buy you.</p>\n<p><strong>Streaming makes it obvious and worse.</strong> A blocked loop cannot push SSE bytes for <em>any</em> open stream, so one bad request freezes every user’s live output at once. That is why I care about this more on LLM backends than on plain CRUD services. The <a href=\"/blog/2026-06-30-fastapi-sse-streaming-llm/\">SSE streaming setup</a> I use in CloudCanvasAI stays smooth only because nothing on the hot path blocks the loop between token yields.</p>\n<p>To catch these before prod, turn on asyncio’s debug mode with <code class=\"language-text\">PYTHONASYNCIODEBUG=1</code> or <code class=\"language-text\">asyncio.run(main(), debug=True)</code>. It logs a warning whenever a callback holds the loop longer than <code class=\"language-text\">loop.slow_callback_duration</code> (100 ms by default), which points a finger straight at the offending call.</p>\n<h2>What I would do differently</h2>\n<p>Early on, when the server got slow under load, I reached for <code class=\"language-text\">--workers</code> and a bigger threadpool, treating it as a capacity problem. It usually is not. It is one specific line that blocks the loop, and adding processes just buys headroom while the real fix is a one-line offload or an async client. Now, when latency spreads across unrelated endpoints, the first thing I check is whether some <code class=\"language-text\">async def</code> on the hot path is calling something synchronous. That single question resolves this class of bug far more often than any amount of horizontal scaling.</p>\n<p>The second habit: decide the boundary early. Every <code class=\"language-text\">async def</code> that touches the outside world gets an async client or an explicit <code class=\"language-text\">to_thread</code>, and anything CPU-heavy goes to a process pool from the start, not after an incident. Keeping the loop clean is much cheaper than finding the one call that dirtied it at 2 a.m.</p>\n<h2>Keep the loop free</h2>\n<p>FastAPI’s speed comes from a single event loop juggling everything, and that same design is why one careless blocking call takes the whole thing down. The framework already hands you the escape hatches: <code class=\"language-text\">def</code> for sync routes, <code class=\"language-text\">to_thread</code> for stray blocking calls, and a process pool for CPU work. The discipline is knowing which kind of work you are looking at, and never quietly parking a slow synchronous call on the loop.</p>\n<p>This is the quiet backbone under the LLM services I’ve built, from the streaming backend in <a href=\"/project/cloud-canvas-ai/\">CloudCanvasAI</a> and <a href=\"/project/gemini-alchemy/\">Gemini Alchemy</a> to the operational console for <a href=\"/project/cms-workflow-operations/\">CMS workflow management</a> at CERN. None of them feel fast because of clever code. They feel fast because the loop is never blocked.</p>\n<hr>\n<p><em>Diagrams by M. Hassan Ahmed, released under CC0. Image credit: original work by the author.</em></p>","frontmatter":{"title":"FastAPI Event Loop Blocking: Sync vs Async","date":"2026-07-10T00:00:00.000Z","description":"One blocking call in a FastAPI route stalls every other request, including live SSE streams. Here is how the event loop breaks, and how to keep it free.","thumbnail":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAAB4klEQVQoz1WRa2+bMBSGkdYmBHzBGLAN2JhAWEISAgtJk3TT9mnStK3t1P//X3aSTtMmPRz58j6cI3Bs8fEvRl/63fdx/wzs90/97sfh8Gvcvwz9z8P4fHp4hVutL9b8yTsuTYEpUTi2gayb7XnZP1ab02J7brozbN9353r90GxP7fBYtcdAVCS2b5aDmEGsgIp5SaK5Fxgv0CSEQ42v1QI+ux7OaO6zAjI4tDfLOH6gAXTDpxnIqSovnR5WeWMVi2rK61isg2hBwrl/y7wpgOPRfEKyCc7ucYpDSDQoMCTMAY8ql6gpkTDFjJjS2MOm6FfG5JlL4DYHOatk2eb1Si925aZJS8I0jESCeSaGIj1moqdsPiOK8jQRqVKWx3ZGUhAdQrNPZfU09K/j+DJ++No0x3Jd5Usd9W3xZd9+a+3nLBr8IHNxeueriZ/yqPVAJqkDj4n0UtmNXnR6WcY5hmmxCliV8FbwTRKtobOLJWGKx2DqkBewhVmcGZb3SN4h8c5LEKsQg6+S4UB6JPapcLFwUTL146mvSq3PnWkrqVM5RRJEkIV3BdISFtBTJHps9aGbt7UJo4rxSog143NEtYuki8S1LRaAAy/+B3GrMeN5Ii1PCsIt4QXlBfx5Fyf/h5Pf8EtEs8CjTlAAAAAASUVORK5CYII=","aspectRatio":1.899441340782123,"src":"/static/1e4adbb579bee59ab351a70e64eaa51f/40a76/hero.png","srcSet":"/static/1e4adbb579bee59ab351a70e64eaa51f/c972b/hero.png 340w,\n/static/1e4adbb579bee59ab351a70e64eaa51f/27625/hero.png 680w,\n/static/1e4adbb579bee59ab351a70e64eaa51f/40a76/hero.png 1360w,\n/static/1e4adbb579bee59ab351a70e64eaa51f/ed396/hero.png 2000w","sizes":"(max-width: 1360px) 100vw, 1360px"}}}}}},"pageContext":{"slug":"/2026-07-10-fastapi-blocking-event-loop/","previous":"blog/2026-07-08-cross-encoder-reranking-rag/","next":"blog/2026-07-09-hnsw-vector-search-explained/"}},"staticQueryHashes":["32046230"]}