<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Raj Gahoi</title>
    <description>The latest articles on DEV Community by Raj Gahoi (@rajgahoi).</description>
    <link>https://dev.to/rajgahoi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4206899%2Fc33f7fd3-c2a4-417e-99f5-1854c2bf882c.jpg</url>
      <title>DEV Community: Raj Gahoi</title>
      <link>https://dev.to/rajgahoi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rajgahoi"/>
    <language>en</language>
    <item>
      <title>Optimizing Async Event Loops: How I Dropped Server Shutdown Latency from 200ms to &lt;1ms in Uvicorn</title>
      <dc:creator>Raj Gahoi</dc:creator>
      <pubDate>Sun, 25 Oct 2026 16:23:15 +0000</pubDate>
      <link>https://dev.to/rajgahoi/optimizing-async-event-loops-how-i-dropped-server-shutdown-latency-from-200ms-to-1ms-in-uvicorn-5c0b</link>
      <guid>https://dev.to/rajgahoi/optimizing-async-event-loops-how-i-dropped-server-shutdown-latency-from-200ms-to-1ms-in-uvicorn-5c0b</guid>
      <description>&lt;h1&gt;
  
  
  Optimizing Async Event Loops: How I Dropped Server Shutdown Latency from 200ms to 1ms in Uvicorn
&lt;/h1&gt;

&lt;p&gt;In high-concurrency microservices, server shutdown latency is often treated as an afterthought. Engineers spend months shaving microseconds off request handlers, yet accept multi-second teardown delays during rolling container deployments, CI runner teardowns, and automated test harnesses.&lt;/p&gt;

&lt;p&gt;While profiling ASGI server lifecycles in &lt;a href="https://github.com/Kludex/uvicorn" rel="noopener noreferrer"&gt;Uvicorn&lt;/a&gt;, the lightning-fast ASGI server powering &lt;a href="https://github.com/fastapi/fastapi" rel="noopener noreferrer"&gt;FastAPI&lt;/a&gt; and &lt;a href="https://github.com/Kludex/starlette" rel="noopener noreferrer"&gt;Starlette&lt;/a&gt;, I investigated an architectural latency tax: &lt;strong&gt;idle shutdown requests could experience 100–200ms of delay&lt;/strong&gt; when the server relied on periodic sleep polling.&lt;/p&gt;

&lt;p&gt;By exploring a wakeable &lt;code&gt;asyncio.Event&lt;/code&gt; synchronization primitive in connection with &lt;a href="https://github.com/Kludex/uvicorn/pull/3238" rel="noopener noreferrer"&gt;Uvicorn PR #3238&lt;/a&gt;, I investigated how to reduce this delay from approximately 200ms to sub-millisecond response times.&lt;/p&gt;

&lt;p&gt;The reported benchmark results include a median latency of &lt;strong&gt;0.42ms&lt;/strong&gt;, representing an approximately &lt;strong&gt;235× improvement&lt;/strong&gt; over a 98.6ms baseline.&lt;/p&gt;

&lt;p&gt;Here is the root-cause analysis, the event-loop mechanics, the architectural comparison, and the reported benchmark numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Anatomy of the Bug: The 200ms Sleep Tax
&lt;/h2&gt;

&lt;p&gt;Uvicorn's main server lifecycle coordinates socket binding, lifespan events, signal handling, and connection management.&lt;/p&gt;

&lt;p&gt;In a polling-based design, the server repeatedly checks whether it should exit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simplified polling-based implementation
&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_exit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever an operator requests shutdown, changes the exit state, or sends a termination signal, the server must eventually resume execution and inspect that state.&lt;/p&gt;

&lt;p&gt;If the coroutine is suspended inside &lt;code&gt;asyncio.sleep()&lt;/code&gt;, the shutdown request may remain unnoticed until the sleep completes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem with Periodic Sleep Polling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time (ms) ─────────────────────────────────────────────►

0 ms                    100 ms                   200 ms
 │                         │                        │
 ▼                         ▼                        ▼
Sleep begins           Timer expires           Next check

             ▲
             │ Shutdown requested at 42 ms
             │
             └──── Up to 158 ms waiting for the timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual delay depends on the polling interval, when the shutdown request arrives, and how quickly the event loop schedules the coroutine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Unpredictable latency:&lt;/strong&gt; A shutdown request arriving immediately after a sleep begins may wait almost the entire interval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compounding test-suite overhead:&lt;/strong&gt; Repeatedly starting and stopping application servers can accumulate unnecessary waiting time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container termination:&lt;/strong&gt; During Kubernetes rollouts and other containerized deployments, prompt shutdown signaling helps the application begin its termination procedure sooner. It does not, by itself, guarantee faster completion of graceful shutdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer productivity:&lt;/strong&gt; Reducing unnecessary teardown delays can improve integration-test turnaround and local development workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architectural Comparison
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before: Polling Loop
&lt;/h3&gt;

&lt;p&gt;The following sequence illustrates a polling-based implementation.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant Loop as asyncio Event Loop
    participant Server as Uvicorn Server Loop
    participant Caller as Shutdown Trigger

    Server-&amp;gt;&amp;gt;Loop: await asyncio.sleep(0.2)
    Note over Server,Loop: Coroutine is suspended
    Caller-&amp;gt;&amp;gt;Server: Request shutdown
    Note over Server: Exit state changes
    Loop--&amp;gt;&amp;gt;Server: Sleep completes
    Server-&amp;gt;&amp;gt;Server: Check should_exit
    Server-&amp;gt;&amp;gt;Server: Begin graceful shutdown&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The problem is that changing a Boolean flag does not automatically wake a coroutine suspended in &lt;code&gt;asyncio.sleep()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The coroutine must resume before it can inspect the updated state.&lt;/p&gt;

&lt;h3&gt;
  
  
  After: Event-Driven Wake-Up
&lt;/h3&gt;

&lt;p&gt;An event-driven design provides an explicit notification mechanism.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant Loop as asyncio Event Loop
    participant Server as Server Loop
    participant Event as asyncio.Event
    participant Caller as Shutdown Trigger

    Server-&amp;gt;&amp;gt;Event: await event.wait()
    Note over Server,Event: Coroutine suspends until signaled
    Caller-&amp;gt;&amp;gt;Event: event.set()
    Event--&amp;gt;&amp;gt;Loop: Schedule waiting coroutine
    Loop--&amp;gt;&amp;gt;Server: Resume coroutine
    Server-&amp;gt;&amp;gt;Server: Check shutdown state
    Server-&amp;gt;&amp;gt;Server: Begin graceful shutdown&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Python's &lt;a href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event" rel="noopener noreferrer"&gt;&lt;code&gt;asyncio.Event&lt;/code&gt;&lt;/a&gt; is designed for signaling between asynchronous tasks.&lt;/p&gt;

&lt;p&gt;Instead of periodically checking whether shutdown has been requested, the server can wait for a notification and resume when the event is set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Event-driven notification removes the need to wait for the polling interval, but the coroutine still depends on event-loop scheduling. Sub-millisecond latency is a benchmark result, not a guaranteed property of &lt;code&gt;asyncio.Event&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Implementation
&lt;/h2&gt;

&lt;p&gt;The objective is to make shutdown signaling responsive without busy-waiting or continuously consuming CPU resources.&lt;/p&gt;

&lt;p&gt;The following examples illustrate the design. They are simplified examples, not a verified reproduction of the exact production diff in PR #3238.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add a Shutdown Event
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_exit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event represents a notification that the server's main loop should stop waiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Initialize the Event During Startup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_exit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initializing the event within the asynchronous lifecycle makes its ownership and initialization point explicit.&lt;/p&gt;

&lt;p&gt;A production implementation must also account for startup failures, repeated invocations, and the server's existing lifecycle guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Replace Periodic Polling
&lt;/h3&gt;

&lt;p&gt;A simple event-driven loop can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_exit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example assumes that the event is the only thing the loop needs to await.&lt;/p&gt;

&lt;p&gt;If the server must also perform periodic work, manage reloads, or observe worker state, that work must be incorporated into the design rather than silently removed.&lt;/p&gt;

&lt;p&gt;For example, a timeout can be used when periodic maintenance is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_exit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Perform required periodic maintenance.
&lt;/span&gt;            &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python's &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.wait_for" rel="noopener noreferrer"&gt;&lt;code&gt;asyncio.wait_for&lt;/code&gt;&lt;/a&gt; provides timeout-based waiting.&lt;/p&gt;

&lt;p&gt;A timeout should not be added without a reason. If no periodic work is needed, waiting directly on the event avoids unnecessary timer wake-ups.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Signal the Event During Shutdown
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request_shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_exit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;set()&lt;/code&gt; wakes tasks waiting on the event.&lt;/p&gt;

&lt;p&gt;If shutdown can be requested from another thread, the implementation must respect asyncio's thread-safety requirements. A common pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_soon_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the official &lt;a href="https://docs.python.org/3/library/asyncio-eventloop.html" rel="noopener noreferrer"&gt;asyncio event-loop documentation&lt;/a&gt; for details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Critical Edge Cases
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Lazy initialization:&lt;/strong&gt; Create and use the event in an appropriate asynchronous lifecycle context, accounting for the Python versions and process model supported by the application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Periodic maintenance:&lt;/strong&gt; If the loop is responsible for health checks, worker monitoring, or reload behavior, preserve those responsibilities in the new design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal handling:&lt;/strong&gt; Ensure termination signals reliably reach the event-loop thread and trigger the appropriate shutdown procedure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated shutdown requests:&lt;/strong&gt; Make shutdown handling idempotent so multiple signals do not cause inconsistent lifecycle transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful shutdown:&lt;/strong&gt; Waking the main loop is not equivalent to completing shutdown. Active requests, connection draining, application lifespan hooks, and resource cleanup may still take time.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Benchmark Results
&lt;/h2&gt;

&lt;p&gt;The following table contains the measurements reported in this investigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;These figures have not been independently verified here against the linked pull request.&lt;/strong&gt; Reproduce the benchmark against the relevant source revision before presenting the results as independently confirmed findings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Legacy Polling&lt;/th&gt;
&lt;th&gt;Event-Driven Approach&lt;/th&gt;
&lt;th&gt;Reported Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minimum shutdown latency&lt;/td&gt;
&lt;td&gt;12.4 ms&lt;/td&gt;
&lt;td&gt;0.18 ms&lt;/td&gt;
&lt;td&gt;68.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median (p50)&lt;/td&gt;
&lt;td&gt;98.6 ms&lt;/td&gt;
&lt;td&gt;0.42 ms&lt;/td&gt;
&lt;td&gt;234.8×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99th percentile (p99)&lt;/td&gt;
&lt;td&gt;198.2 ms&lt;/td&gt;
&lt;td&gt;0.89 ms&lt;/td&gt;
&lt;td&gt;222.7×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle CPU utilization&lt;/td&gt;
&lt;td&gt;0.02%&lt;/td&gt;
&lt;td&gt;0.02%&lt;/td&gt;
&lt;td&gt;No reported change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100-test teardown suite&lt;/td&gt;
&lt;td&gt;18.42 s&lt;/td&gt;
&lt;td&gt;0.12 s&lt;/td&gt;
&lt;td&gt;153.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speedup ratios are calculated from the reported latency values and rounded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shutdown Latency Distribution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lower latency is better.

Legacy polling (p99)
████████████████████████████████████████ 198.2 ms

Event-driven approach (p99)
▏ 0.89 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to Interpret These Results
&lt;/h3&gt;

&lt;p&gt;The reported median decreases from 98.6ms to 0.42ms.&lt;/p&gt;

&lt;p&gt;That corresponds to approximately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;98.6 / 0.42 = 234.8× faster&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reported p99 decreases from 198.2ms to 0.89ms, corresponding to approximately 222.7× faster.&lt;/p&gt;

&lt;p&gt;However, a reproducible performance report should document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The exact source commit and Python version&lt;/li&gt;
&lt;li&gt;Operating system and hardware&lt;/li&gt;
&lt;li&gt;The shutdown-trigger mechanism&lt;/li&gt;
&lt;li&gt;The number of iterations and warm-up procedure&lt;/li&gt;
&lt;li&gt;Whether the measurement covers idle-loop wake-up or complete graceful shutdown&lt;/li&gt;
&lt;li&gt;Whether active connections and application hooks are involved&lt;/li&gt;
&lt;li&gt;The method used to measure CPU utilization&lt;/li&gt;
&lt;li&gt;Raw measurements and percentile calculation methods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reported 100-test teardown result also requires an isolated benchmark to establish whether the improvement comes entirely from shutdown signaling or from other differences in test execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Key Engineering Lessons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Avoid Unnecessary Polling in Event-Driven Systems
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;asyncio.sleep()&lt;/code&gt; is useful for scheduled work and periodic tasks.&lt;/p&gt;

&lt;p&gt;However, when a coroutine is waiting for a specific state transition, an explicit synchronization primitive may be more appropriate than repeatedly checking a Boolean flag.&lt;/p&gt;

&lt;p&gt;The right choice depends on the lifecycle requirements, not a blanket rule that sleeping is always an anti-pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Low CPU Usage Does Not Mean Low Latency
&lt;/h3&gt;

&lt;p&gt;A suspended coroutine can consume almost no CPU while still introducing significant response delay.&lt;/p&gt;

&lt;p&gt;Performance engineering must measure both resource utilization and responsiveness.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Shutdown Responsiveness and Shutdown Duration Are Different
&lt;/h3&gt;

&lt;p&gt;A server can respond to a shutdown request quickly but still take time to drain connections and clean up resources.&lt;/p&gt;

&lt;p&gt;Measure these phases separately to avoid optimizing the wrong bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Performance Claims Need Reproducible Evidence
&lt;/h3&gt;

&lt;p&gt;Benchmark improvements are most useful when other engineers can reproduce them.&lt;/p&gt;

&lt;p&gt;A strong report includes the source revision, test harness, environment, sample count, raw results, and measurement methodology.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Small Lifecycle Improvements Can Compound
&lt;/h3&gt;

&lt;p&gt;In CI environments and automated integration tests, unnecessary waits may be repeated many times.&lt;/p&gt;

&lt;p&gt;Even a small improvement can become meaningful when multiplied across a large number of server lifecycle operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Server shutdown latency deserves the same engineering attention as request-processing latency.&lt;/p&gt;

&lt;p&gt;Periodic polling can delay a shutdown response when a coroutine must wait for a timer before checking its state.&lt;/p&gt;

&lt;p&gt;An event-driven approach using Python's &lt;code&gt;asyncio.Event&lt;/code&gt; provides an explicit notification mechanism that can remove this polling delay.&lt;/p&gt;

&lt;p&gt;The implementation must still preserve signal handling, worker monitoring, active-connection management, and graceful-shutdown behavior.&lt;/p&gt;

&lt;p&gt;The broader lesson is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure the lifecycle, identify the actual bottleneck, and choose synchronization primitives that match the behavior your system needs.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Article Author Attribution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Written by &lt;a href="https://github.com/RajGahoi" rel="noopener noreferrer"&gt;Raj Gahoi&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;B.Tech CSE | Open-Source Contributor | Software Developer&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Connect with me on &lt;a href="https://github.com/RajGahoi" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/rajgahoi0411/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Kludex/uvicorn" rel="noopener noreferrer"&gt;Uvicorn repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Kludex/uvicorn/pull/3238" rel="noopener noreferrer"&gt;Uvicorn PR #3238&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/asyncio-sync.html" rel="noopener noreferrer"&gt;Python asyncio synchronization primitives&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/asyncio-task.html" rel="noopener noreferrer"&gt;Python asyncio tasks and timeouts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.python.org/3/library/asyncio-eventloop.html" rel="noopener noreferrer"&gt;Python asyncio event loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fastapi/fastapi" rel="noopener noreferrer"&gt;FastAPI repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Kludex/starlette" rel="noopener noreferrer"&gt;Starlette repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>performance</category>
      <category>python</category>
    </item>
  </channel>
</rss>
