AI Intermediate: Infrastructure & Agentic SystemsDay 6 of 30 · Week 1: Bare-Metal Infrastructure & Advanced Containerization
Course outline▾

Week 2 · Kubernetes Foundation for MLOps

  1. Day 8K3d & Lightweight Kubernetes: Running Multi-Node Clusters on 16GB of RAM
  2. Day 9Kubernetes Core for Machine Learning: Pods, Deployments, and Headless Services
  3. Day 10Persistent Volumes (PV/PVC): Managing Heavy Model Weights and Vector Stores
  4. Day 11StatefulSets for Vector Databases: Deploying Milvus or Qdrant in Kubernetes
  5. Day 12Auto-Scaling AI Inference: Using Horizontal Pod Autoscalers (HPA) for Spiky API Traffic
  6. Day 13Helm Charts: Packaging, Versioning, and Updating AI Deployments
  7. Day 14Week 2 Capstone: Deploying a Highly Available Local AI Application on K3d

Week 3 · High-Performance Networking & Security Mesh

  1. Day 15AI Network Bottlenecks: Transitioning from iptables to eBPF
  2. Day 16Cilium & eBPF: Sub-Millisecond Routing for Heavy AI Microservices
  3. Day 17Service Mesh 101: Installing Istio over Your Local Cluster
  4. Day 18Envoy Proxy as an AI Gateway: Load Balancing and Terminating LLM Prompts
  5. Day 19Zero-Trust mTLS: Encrypting Traffic Between Autonomous Agents
  6. Day 20API Rate Limiting & Egress Control: Preventing Agents from Exfiltrating Data
  7. Day 21Distributed Tracing: Debugging Agent Workflows with Jaeger and OpenTelemetry

Week 4 · Enterprise Agents & CI/CD Workflows

  1. Day 22Introduction to MLOps: Bridging AI Development with Continuous Integration
  2. Day 23GitOps with ArgoCD: Automated, Self-Healing AI Deployments
  3. Day 24Building Ephemeral AI Sandboxes: Secure Execution Environments for Agent Code
  4. Day 25Multi-Agent Orchestration Frameworks: Exploring Mastra, LangGraph, and CrewAI
  5. Day 26Tool-Calling in Production: Connecting Agents Securely to Internal Databases
  6. Day 27MLOps Guardrails: Evaluating Agents and Preventing Hallucinations in Real-Time
  7. Day 28Continuous Training Pipelines: Connecting Data Ingestion to Automated Model Retraining
  8. Day 29Managing Technical Debt and Infrastructure Drift in Enterprise AI
  9. Day 30The AI-Native Platform: Scaling from a Sandbox Environment to Global Cloud Architecture
Open the course page →

Day 6: Containerizing AI Microservices — Building and Packaging APIs with FastAPI

2026-11-12 · 9 min read

An internal application should not call a raw model socket. Between them you need an application layer that handles rate limiting, payload validation, authentication and error handling. FastAPI is a strong fit for that job.

The request flow

A REQUEST THROUGH THE GATEWAYClientJSON payloadFastAPIingress gatewayPydanticschema checkAsync queuevalidInference enginevLLM or Ollama422 Bad Requestrejected at onceSSE token stream back to the clientBad payloads never reach the expensive GPU.
FIG 1 · The gateway validates the payload with Pydantic before queuing it. Invalid requests get an immediate 422, and valid ones stream tokens back over Server-Sent Events.
  1. The client sends a JSON payload to the FastAPI gateway.
  2. Pydantic checks the types and the schema.
  3. A valid request goes to an async queue and on to the inference engine (vLLM or Ollama), and the answer streams back as Server-Sent Events.
  4. An invalid request gets a 422 Bad Request immediately, so it never costs a GPU cycle.

Why FastAPI

  • Native async. With asyncio, a single worker holds thousands of open connections while it awaits the model, unlike a synchronous Flask worker that blocks per request.
  • Strict input validation. Pydantic enforces types and limits before any expensive inference starts.
SYNC WORKER AGAINST ASYNC WORKER, WAITING ON THE MODELSynchronous (Flask style): one request per worker at a timereq 1 waits…req 2 waits…Asynchronous (FastAPI + asyncio): many requests while awaitingreq 1req 2req 3req 4The event loop serves other clients while one request awaits the model.
FIG 2 · Inference is slow I/O from the gateway's point of view. Async code keeps thousands of connections open while it awaits, instead of blocking a worker per request.

An example service

This gateway validates a diagnostic request, calls Ollama and streams the tokens. It assumes Ollama is running locally with the gemma2:2b model pulled.

import json
import httpx
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

app = FastAPI(title="Diagnostic AI gateway")
OLLAMA_URL = "http://localhost:11434/api/generate"   # in Docker Compose: http://inference-engine:11434/api/generate


class DiagnosticRequest(BaseModel):
    device_id: str
    telemetry_logs: str = Field(min_length=10, max_length=5000)
    severity_threshold: int = Field(ge=1, le=5)


async def stream_inference(req: DiagnosticRequest):
    prompt = (
        f"Device {req.device_id}. Severity threshold {req.severity_threshold}.\n"
        f"Analyse these logs and list likely faults:\n{req.telemetry_logs}"
    )
    payload = {"model": "gemma2:2b", "prompt": prompt, "stream": True}
    try:
        async with httpx.AsyncClient(timeout=None) as client:
            async with client.stream("POST", OLLAMA_URL, json=payload) as resp:
                resp.raise_for_status()
                async for line in resp.aiter_lines():
                    if not line:
                        continue
                    chunk = json.loads(line)                       # Ollama streams one JSON object per line
                    if chunk.get("response"):
                        yield f"data: {json.dumps({'token': chunk['response']})}\n\n"
                    if chunk.get("done"):
                        yield "data: [DONE]\n\n"
    except httpx.HTTPError as exc:
        # Once streaming has started the HTTP status is already sent,
        # so report the failure as an event instead of raising.
        yield f"data: {json.dumps({'error': str(exc)})}\n\n"


@app.post("/api/v1/diagnose")
async def diagnose(req: DiagnosticRequest):
    # Invalid payloads never get here: FastAPI returns 422 automatically.
    return StreamingResponse(stream_inference(req), media_type="text/event-stream")

Run it with uvicorn main:app --host 0.0.0.0 --port 8000 and test it with curl -N -X POST localhost:8000/api/v1/diagnose -H "Content-Type: application/json" -d '{"device_id":"sw-14","telemetry_logs":"port 12 link flap, CRC errors rising","severity_threshold":3}'.

One detail matters for error handling. You can raise an HTTPException (for example a 500) only before the first byte is sent. After streaming has begun the status code is already on the wire, so failures must travel as an event in the stream, as shown above.

Streaming changes how it feels

BLOCKING RESPONSE AGAINST SERVER-SENT EVENTSBlocking: nothing until the whole answer is doneuser sees a spinner for about 10 secondsStreaming (text/event-stream): first words almost at onceSame total time, far better perceived speed.data: {"token": "The"}data: {"token": " router"} ← each event is flushed as soon as it exists
FIG 3 · Total generation time is unchanged, but streaming turns a long blocking wait into a response that starts almost immediately. Remember to disable proxy buffering.

Server-Sent Events (text/event-stream) send each token as soon as it exists. Total generation time is the same, but a 10-second blocking wait becomes a response that starts in well under a second. If a proxy sits in front of the gateway, turn off response buffering there too, or the stream is held back.

Sizing workers

DECOUPLE WORKERS FROM THE GPUworker 1asyncworker 2asyncworker 3asyncGateway process2 to 4 async workers is a common startONE inference enginevLLM batches many requests on the GPUMore workers than the engine can batch just lengthens the queue.Start small, load-test, then adjust. The right number depends on your hardware and traffic.
FIG 4 · Gateway workers are cheap and async, while the GPU engine is the scarce resource. Size workers from load tests, not from the GPU count.

Decouple the number of gateway workers from the number of GPUs. Async workers are cheap and the inference engine does the real batching, so two to four workers is a common starting point. Load-test and adjust, because more workers than the engine can batch only lengthens the queue.

Package the service with the multi-stage Dockerfile from Day 2, and it becomes a deployable microservice: validated input, streamed output and a clear boundary in front of the model.

Run it yourself

This is the exact example from the video. A real FastAPI app, tested in process. A fixed token list stands in for the Ollama call, and the file is trimmed from the lesson's version.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
app = FastAPI()

class Req(BaseModel):
    logs: str = Field(min_length=10)
    severity: int = Field(ge=1, le=5)

async def sse():                      # stand-in for the engine
    for t in ["Port", " 12", " flaps"]:
        yield f"data: {t}\n\n"

@app.post("/diagnose")
async def diagnose(req: Req):
    return StreamingResponse(sse(), media_type="text/event-stream")

Helper file client_test.py, used by the run:

from fastapi.testclient import TestClient
from app import app

c = TestClient(app)
bad = c.post("/diagnose", json={"logs": "x", "severity": 9})
print("bad payload:", bad.status_code)
ok = c.post("/diagnose", json={"logs": "port 12 link flap", "severity": 3})
print("good payload:", ok.status_code)
print("type:", ok.headers["content-type"])
for line in ok.text.split("\n\n")[:3]:
    print(line)

Run it:

python client_test.py

Output I got while recording the video:

bad payload: 422
good payload: 200
type: text/event-stream; charset=utf-8
data: Port
data:  12
data:  flaps

Coming Up Next

Day 7: Docker Compose: architecting a multi-container database and inference stack locally.

#FastAPI #Python #AIEngineering #SoftwareArchitecture #Microservices #CloudNative #DevOps #BackendDevelopment