<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[lucrolearning]]></title><description><![CDATA[lucrolearning]]></description><link>https://blog.lucrolearning.com</link><image><url>https://cdn.hashnode.com/uploads/logos/6a51387ca8cf56c623690316/4354f2a7-738b-4da6-9295-7beeef9f3bea.jpg</url><title>lucrolearning</title><link>https://blog.lucrolearning.com</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Jul 2026 00:30:42 GMT</lastBuildDate><atom:link href="https://blog.lucrolearning.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building the Execution Engine: Queue Manager, Workers and Process Orchestration]]></title><description><![CDATA[Part 3 of the "Beyond FastAPI" series.

In the previous article, we built a thin orchestration layer using FastAPI. The API validates incoming requests, persists them as pending jobs, and immediately ]]></description><link>https://blog.lucrolearning.com/building-the-execution-engine-queue-manager-workers-and-process-orchestration</link><guid isPermaLink="true">https://blog.lucrolearning.com/building-the-execution-engine-queue-manager-workers-and-process-orchestration</guid><category><![CDATA[Python]]></category><category><![CDATA[# orchestration layer]]></category><category><![CDATA[queue manager]]></category><dc:creator><![CDATA[Kashif Mohammad]]></dc:creator><pubDate>Thu, 16 Jul 2026 19:00:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/8fe7ccdf-79c4-4fbd-a183-833eef449f32.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>Part 3 of the "Beyond FastAPI" series.</em></p>
</blockquote>
<p>In the <a href="https://hashnode.com/edit/cmrnf7vk900000ahtatwo5i7b">previous article</a>, we built a thin orchestration layer using FastAPI. The API validates incoming requests, persists them as pending jobs, and immediately returns a Job ID to the client. At this point, our application is responsive, but it's also incomplete.</p>
<p>Although jobs are now safely stored in MongoDB, nothing is actually executing them.</p>
<p>Something still needs to answer a number of important questions.</p>
<ul>
<li><p>Which job should run next?</p>
</li>
<li><p>How many jobs can execute concurrently?</p>
</li>
<li><p>How do we prevent two workers from processing the same job?</p>
</li>
<li><p>What happens when a worker crashes?</p>
</li>
<li><p>How do we detect jobs that become stuck?</p>
</li>
<li><p>How do we clean up finished processes?</p>
</li>
</ul>
<p>These responsibilities don't belong inside the API. They belong to a dedicated scheduling component. In our architecture, that component is the <strong>Queue Manager</strong>.</p>
<hr />
<h3>The Queue Manager Is Not the Queue</h3>
<p>One misconception developers often have when reading architectures like this is assuming that the Queue Manager <em>is</em> the queue.</p>
<p>It isn't. The queue already exists. It's our MongoDB collection. Although it can be any other SQL based database as following.</p>
<pre><code class="language-plaintext">┌──────────────────────────┐
│ MongoDB Jobs Collection  │
├──────────────────────────┤
│ pending                  │.            │
│ pending                  │.            │ 
│ running                  │.            │
│ completed                │.            │
│ failed                   │.            │
└──────────────────────────┘
</code></pre>
<p>Every analysis request submitted through FastAPI is stored as a document (or row) inside this collection. Each document represents the current state of a job.</p>
<p>The Queue Manager doesn't own this data. Instead, it continuously observes it and makes scheduling decisions based on the current state of the system. This distinction is subtle but extremely important.</p>
<p>So MongoDB as the <strong>source of truth</strong>. The Queue Manager is simply the <strong>scheduler</strong> responsible for moving jobs through their lifecycle.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/6459f262-2cd5-4580-8403-9e7bdd75caaa.jpg" alt="" style="display:block;margin:0 auto" />

<h3>Separating Scheduling from Execution</h3>
<p>One of the design goals of this platform was ensuring that no single component performs too many responsibilities.</p>
<p>Let's compare two possible implementations.</p>
<h3>Option 1</h3>
<pre><code class="language-plaintext">FastAPI

↓

Find Pending Job

↓

Execute Analysis

↓

Store Result
</code></pre>
<p>This is simple.</p>
<p>Unfortunately, it's also tightly coupled.</p>
<p>The API now owns:</p>
<ul>
<li><p>HTTP communication</p>
</li>
<li><p>Scheduling</p>
</li>
<li><p>Concurrency</p>
</li>
<li><p>Worker lifecycle</p>
</li>
<li><p>Analysis execution</p>
</li>
</ul>
<p>Every new feature increases the complexity of the web application.</p>
<p>Instead, our architecture separates these concerns.</p>
<pre><code class="language-plaintext">              FastAPI
                  │
                  ▼
        MongoDB Job Queue
                  │
                  ▼
           Queue Manager
          ┌───────┴────────┐
          ▼                ▼
     Listener Loop   Watchdog Loop
          │
          ▼
     Worker Process
</code></pre>
<p>Each component now has a single responsibility.</p>
<p>FastAPI accepts work.</p>
<p>MongoDB stores work.</p>
<p>Queue Manager schedules work.</p>
<p>Workers execute work.</p>
<p>Because each component focuses on one job, the overall system becomes much easier to understand, test and evolve.</p>
<hr />
<h2>Anatomy of the Queue Manager</h2>
<p>Let's look at the class itself.</p>
<pre><code class="language-python">class QueueManager:

    def __init__(
        self,
        DBFactory
    ):
        self.db = DBFactory()
        self._db_impl_factory_for_worker = DBFactory

        self._listener_task = None
        self._watchdog_task = None

        self._running_processes = {}
</code></pre>
<p>Although the implementation is relatively small, every member variable exists for a specific reason.</p>
<h3>Database Interface</h3>
<pre><code class="language-python">self.db
</code></pre>
<p>The scheduler never communicates directly with MongoDB. Instead, it depends on the same abstraction used throughout the application. This keeps scheduling logic independent of the underlying persistence implementation.</p>
<hr />
<h3>Worker Database Factory</h3>
<pre><code class="language-python">self._db_impl_factory_for_worker
</code></pre>
<p>This is one of the more interesting design decisions in the project.</p>
<p>Instead of sharing a single database instance with worker processes, we inject a <strong>factory function</strong> capable of creating fresh database implementations.</p>
<p>Why?</p>
<p>Because worker processes execute in separate operating system processes. Sharing an already connected database client across processes can lead to unpredictable behaviour depending on the driver and operating system.</p>
<p>Instead, every worker creates its own independent database connection.</p>
<pre><code class="language-plaintext">Parent Process
      │
      ▼
Queue Manager
      │
      ▼
Factory Function
      │
 ┌────┴────┐
 ▼         ▼
Worker 1  Worker 2
 │         │
 ▼         ▼
MongoDB   MongoDB
Connection Connection
</code></pre>
<p>This keeps process boundaries clean while avoiding shared state between workers.</p>
<blockquote>
<p><strong>A Note on Database Connections</strong></p>
</blockquote>
<p>At first glance, creating a separate database client for each worker process might seem expensive. However, workers are <strong>not spawned without bounds</strong>. The Queue Manager enforces a configurable concurrency limit, ensuring that only a fixed number of worker processes can run simultaneously. Each worker establishes its own process-local MongoDB client; following MongoDB's recommendation of one client per process and exits once its job completes. Because the number of concurrent workers is tightly controlled, the maximum number of database clients is also bounded and predictable. We'll explore how this concurrency limit is enforced in the next section when we dive into the Listener Loop and scheduling logic.</p>
<hr />
<h3>Background Tasks</h3>
<p>The Queue Manager owns two asynchronous tasks.</p>
<pre><code class="language-python">_listener_task

_watchdog_task
</code></pre>
<p>These are long-running background coroutines that execute independently of incoming HTTP requests. Each solves a different problem:</p>
<p>The <strong>Listener Loop</strong> continuously searches for new work.</p>
<p>The <strong>Watchdog Loop</strong> monitors already running work.</p>
<p>Keeping these responsibilities separate makes the scheduling logic significantly easier to reason about. We'll dive into both loops next.</p>
<hr />
<h3>Tracking Running Processes</h3>
<p>Finally, the Queue Manager maintains an in-memory registry.</p>
<pre><code class="language-python">_running_processes
</code></pre>
<p>Conceptually, it looks like this.</p>
<pre><code class="language-plaintext">{
    "job-123": Process(pid=4128),

    "job-456": Process(pid=4132),

    "job-789": Process(pid=4141)
}
</code></pre>
<p>Notice what this dictionary is <strong>not</strong>.</p>
<p>It isn't the source of truth for job state. <em><mark class="bg-yellow-200 dark:bg-yellow-500/30">MongoDB already provides that</mark></em><mark class="bg-yellow-200 dark:bg-yellow-500/30">.</mark> Instead, this registry exists purely so the Queue Manager can interact with operating system processes.</p>
<p>For example:</p>
<ul>
<li><p>Determining whether a worker is still alive</p>
</li>
<li><p>Terminating workers during shutdown</p>
</li>
<li><p>Reclaiming finished processes</p>
</li>
<li><p>Detecting crashed workers</p>
</li>
</ul>
<p>Keeping process metadata separate from persisted job state keeps the architecture both simple and resilient.</p>
<hr />
<h3>Starting the Execution Engine</h3>
<p>Once FastAPI starts, it invokes the Queue Manager's startup routine.</p>
<pre><code class="language-python">async def start(self):


    self._listener_task = asyncio.create_task(
        self._listener_loop()
    )

    self._watchdog_task = asyncio.create_task(
        self._watchdog_loop()
    )
</code></pre>
<p>Notice something interesting. Neither loop blocks the application. Instead, both are started using:</p>
<pre><code class="language-python">asyncio.create_task(...)
</code></pre>
<p>This allows them to execute concurrently alongside the FastAPI event loop.</p>
<pre><code class="language-plaintext">                Event Loop

         ┌─────────┬──────────┐
         ▼         ▼          ▼

    HTTP Server  Listener   Watchdog
</code></pre>
<p>Although all three components share the same event loop, they remain completely independent.</p>
<ul>
<li><p>The HTTP server continues accepting requests.</p>
</li>
<li><p>The Listener discovers new jobs.</p>
</li>
<li><p>The Watchdog monitors existing workers.</p>
</li>
</ul>
<p>No component blocks another. This is precisely the type of workload <code>asyncio</code> was designed for—multiple long-running, I/O-bound tasks cooperating within a single thread.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/bd25a0f0-ca4a-454c-9618-8968d5711858.jpg" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Why <code>asyncio</code> Instead of Threads?</h3>
<p>A common question is why the scheduler itself isn't implemented using threads. The answer lies in understanding what the Queue Manager actually does. It isn't performing CPU-intensive computation. Instead, it's mostly waiting.</p>
<ul>
<li><p>Waiting for database operations.</p>
</li>
<li><p>Waiting for polling intervals.</p>
</li>
<li><p>Waiting for workers to finish.</p>
</li>
<li><p>Waiting for timeout checks.</p>
</li>
</ul>
<p>These are classic <strong>I/O-bound</strong> operations.</p>
<p>Using <code>asyncio</code> allows a single thread to efficiently coordinate hundreds or even thousands of these waiting operations without the overhead of managing multiple operating system threads.</p>
<p>The workers, on the other hand, are a completely different story.</p>
<p>They execute expensive LLM pipelines, consume significant CPU and memory resources, and need strong isolation from one another.</p>
<p>That's why the workers are implemented using <strong>separate processes</strong>, not asynchronous coroutines.</p>
<p>This separation allows each technology to solve the problem it is best suited for:</p>
<ul>
<li><p><strong>asyncio</strong> coordinates work.</p>
</li>
<li><p><strong>Processes</strong> execute work.</p>
</li>
</ul>
<p>As we'll see next, the Listener Loop brings these two worlds together by continuously transforming pending jobs into isolated worker processes.</p>
]]></content:encoded></item><item><title><![CDATA[Building the Orchestration Layer: Designing a Thin FastAPI API for Long-Running AI Workloads]]></title><description><![CDATA[Part 2 of the "Beyond FastAPI" series.

In the previous article, we explored why long-running AI workloads don't belong inside a traditional request-response cycle. We introduced an architecture that ]]></description><link>https://blog.lucrolearning.com/building-the-orchestration-layer-designing-a-thin-fastapi-api-for-long-running-ai-workloads</link><guid isPermaLink="true">https://blog.lucrolearning.com/building-the-orchestration-layer-designing-a-thin-fastapi-api-for-long-running-ai-workloads</guid><category><![CDATA[Python]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[# orchestration layer]]></category><dc:creator><![CDATA[Kashif Mohammad]]></dc:creator><pubDate>Thu, 16 Jul 2026 11:23:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/71b90793-9d55-4337-9845-f018188823e7.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<blockquote>
<p><em>Part 2 of the "Beyond FastAPI" series.</em></p>
</blockquote>
<p>In the <a href="https://hashnode.com/edit/cmrm4rj0c00000akjac41alas">previous article</a>, we explored why long-running AI workloads don't belong inside a traditional request-response cycle. We introduced an architecture that decouples request handling from job execution, allowing our API to remain responsive while computationally intensive analysis runs asynchronously in the background.</p>
<p>In this article, we'll build the <strong>orchestration layer</strong> of that architecture.</p>
<p>Rather than focusing on FastAPI itself, we'll look at how the application is composed, how dependencies are injected, and why the API layer should remain intentionally thin. We'll also see how background services such as our queue manager are integrated into the application's lifecycle without leaking infrastructure concerns into the web layer.</p>
<hr />
<h2>The Role of the Orchestration Layer</h2>
<p>When developers first begin building APIs, it's common to let the web framework become the centre of the application.</p>
<p>A request arrives.</p>
<p>The endpoint validates input.</p>
<p>The endpoint queries the database.</p>
<p>The endpoint performs business logic.</p>
<p>The endpoint calls external APIs.</p>
<p>The endpoint returns the response.</p>
<p>This works remarkably well—until the business logic becomes expensive.</p>
<p>Our thematic analysis pipeline isn't a simple CRUD operation. A single request can involve fetching thousands of survey responses, cleaning and preprocessing text, generating embeddings, orchestrating multiple LLM calls, aggregating intermediate results, and finally persisting the generated insights.</p>
<p>That's not the responsibility of an HTTP endpoint.</p>
<p>Instead, our FastAPI application has a much simpler responsibility:</p>
<blockquote>
<p><strong>Accept work. Persist work. Return immediately.</strong></p>
</blockquote>
<p>Everything else belongs elsewhere.</p>
<hr />
<blockquote>
<p><strong>Key Design Principle</strong></p>
<p>HTTP APIs should be responsible for communication, not computation.</p>
</blockquote>
<hr />
<h2>Overall Architecture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/4616d43b-a15a-44cf-bdfa-d58885cf2966.png" alt="" style="display:block;margin:0 auto" />

<p>Notice something important.</p>
<p>The API is no longer responsible for executing the analysis.</p>
<p>Instead, it acts as the entry point into a much larger execution platform.</p>
<p>This separation of concerns has several advantages:</p>
<ul>
<li><p>API requests complete in milliseconds instead of minutes.</p>
</li>
<li><p>Long-running analyses survive independently of HTTP connections.</p>
</li>
<li><p>Workers can scale independently of the API.</p>
</li>
<li><p>Failures are isolated to individual jobs.</p>
</li>
<li><p>Progress can be tracked asynchronously.</p>
</li>
</ul>
<p>FastAPI simply becomes the gateway into the system rather than the system itself.</p>
<hr />
<h3>The Composition Root</h3>
<p>One of the most overlooked aspects of application architecture is <strong>where objects are created</strong>.</p>
<p>Every application has a point where its major components are instantiated and connected together. This is commonly referred to as the <strong>Composition Root</strong>.</p>
<p>In our project, that responsibility belongs to the application's entry point.</p>
<pre><code class="language-python">def run():
    myDB = _make_db_impl()
    DBFactory = _make_db_impl

    app = create_app(
       DBFactory
    )

    uvicorn.run(app, host="0.0.0.0", port=8000)
</code></pre>
<p>At first glance this looks almost trivial.</p>
<p>In reality, this function defines the entire dependency graph of the application.</p>
<p>Before FastAPI ever starts accepting requests, we create:</p>
<ul>
<li><p>the database implementation</p>
</li>
<li><p>the worker database factory</p>
</li>
<li><p>the queue manager</p>
</li>
<li><p>the web server</p>
</li>
<li><p>the FastAPI application</p>
</li>
</ul>
<p>Only once every component has been assembled do we hand control over to Uvicorn.</p>
<p>This may seem like a small design choice, but it has significant architectural benefits.</p>
<p>The application has exactly one place responsible for wiring dependencies together. No class reaches into another module to construct its own dependencies. Nothing relies on global state. Every component receives exactly what it needs through its constructor.</p>
<p>This makes the application significantly easier to understand, maintain, and test.</p>
<h3>Why Dependency Injection Matters</h3>
<p>Let's imagine we wrote our queue manager like this:</p>
<pre><code class="language-python">class QueueManager:

    def __init__(self):
        self.db = MongoJobsDB(...)
</code></pre>
<p>This works.</p>
<p>But now the queue manager is permanently coupled to MongoDB.</p>
<p>Testing becomes difficult.</p>
<p>Switching databases becomes difficult.</p>
<p>Replacing MongoDB with Redis, PostgreSQL, or a mock implementation becomes impossible without modifying the class itself.</p>
<p>Instead, our implementation receives its dependencies from the outside.</p>
<pre><code class="language-python">class QueueManager:

    def __init__(
        self,
        DBFactory,
    ):
        self.db = DBFactory()
</code></pre>
<p>This small change fundamentally alters the architecture.</p>
<p>The queue manager no longer knows or cares where the database comes from. It simply depends on the behaviour defined by the interface. This is an example of the <strong>Dependency Inversion Principle</strong>, one of the SOLID design principles.</p>
<p>Rather than depending on concrete implementations, high-level components depend on abstractions.</p>
<p>As a result:</p>
<ul>
<li><p>business logic becomes independent of infrastructure</p>
</li>
<li><p>testing becomes straightforward</p>
</li>
<li><p>implementations become interchangeable</p>
</li>
<li><p>components become easier to reuse</p>
</li>
<li><p>QueueManager<br /><code>│</code><br /><code>▼</code><br /><code>DBInterface</code><br /><code>▲</code><br /><code>│</code><br /><code>┌─────────────┴──────────────┐</code><br /><code>▼ ▼</code><br /><code>MongoJobsDB MockDatabase</code></p>
</li>
</ul>
<p>Although our current implementation only uses MongoDB, the queue manager itself has no knowledge of that decision.</p>
<p>That's exactly where we want to be.</p>
<h3>Building a Thin API Layer</h3>
<p>One of the easiest ways for an application to become difficult to maintain is to allow the web framework to absorb business logic. Over time, the web layer becomes tightly coupled to every other component in the system, making it increasingly difficult to test, extend, or replace.</p>
<p>Instead, the FastAPI application in this project acts purely as a <strong>transport layer</strong>. Its responsibilities are deliberately limited to:</p>
<ul>
<li><p>Receiving HTTP requests</p>
</li>
<li><p>Validating incoming data</p>
</li>
<li><p>Delegating work to the orchestration layer</p>
</li>
<li><p>Returning an appropriate HTTP response</p>
</li>
</ul>
<p>Everything else belongs elsewhere.</p>
<p>This philosophy is reflected in the <code>WebServer</code> class.</p>
<pre><code class="language-python">class WebServer:
    def __init__(self, db: DBInterface, queue_mgr: QueueManager):
        self.db = db
        self.queue_mgr = queue_mgr

        self.app = FastAPI(
            title="Async Queue Server",
            lifespan=self._lifespan,
        )

        self.app.add_api_route(
            "/enqueue",
            self.enqueue_post,
            methods=["POST"],
            response_model=EnqueueResponse,
        )

        self.app.add_api_route(
            "/jobs/{job_id}",
            self.get_job,
            methods=["GET"],
        )
</code></pre>
<p>There are a couple of interesting design decisions here.</p>
<p>First, notice that the <code>WebServer</code> doesn't construct its own dependencies.</p>
<p>Both the database implementation and the queue manager are injected into the constructor, continuing the dependency injection pattern introduced earlier.</p>
<p>Second, the class doesn't contain any scheduling logic.</p>
<p>It doesn't spawn workers.</p>
<p>It doesn't communicate with LLMs.</p>
<p>It doesn't poll queues.</p>
<p>It doesn't even know how jobs are executed.</p>
<p>Its only responsibility is exposing a clean HTTP interface over the orchestration platform.</p>
<hr />
<h3>Why Register Routes Programmatically?</h3>
<p>If you've used FastAPI before, you're probably familiar with decorators.</p>
<pre><code class="language-plaintext">@app.post("/enqueue")
async def enqueue():
    ...
</code></pre>
<p>Decorators are concise and work well for smaller applications.</p>
<p>However, once the application begins to grow, I generally prefer encapsulating related endpoints inside classes and registering routes programmatically.</p>
<pre><code class="language-python">self.app.add_api_route(
    "/enqueue",
    self.enqueue_post,
    methods=["POST"],
)
</code></pre>
<p>This provides several advantages:</p>
<ul>
<li><p>The web server becomes a self-contained component.</p>
</li>
<li><p>Dependencies are injected through the constructor.</p>
</li>
<li><p>Routes are registered in one place.</p>
</li>
<li><p>And the application structure more closely resembles the architecture itself.</p>
</li>
</ul>
<p>Instead of scattered decorators across multiple modules, the routing layer becomes an object with a clearly defined responsibility. This is largely a matter of preference, but I've found it scales better as applications grow beyond a handful of endpoints.</p>
<hr />
<h3>Managing Background Services with FastAPI Lifespan</h3>
<p>One of the more elegant features introduced in modern versions of FastAPI is the <strong>lifespan protocol</strong>.</p>
<p>Rather than relying on startup events or background threads, FastAPI allows applications to define asynchronous startup and shutdown logic using an asynchronous context manager.</p>
<pre><code class="language-python">@asynccontextmanager
async def _lifespan(self, app: FastAPI):
    await self.queue_mgr.start()

    try:
        yield
    finally:
        await self.queue_mgr.stop()
</code></pre>
<p>It defines the lifecycle of the entire orchestration platform. When the application starts:</p>
<pre><code class="language-plaintext">FastAPI Starts
      │
      ▼
Queue Manager Starts
      │
      ▼
Listener Loop Starts
      │
      ▼
Watchdog Starts
      │
      ▼
API Ready
</code></pre>
<p>Likewise, during shutdown:</p>
<pre><code class="language-plaintext">SIGTERM
      │
      ▼
FastAPI Shutdown
      │
      ▼
QueueManager.stop()
      │
      ▼
Cancel Async Tasks
      │
      ▼
Terminate Workers
      │
      ▼
Exit
</code></pre>
<p>Every long-running background component starts together.</p>
<p>Every component stops together.</p>
<p>This avoids one of the most common problems in asynchronous applications—background tasks that continue running after the web server has begun shutting down.</p>
<p>Instead, the entire application behaves as a single coordinated unit.<br />Here is the comparison illustrated:  </p>
<img src="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/8411b7c9-ee79-402c-979b-d3d9ebc37e12.jpg" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Accepting Work Instead of Executing Work</h3>
<p>The most important endpoint in the application is <code>/enqueue</code>.</p>
<p>Its implementation is surprisingly small.</p>
<pre><code class="language-python">async def enqueue_post(self, req: EnqueueRequest):
    job_id = await self.db.insert_pending_job(req.payload)

    return EnqueueResponse(
        id=job_id,
        status="pending",
    )
</code></pre>
<p>That’s all it does: no worker creation, no scheduling, no analysis, no LLM calls, and no expensive computation. The endpoint simply records a pending job and immediately returns a response. It may seem almost too simple until you compare it with the alternative.</p>
<h2>Synchronous Design</h2>
<pre><code class="language-plaintext">Client
   │
   ▼
POST /analyse
   │
   ▼
FastAPI
   │
   ▼
Fetch Data
   │
   ▼
Generate Embeddings
   │
   ▼
Call LLM
   │
   ▼
Store Results
   │
   ▼
HTTP Response
</code></pre>
<p>The client waits for every stage of the pipeline to complete.</p>
<p>If the analysis takes five minutes, the HTTP request remains open for five minutes.</p>
<p>Now compare that to the asynchronous design.</p>
<h2>Asynchronous Design</h2>
<pre><code class="language-plaintext">Client
   │
   ▼
POST /enqueue
   │
   ▼
Insert Pending Job
   │
   ▼
Return Job ID
</code></pre>
<p>Milliseconds later, the response has already been returned.</p>
<p>Meanwhile, independently of the HTTP request:</p>
<pre><code class="language-plaintext">Queue Manager
      │
      ▼
Claim Job
      │
      ▼
Spawn Worker
      │
      ▼
Run Analysis
      │
      ▼
Store Results
</code></pre>
<p>The API remains completely free to serve additional clients.</p>
<p>The orchestration platform performs the expensive work independently.</p>
<p>This decoupling is one of the defining characteristics of asynchronous systems.</p>
<hr />
<h3>Tracking Progress Instead of Holding Connections Open</h3>
<p>Returning a Job ID fundamentally changes the interaction model between the client and the server, the client receives an acknowledgement that work has been accepted.</p>
<pre><code class="language-http">POST /enqueue

HTTP/1.1 200 OK

{
    "id": "0f9d4b8f",
    "status": "pending"
}
</code></pre>
<p>The client can now continue using the application while the analysis executes in the background.</p>
<p>Whenever progress is needed, it simply queries another endpoint.</p>
<pre><code class="language-http">GET /jobs/0f9d4b8f
</code></pre>
<p>This pattern has several advantages.</p>
<ul>
<li><p>The API never holds long-lived HTTP connections.</p>
</li>
<li><p>Progress can be reported incrementally.</p>
</li>
<li><p>Users can safely refresh the page without interrupting processing.</p>
</li>
<li><p>Clients can poll or subscribe for updates.</p>
</li>
<li><p>Jobs become durable entities rather than transient HTTP requests.</p>
</li>
</ul>
<p>From a user's perspective, the experience is actually better.</p>
<p>Instead of staring at a loading spinner for several minutes, they receive immediate feedback that their request has been accepted and can monitor its progress asynchronously.</p>
<hr />
<p><em>Let's continue to design reliable worker processes and queue manager in part 3!</em></p>
]]></content:encoded></item><item><title><![CDATA[Beyond FastAPI: Building a Production-Ready Asynchronous Job Orchestrator in Python]]></title><description><![CDATA[Background
Modern organizations collect an enormous amount of unstructured textual data through customer surveys, support tickets, employee feedback, product reviews, and research responses. While Lar]]></description><link>https://blog.lucrolearning.com/beyond-fastapi-building-a-production-ready-asynchronous-job-orchestrator-in-python</link><guid isPermaLink="true">https://blog.lucrolearning.com/beyond-fastapi-building-a-production-ready-asynchronous-job-orchestrator-in-python</guid><category><![CDATA[software architecture]]></category><category><![CDATA[Python]]></category><category><![CDATA[job-orchestration]]></category><dc:creator><![CDATA[Kashif Mohammad]]></dc:creator><pubDate>Wed, 15 Jul 2026 13:43:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/e048ae54-af11-40ca-8b44-bf5d5e9b1511.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Background</h2>
<p>Modern organizations collect an enormous amount of unstructured textual data through customer surveys, support tickets, employee feedback, product reviews, and research responses. While Large Language Models (LLMs) have made it possible to automate qualitative analysis, applying them to production-scale datasets introduces challenges that go far beyond simply making an API call. Processing thousands of responses can take several minutes, involve multiple stages of analysis, require retries, and depend on external services that are inherently slow or unreliable.</p>
<p>The core feature I wanted to build was an <strong>LLM-powered thematic analysis engine</strong> capable of processing large collections of textual responses and automatically identifying recurring themes, topics, and insights. The objective was to provide users with a simple interface where they could submit an analysis request, continue using the application, and retrieve the results once processing had completed. Rather than treating each request as a traditional synchronous API call, the system needed to support long-running, resource-intensive workloads while remaining responsive to other users.</p>
<p>This immediately shifted the problem from being purely about AI to being about <strong>backend systems engineering</strong>. The challenge was no longer just generating prompts or orchestrating LLM interactions—it was designing an execution platform that could reliably schedule analysis jobs, execute them in isolation, track progress, recover from failures, and scale as demand increased. In other words, the thematic analysis pipeline became just one component of a much larger asynchronous processing architecture.</p>
<p>In this series, I'll walk through the design and implementation of that execution platform. We'll look at how <strong>FastAPI</strong>, <strong>asyncio</strong>, <strong>multiprocessing</strong>, and <strong>MongoDB</strong> can be combined to build a production-style asynchronous job orchestration system capable of supporting long-running AI workloads without compromising the responsiveness or reliability of the API layer.</p>
<h2>Part 1 - Why FastAPI Alone Isn't Enough for Long-Running Workloads</h2>
<h3>Introduction</h3>
<p>Imagine exposing an API endpoint that performs thematic analysis on thousands of customer comments. Behind a single request lies a complex, multi-stage workflow that includes data retrieval, text preprocessing, embedding generation, multiple interactions with LLM services, aggregation of intermediate results, and persistence of the final analysis. Since each stage may involve expensive computation or external network calls, the total execution time can range from several seconds to many minutes. Executing such workloads synchronously would tie up server resources, increase the likelihood of request timeouts, and degrade the responsiveness of the API for other users..</p>
<p>That also creates several other problems:</p>
<ul>
<li><p>Reverse proxy timeouts</p>
</li>
<li><p>No progress tracking</p>
</li>
<li><p>Poor scalability</p>
</li>
<li><p>Inability to recover if the server crashes</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/3e644a60-1708-4c95-a833-3e071c09534e.jpg" alt="" style="display:block;margin:0 auto" />

<p>In other words, the API is trying to fulfil two very different responsibilities: serving HTTP requests and executing computationally expensive background jobs. These concerns have different scalability and reliability requirements, and coupling them together makes the system harder to scale, monitor, and recover. A better approach is to keep the API lightweight by accepting the request, persisting the job, and delegating the long-running work to a dedicated orchestration and worker layer.</p>
<h2>A Better Architecture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a51387ca8cf56c623690316/f1dc7309-8aab-4fbc-8d24-83c7230f08c7.jpg" alt="" style="display:block;margin:0 auto" />

<p>Instead of coupling the API directly to the analysis pipeline, we can decouple request handling from job execution by introducing an asynchronous orchestration layer. In this architecture, the API is responsible only for validating incoming requests, creating a job, and returning a unique job identifier to the client. The actual analysis is performed independently in the background, allowing the API to remain responsive regardless of how long the analysis takes.</p>
<p>The journey begins when a client submits an analysis request to the <strong>FastAPI</strong> application. Rather than executing the analysis immediately, FastAPI persists the request as a <strong>pending job</strong> in a MongoDB-backed job queue and immediately responds with a job ID. This completes the HTTP request within milliseconds while ensuring that the analysis request has been durably stored.</p>
<p>A dedicated <strong>Queue Manager</strong> continuously monitors the job queue for pending work. Based on configurable concurrency limits, it claims jobs atomically and dispatches them to a pool of isolated worker processes. This orchestration layer acts as the scheduler for the system, ensuring that only a safe number of analyses execute concurrently while preventing multiple workers from processing the same job.</p>
<p>Each worker process provides an isolated execution environment for a single analysis job. Rather than containing the business logic itself, the worker dynamically loads the appropriate <strong>LLM analysis pipeline</strong> through a plugin architecture. This keeps the orchestration layer generic and allows new analysis pipelines to be introduced without changing the worker infrastructure. During execution, workers periodically report progress milestones, making it possible for clients to monitor long-running jobs instead of waiting for a single HTTP response.</p>
<p>Finally, once the analysis completes, the generated themes, insights, and metadata are persisted to <strong>MongoDB</strong>, where they can be retrieved through a separate API endpoint. If a worker crashes or becomes unresponsive, the orchestration layer can detect the failure, mark the job appropriately, and recover gracefully without affecting the API server or other running analyses.</p>
<p>By separating <strong>request handling</strong>, <strong>job scheduling</strong>, and <strong>analysis execution</strong> into distinct components, each part of the system can scale independently. The API remains lightweight and responsive, the queue manager efficiently coordinates workloads, and the worker pool focuses exclusively on executing computationally expensive AI tasks. This separation of concerns results in a more scalable, resilient, and maintainable architecture for long-running LLM-powered applications.</p>
<p>Here is the high level break down of the components involved in the new architecture along with their primary function</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Responsibility</th>
</tr>
</thead>
</table>
<table style="min-width:50px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><td><p>FastAPI</p></td><td><p>Accepts Requests for thematic analysis</p></td></tr><tr><td><p>MongoDb</p></td><td><p>Persists jobs, intermediate and final results of analsyis, also stores milestone progress of analysis process</p></td></tr><tr><td><p>Queue Manager</p></td><td><p>Schedules jobs</p></td></tr><tr><td><p>Workers</p></td><td><p>Execute analysis</p></td></tr><tr><td><p>LLM pipeline</p></td><td><p>Core logic of LLM based analysis</p></td></tr></tbody></table>

<table style="min-width:25px"><colgroup><col style="min-width:25px"></col></colgroup><tbody><tr><td><p></p></td></tr></tbody></table>

<h2>Key Design Principles</h2>
<p>Although the technologies used to build this platform are important, the architecture is ultimately driven by a handful of fundamental design principles. These principles allow the system to remain responsive, resilient, and scalable as the complexity and volume of analysis jobs grow.</p>
<h3>Decoupling</h3>
<p>The API layer and the analysis pipeline serve very different purposes and therefore should not depend on each other. The API is responsible for accepting requests, validating input, and creating jobs, while the worker infrastructure is responsible for executing long-running analyses. By introducing a job queue between these components, requests can be accepted and acknowledged immediately without waiting for the analysis to complete. This separation also allows each component to evolve independently without impacting the others.</p>
<h3>Fault Isolation</h3>
<p>Long-running workloads inevitably fail. External APIs may become unavailable, network requests may time out, or unexpected errors may occur during processing. By executing each analysis in an isolated worker process, failures are contained to a single job rather than affecting the API server or other running analyses. A crashed worker simply results in a failed job, while the rest of the platform continues operating normally.</p>
<h3>Horizontal Scalability</h3>
<p>As demand increases, the solution should be to add more execution capacity rather than making a single server more powerful. Since jobs are persisted in a shared queue and workers operate independently, additional worker processes—or even workers running on different machines—can be introduced to increase throughput. The API layer, queue manager, and worker pool can each be scaled independently according to their individual workloads.</p>
<h3>Asynchronous Processing</h3>
<p>The request-response lifecycle should be measured in milliseconds, even if the underlying analysis takes several minutes. By treating analysis as an asynchronous background task, the API remains responsive and clients are free to continue using the application while their jobs execute. Progress can be tracked independently, and results can be retrieved once processing has completed.</p>
<h3>Worker Isolation</h3>
<p>Each analysis executes within its own operating system process, providing complete isolation from every other job. This prevents memory leaks, crashes, or resource-intensive analyses from impacting other workers. It also enables graceful shutdown, independent process lifecycle management, and more predictable resource utilisation, all of which are essential for building reliable long-running processing systems.</p>
<hr />
<p><em>Lets continue to work on FastAPI in part 2</em></p>
]]></content:encoded></item></channel></rss>