<?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>Forem: Dip Chakraborty</title>
    <description>The latest articles on Forem by Dip Chakraborty (@dipcb05).</description>
    <link>https://forem.com/dipcb05</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1447416%2Fdf8bfb0f-0050-4aa5-9046-c2b04b5098d7.jpeg</url>
      <title>Forem: Dip Chakraborty</title>
      <link>https://forem.com/dipcb05</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dipcb05"/>
    <language>en</language>
    <item>
      <title>Node.js vs FastAPI Event Loop: A Deep Dive into Async Concurrency</title>
      <dc:creator>Dip Chakraborty</dc:creator>
      <pubDate>Wed, 04 Mar 2026 21:34:50 +0000</pubDate>
      <link>https://forem.com/dipcb05/nodejs-vs-fastapi-event-loop-a-deep-dive-into-async-concurrency-35b1</link>
      <guid>https://forem.com/dipcb05/nodejs-vs-fastapi-event-loop-a-deep-dive-into-async-concurrency-35b1</guid>
      <description>&lt;p&gt;If you build APIs today, two stacks repeatedly appear in production systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;FastAPI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are widely known for handling high-concurrency workloads, and both rely heavily on event-driven architectures.&lt;/p&gt;

&lt;p&gt;At first glance, the two appear almost identical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both rely on non-blocking I/O&lt;/li&gt;
&lt;li&gt;Both encourage asynchronous programming&lt;/li&gt;
&lt;li&gt;Both can handle thousands of concurrent connections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of these similarities, many developers assume they behave in roughly the same way internally.&lt;/p&gt;

&lt;p&gt;But the reality is more nuanced!&lt;/p&gt;

&lt;p&gt;While the high-level philosophy is similar, the execution model, scheduling behavior, and CPU utilization strategy differ significantly.&lt;/p&gt;

&lt;p&gt;Understanding these differences becomes extremely important when designing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large-scale APIs&lt;/li&gt;
&lt;li&gt;real-time systems&lt;/li&gt;
&lt;li&gt;microservices&lt;/li&gt;
&lt;li&gt;AI inference backends&lt;/li&gt;
&lt;li&gt;streaming infrastructures&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architectures
&lt;/h2&gt;

&lt;p&gt;Node.js executes JavaScript on a single thread.&lt;/p&gt;

&lt;p&gt;This design decision was intentional. JavaScript historically ran inside browsers where concurrency was handled through event-driven callbacks rather than threads.&lt;/p&gt;

&lt;p&gt;Node adopted the same model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxszj1snamoczo2gxom9e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxszj1snamoczo2gxom9e.png" alt="Simplest Architecture of Nodejs Event Loop" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Important characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All JavaScript code runs on one thread&lt;/li&gt;
&lt;li&gt;I/O operations are delegated to libuv&lt;/li&gt;
&lt;li&gt;When I/O completes, callbacks are pushed back into the event loop&lt;/li&gt;
&lt;li&gt;This design works extremely well for I/O-heavy workloads, which most web servers are.&lt;/li&gt;
&lt;li&gt;Instead of spawning thousands of threads, Node maintains a single event loop and multiplexes many connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is a trade-off.&lt;/p&gt;

&lt;p&gt;If CPU-heavy work blocks the JavaScript thread, the entire server stalls.&lt;/p&gt;

&lt;p&gt;Simple Example - &lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
while(true) {}&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The Node.js event loop is implemented through libuv, a high-performance C library responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asynchronous I/O&lt;/li&gt;
&lt;li&gt;networking&lt;/li&gt;
&lt;li&gt;file system operations&lt;/li&gt;
&lt;li&gt;timers&lt;/li&gt;
&lt;li&gt;thread pool scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The event loop itself runs through several distinct phases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6lcbm2qia3b59lx0u1h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg6lcbm2qia3b59lx0u1h.png" alt="Nodejs Event Loop Phases" width="800" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Timers Phase: Handles callbacks scheduled via:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setTimeout()&lt;br&gt;
   setInterval()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Pending Callbacks: Processes certain system-level callbacks such as TCP errors.&lt;/p&gt;

&lt;p&gt;Poll Phase: This is the heart of the event loop. In this phase, Node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;waits for I/O events&lt;/li&gt;
&lt;li&gt;retrieves completed operations from the OS&lt;/li&gt;
&lt;li&gt;executes the associated callbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check Phase: Executes callbacks&lt;/p&gt;

&lt;p&gt;Close Callbacks: Handles cleanup events&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbqqnuliszwo577a581i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbqqnuliszwo577a581i.png" alt="nodejs request lifecycle" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lets talk about FastAPI. FastAPI itself is just a framework.&lt;/p&gt;

&lt;p&gt;The real runtime comes from ASGI servers, typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uvicorn&lt;/li&gt;
&lt;li&gt;Hypercorn&lt;/li&gt;
&lt;li&gt;Gunicorn + Uvicorn workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each worker is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a separate OS process&lt;/li&gt;
&lt;li&gt;running its own Python interpreter&lt;/li&gt;
&lt;li&gt;with its own event loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concurrency therefore comes from two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async coroutines inside the worker&lt;/li&gt;
&lt;li&gt;Multiple worker processes across CPU cores&lt;/li&gt;
&lt;li&gt;This means FastAPI can utilize multiple CPU cores naturally, something Node requires cluster mode to achieve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbs5wwr75tjg4pkhwecw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbs5wwr75tjg4pkhwecw0.png" alt="Fast API Architecture" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FastAPI relies on Python's asyncio framework, which implements asynchronous execution using coroutines rather than callbacks.&lt;/p&gt;

&lt;p&gt;A typical endpoint might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app.get("/users")
async def get_users():
    result = await db.fetch_all()
    return result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python creates a coroutine object, which behaves like a pausable computation.&lt;/p&gt;

&lt;p&gt;The event loop schedules and resumes these coroutines as needed. &lt;/p&gt;

&lt;p&gt;Instead of registering callbacks, Python pauses the coroutine and later resumes it.&lt;/p&gt;

&lt;p&gt;This produces a more linear and readable programming model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Felupfcvb3jutxxpltmnf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Felupfcvb3jutxxpltmnf.png" alt="FastAPI Request LifeCycle" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Callback vs Coroutine Model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where the programming model diverges significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js
&lt;/h2&gt;

&lt;p&gt;Early Node applications relied heavily on callbacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db.query(sql, (err, result) =&amp;gt; {
   console.log(result)
})

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This led to the infamous callback hell problem.&lt;/p&gt;

&lt;p&gt;Later improvements introduced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Promises&lt;/li&gt;
&lt;li&gt;async / await
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const result = await db.query(sql)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, internally Node still schedules operations via callback queues.&lt;/p&gt;

&lt;p&gt;The async/await syntax is largely syntactic sugar over Promises and callbacks.&lt;/p&gt;

&lt;p&gt;In the other side, Python’s async system is built around coroutines and cooperative scheduling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;result = await db.query()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Coroutines can pause and resume naturally, complex asynchronous flows often become much easier to reason about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foz6b1xltwuib2rxuy98t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foz6b1xltwuib2rxuy98t.png" alt="Coroutines" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Threads
&lt;/h2&gt;

&lt;p&gt;Another important architectural distinction is thread utilization.&lt;/p&gt;

&lt;p&gt;Node consists of: &lt;br&gt;
1 main JavaScript thread + libuv worker thread pool&lt;/p&gt;

&lt;p&gt;The worker pool handles operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file system access&lt;/li&gt;
&lt;li&gt;DNS lookups&lt;/li&gt;
&lt;li&gt;compression&lt;/li&gt;
&lt;li&gt;cryptography&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default thread pool size: 4 threads&lt;/p&gt;

&lt;p&gt;This can be increased using: &lt;code&gt;UV_THREADPOOL_SIZE&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;However, JavaScript execution itself remains single-threaded.&lt;/p&gt;

&lt;p&gt;Even with a larger thread pool, heavy CPU tasks executed in JavaScript can still block the event loop.&lt;/p&gt;

&lt;p&gt;FastAPI deployments typically rely on multiple worker processes: &lt;br&gt;
Gunicorn + 4 Uvicorn workers&lt;/p&gt;

&lt;p&gt;Each worker contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 OS process&lt;/li&gt;
&lt;li&gt;1 Python interpreter&lt;/li&gt;
&lt;li&gt;1 event loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result:&lt;br&gt;
&lt;code&gt;Worker 1 → CPU core 1&lt;br&gt;
Worker 2 → CPU core 2&lt;br&gt;
Worker 3 → CPU core 3&lt;br&gt;
Worker 4 → CPU core 4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This enables true parallelism across CPU cores, something Python normally struggles with because of the Global Interpreter Lock (GIL).&lt;/p&gt;

&lt;p&gt;But since each worker is a separate process, the GIL becomes irrelevant.&lt;/p&gt;

&lt;p&gt;Now we can summarize the concurrency strategy of each runtime.&lt;/p&gt;

&lt;p&gt;Node.js Concurrency model:&lt;/p&gt;

&lt;p&gt;Single-threaded event loop + Non-blocking I/O&lt;/p&gt;

&lt;p&gt;This works exceptionally well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIs&lt;/li&gt;
&lt;li&gt;streaming services&lt;/li&gt;
&lt;li&gt;real-time applications&lt;/li&gt;
&lt;li&gt;WebSocket servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node excels at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high I/O throughput&lt;/li&gt;
&lt;li&gt;real-time systems&lt;/li&gt;
&lt;li&gt;streaming pipelines&lt;/li&gt;
&lt;li&gt;WebSocket infrastructure&lt;/li&gt;
&lt;li&gt;Typical use cases include:&lt;/li&gt;
&lt;li&gt;chat servers&lt;/li&gt;
&lt;li&gt;API gateways&lt;/li&gt;
&lt;li&gt;live dashboards&lt;/li&gt;
&lt;li&gt;collaborative applications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node also benefits from a massive ecosystem and mature tooling.&lt;/p&gt;

&lt;p&gt;However, CPU-heavy tasks can block the event loop.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;large JSON parsing&lt;/li&gt;
&lt;li&gt;image processing&lt;/li&gt;
&lt;li&gt;encryption&lt;/li&gt;
&lt;li&gt;machine learning inference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tasks often require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;worker threads&lt;/li&gt;
&lt;li&gt;microservices&lt;/li&gt;
&lt;li&gt;background job systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FastAPI concurrency combines:&lt;/p&gt;

&lt;p&gt;async coroutines + multiple worker processes&lt;/p&gt;

&lt;p&gt;This creates a system where:&lt;/p&gt;

&lt;p&gt;Worker 1 → blocked by CPU task&lt;br&gt;
Worker 2 → still serving requests&lt;br&gt;
Worker 3 → still serving requests&lt;br&gt;
Worker 4 → still serving requests&lt;/p&gt;

&lt;p&gt;The system remains responsive because workload is distributed across processes.&lt;/p&gt;

&lt;p&gt;Major advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seamless integration with machine learning frameworks&lt;/li&gt;
&lt;li&gt;strong type hints&lt;/li&gt;
&lt;li&gt;automatic request validation via Pydantic&lt;/li&gt;
&lt;li&gt;excellent developer ergonomics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Performance is surprisingly strong.&lt;/p&gt;

&lt;p&gt;Btw, One common misunderstanding is that FastAPI automatically runs everything asynchronously. That is not true.&lt;/p&gt;

&lt;p&gt;Consider this endpoint:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;def endpoint(): ...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is not asynchronous. FastAPI will run it inside a thread pool executor.&lt;/p&gt;

&lt;p&gt;In this case:&lt;/p&gt;

&lt;p&gt;Blocking function -&amp;gt; Executed in thread pool -&amp;gt; Event loop remains free&lt;/p&gt;

&lt;p&gt;To use the event loop directly, endpoints must be written as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;async def endpoint():&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Understanding the full stack layers also helps.&lt;/p&gt;

&lt;p&gt;Nodejs: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;V8 Engine -&amp;gt; Node.js Runtime -&amp;gt; libuv -&amp;gt; Operating System&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;V8 executes JavaScript. libuv handles async I/O&lt;/p&gt;

&lt;p&gt;FastAPI: &lt;/p&gt;

&lt;p&gt;&lt;code&gt;Python Interpreter -&amp;gt; FastAPI Framework -&amp;gt; ASGI -&amp;gt; Uvicorn Server -&amp;gt; asyncio / uvloop -&amp;gt; Operating System&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A interesting detail: Many FastAPI deployments use uvloop, an event loop implementation written in C.&lt;/p&gt;

&lt;p&gt;And fun thing is, uvloop is built on top of libuv — the same library used by Node.js.&lt;/p&gt;

&lt;p&gt;So in many modern deployments:&lt;/p&gt;

&lt;p&gt;FastAPI + uvloop = Python + libuv&lt;/p&gt;

&lt;p&gt;Different language. Same underlying asynchronous engine.&lt;/p&gt;

&lt;p&gt;So, Both Node.js and FastAPI follow the same fundamental philosophy:&lt;/p&gt;

&lt;p&gt;event-driven, non-blocking I/O.&lt;/p&gt;

&lt;p&gt;But they scale concurrency in different ways.&lt;/p&gt;

&lt;p&gt;Node.js -&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single JavaScript execution thread&lt;/li&gt;
&lt;li&gt;libuv thread pool for I/O&lt;/li&gt;
&lt;li&gt;parallelism requires cluster mode or worker threads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FastAPI&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;asyncio coroutines&lt;/li&gt;
&lt;li&gt;multiple worker processes&lt;/li&gt;
&lt;li&gt;natural multi-core scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;p&gt;Use Node.js when building real-time systems and working within the JavaScript ecosystem.&lt;/p&gt;

&lt;p&gt;Use FastAPI when building services around Python, data science, machine learning, or AI workloads.&lt;/p&gt;

&lt;p&gt;What matters most is understanding how their concurrency models actually behave under load because architecture decisions made early tend to shape how systems scale later.&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts, which runtime do you prefer for high-concurrency APIs and why?&lt;/p&gt;

&lt;p&gt;If you enjoyed this article, you can read more of my blogs here:&lt;br&gt;
&lt;a href="https://writings.dipchakraborty.com" rel="noopener noreferrer"&gt;https://writings.dipchakraborty.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>fastapi</category>
      <category>eventdriven</category>
      <category>async</category>
    </item>
  </channel>
</rss>
