FastAPI
Build lightweight Python APIs for AI model serving and workflows.
A trained model becomes useful to other software when it has a stable, secure, and observable interface. FastAPI can provide that interface by validating HTTP requests, invoking preprocessing and inference code, and returning versioned response schemas.
What Is FastAPI?
FastAPI is a Python framework for building web APIs using standard type hints. It integrates request and response validation, serialization, dependency injection, asynchronous request handling, and OpenAPI documentation.
FastAPI is the application framework, while an ASGI server such as Uvicorn runs the application. A production deployment may place that server behind a reverse proxy, load balancer, container platform, or API gateway.
FastAPI's Role in MLOps
Client request
-> Gateway authentication and limits
-> FastAPI request validation
-> Feature transformation
-> Model inference
-> Output validation and policy
-> Versioned JSON response
-> Logs, metrics, and trace
-> Production monitoring and feedbackFastAPI is one layer of the serving system. Model versioning, artifact storage, deployment, autoscaling, monitoring, data quality, feedback, retraining, and governance require additional MLOps components.
Install and Run
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn
uvicorn app.main:app --reloadReload mode is for local development. Production uses tested runtime configuration, controlled worker and process management, timeouts, graceful shutdown, and deployment health checks.
A Minimal API
from fastapi import FastAPI
app = FastAPI(
title="Ticket Classification API",
version="1.0.0",
)
@app.get("/")
def home() -> dict[str, str]:
return {"message": "Ticket Classification API"}FastAPI generates an OpenAPI schema and interactive documentation from routes, models, and metadata. Documentation is useful but may need authentication or restricted exposure in production.
HTTP Methods and Status Codes
| Method | Typical Use | Example |
|---|---|---|
| GET | Retrieve a resource or status | Read model metadata |
| POST | Submit data or create work | Request a prediction |
| PUT | Replace a resource | Replace a complete configuration |
| PATCH | Update selected fields | Change deployment metadata |
| DELETE | Remove a resource | Delete an allowed temporary job |
| Status | Meaning |
|---|---|
| 200 OK | Successful request with a response body |
| 201 Created | A new resource was created |
| 202 Accepted | Work was accepted for asynchronous processing |
| 400 Bad Request | Request is invalid beyond schema parsing |
| 401 Unauthorized | Authentication is missing or invalid |
| 403 Forbidden | Identity lacks permission |
| 404 Not Found | Resource does not exist |
| 422 Unprocessable Entity | Request validation failed |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Unexpected server failure |
| 503 Service Unavailable | Service is temporarily unable to serve safely |
Request and Response Models
from typing import Literal
from pydantic import BaseModel, Field
class PredictionRequest(BaseModel):
text: str = Field(min_length=1, max_length=10_000)
language: Literal["en", "hi"] = "en"
class PredictionResponse(BaseModel):
label: Literal["billing", "technical", "sales", "needs_review"]
confidence: float = Field(ge=0.0, le=1.0)
model_version: str
request_id: strExplicit schemas reject malformed input early, document the contract, and prevent accidental leakage of internal fields. Validation ensures shape and allowed values; it does not prove that input is truthful, safe, authorized, or representative.
A Prediction Endpoint
from uuid import uuid4
from fastapi import FastAPI, HTTPException
app = FastAPI()
MODEL_VERSION = "ticket-classifier-17"
@app.post("/v1/predict", response_model=PredictionResponse)
def predict(payload: PredictionRequest) -> PredictionResponse:
request_id = str(uuid4())
try:
features = preprocess(payload.text, payload.language)
label, confidence = model_predict(features)
except KnownInputError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if label not in {"billing", "technical", "sales"}:
label = "needs_review"
return PredictionResponse(
label=label,
confidence=confidence,
model_version=MODEL_VERSION,
request_id=request_id,
)The model, preprocessing functions, and domain exception are placeholders. Production code should load an approved artifact, enforce compatibility, validate model output, avoid exposing internal errors, and record request and model metadata.
Load Models Once
Loading a model on every request wastes time and memory. Use FastAPI's lifespan support to initialize expensive resources during startup and release them during shutdown.
from contextlib import asynccontextmanager
from fastapi import FastAPI
resources: dict[str, object] = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
model, metadata = load_approved_model(version="17")
validate_model_compatibility(model, metadata)
resources["model"] = model
resources["metadata"] = metadata
yield
resources.clear()
app = FastAPI(lifespan=lifespan)If several worker processes are started, each may load its own model copy. Account for multiplied CPU or GPU memory before increasing worker count.
Sync vs Async Endpoints
| Endpoint Style | Best Fit | Caution |
|---|---|---|
| def | Blocking libraries and CPU-bound code | Throughput depends on thread and process behavior |
| async def | Awaitable network, database, and file I/O | Blocking code inside it can stall the event loop |
| Background queue | Slow or durable work not suitable for a request | Needs status, retries, idempotency, and result storage |
Declaring a function async does not make CPU-heavy inference nonblocking. Benchmark the model and use suitable worker processes, separate inference servers, accelerator scheduling, or job queues according to the workload.
Asynchronous Jobs
Long document processing, video inference, or large batch scoring should usually return 202 Accepted with a job ID and process work in a durable queue rather than holding one HTTP connection open.
{
"job_id": "job_8f31",
"status": "queued",
"status_url": "/v1/jobs/job_8f31"
}Dependency Injection
FastAPI dependencies can provide authentication, authorization, database sessions, tenant context, rate-limit checks, and shared services. Keep dependencies explicit and testable, and avoid hidden global state that makes behavior difficult to reason about.
from typing import Annotated
from fastapi import Depends, Header, HTTPException
async def require_api_key(
x_api_key: Annotated[str | None, Header()] = None,
) -> str:
if not x_api_key or not verify_api_key(x_api_key):
raise HTTPException(status_code=401, detail="Invalid credentials")
return x_api_key
@app.post("/v1/predict")
def predict(
payload: PredictionRequest,
_: Annotated[str, Depends(require_api_key)],
) -> PredictionResponse:
...For production identity, use an approved authentication design such as short-lived tokens, OAuth, or service identity. Never hard-code keys, compare secrets unsafely, or log authorization values.
Authentication and Authorization
- Authenticate callers at an API gateway, the application, or both according to architecture.
- Authorize the specific model, endpoint, tenant, and action after authentication.
- Use least-privilege service identities and short-lived credentials where possible.
- Keep tenant and customer context server-controlled and verify resource ownership.
- Separate public, internal, operator, health, and administrative endpoints.
- Audit sensitive prediction and management actions without storing unnecessary input data.
Error Handling
| Error Type | Response |
|---|---|
| Schema validation | Return a clear client error without invoking the model |
| Domain validation | Return an appropriate 4xx response with safe details |
| Rate limit | Return 429 and retry guidance where appropriate |
| Temporary model dependency | Return 503 or use a defined fallback |
| Timeout | Cancel or abandon work safely and return a controlled error |
| Unexpected exception | Log with request ID and return a generic 500 response |
Do not return stack traces, filesystem paths, SQL details, prompts, secrets, or internal model errors to clients. Map known failures consistently and monitor unknown ones.
Health Endpoints
@app.get("/health/live")
def liveness() -> dict[str, str]:
return {"status": "alive"}
@app.get("/health/ready")
def readiness() -> dict[str, str]:
if "model" not in resources:
raise HTTPException(status_code=503, detail="Model not ready")
return {"status": "ready"}Liveness should answer whether the process is alive. Readiness should answer whether the instance can safely receive traffic, including whether the expected model is loaded. Do not expose sensitive configuration through health endpoints.
API Versioning
Version client-facing contracts independently from the model artifact. A /v1/predict endpoint can serve a newer compatible model while preserving the request and response schema. Introduce a new API version for breaking contract changes and publish a migration and deprecation plan.
Model Metadata
Return or log enough metadata to trace behavior without exposing sensitive internals. Useful fields can include API version, model version, request ID, prediction timestamp, and a review flag. Avoid revealing filesystem paths, registry credentials, training records, or proprietary details unnecessarily.
Preprocessing and Postprocessing
- Package feature transformation with the model or as a versioned compatible component.
- Validate units, categories, missing values, encoding, and shape before inference.
- Prevent training-serving skew by reusing and testing feature definitions.
- Validate prediction types, numeric ranges, allow-listed labels, and calibration assumptions.
- Apply deterministic business policy after prediction rather than embedding all decisions in model output.
- Route out-of-distribution, low-confidence, or policy-sensitive results to fallback or review.
Batching and Concurrency
Batching can improve accelerator throughput but may increase waiting time. Limit request and batch size, define maximum queue delay, preserve per-item errors, and measure latency at realistic concurrency. Do not let an oversized request exhaust memory for other clients.
Rate Limiting and Abuse Protection
- Set request, concurrency, body-size, file-size, and inference-cost limits.
- Rate-limit by an appropriate authenticated identity rather than only IP address when possible.
- Reject unsupported media types and validate file signatures and dimensions.
- Use timeouts and circuit breakers for model and downstream calls.
- Protect expensive endpoints from automated abuse, model extraction, and denial of service.
- Place edge protections in a gateway or proxy while retaining application-level safeguards.
File Uploads
Image, audio, and document APIs need strict file limits and safe parsing. Validate declared and actual type, size, dimensions or duration, archive contents, malware policy, and retention. Store uploads only when required and use generated object identifiers rather than trusting client filenames.
CORS
Cross-Origin Resource Sharing controls which browser origins may call an API. Configure an explicit allow-list of trusted origins, methods, and headers. CORS is a browser mechanism, not authentication, and a permissive wildcard can be unsafe with credentials.
Structured Logging
{
"timestamp": "2026-07-20T12:15:00Z",
"level": "info",
"service": "ticket-classifier-api",
"request_id": "req_8f31",
"trace_id": "trace_2aa7",
"route": "/v1/predict",
"status_code": 200,
"duration_ms": 84,
"model_version": "17",
"prediction_label": "technical"
}Do not log full sensitive request bodies by default. Redact tokens, personal data, confidential features, prompts, and model outputs according to data policy.
Metrics and Tracing
| Layer | Metrics |
|---|---|
| HTTP | Request count, status, latency, body rejections, and active requests |
| Application | Queue time, preprocessing, inference, and postprocessing latency |
| Model | Version, prediction distribution, confidence, fallback, and review rate |
| System | CPU, memory, GPU, worker saturation, and restarts |
| Business | Correctness, conversion, resolution, and customer outcome |
| Cost | Compute, model calls, and cost per successful prediction |
Propagate trace context through gateways, queues, feature services, model servers, and downstream systems. A healthy API does not prove the model is accurate or the business outcome is good.
Testing FastAPI
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_predict_rejects_empty_text():
response = client.post(
"/v1/predict",
json={"text": "", "language": "en"},
headers={"X-API-Key": "test-key"},
)
assert response.status_code == 422
def test_liveness():
response = client.get("/health/live")
assert response.status_code == 200- Unit-test preprocessing, postprocessing, authorization, and domain rules.
- Test valid, missing, malformed, oversized, extreme, and adversarial inputs.
- Use dependency overrides or fakes for external systems and expensive models.
- Run contract tests against request and response schemas.
- Test model-artifact compatibility, evaluation thresholds, and fallback behavior.
- Test timeouts, concurrency, rate limits, startup failure, graceful shutdown, and dependency outages.
- Load-test latency, throughput, memory, and error rate with representative traffic.
Project Structure
app/
├── main.py
├── api/
│ ├── routes.py
│ └── dependencies.py
├── schemas/
│ ├── requests.py
│ └── responses.py
├── services/
│ ├── inference.py
│ └── preprocessing.py
├── core/
│ ├── config.py
│ ├── security.py
│ └── logging.py
└── monitoring/
└── metrics.py
tests/
configs/
Dockerfile
pyproject.tomlKeep route functions thin. Place model and business behavior in testable service modules, centralize validated configuration, and isolate infrastructure dependencies.
Configuration
- Read environment-specific settings from validated runtime configuration.
- Keep secrets in a protected secret manager or platform connection.
- Fail startup if required model, endpoint, or credential configuration is missing.
- Avoid environment-dependent defaults that can accidentally connect tests to production.
- Record safe resolved configuration and artifact versions for traceability.
- Separate development, staging, and production data, identities, and endpoints.
Docker Deployment
FROM python:3.11-slim
RUN useradd --system --create-home app
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY --chown=app:app app/ ./app/
USER app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Pin dependencies, use a trusted minimal image, run as non-root, keep secrets outside the image, scan the artifact, and promote the same immutable image through staging and production.
Worker and Scaling Strategy
- Benchmark one process before selecting worker count.
- Account for one model copy per process and accelerator context.
- Scale horizontally with container replicas when workload and platform support it.
- Set resource requests and limits based on measured memory and CPU or GPU use.
- Use readiness checks and warm-up so traffic reaches only loaded instances.
- Limit in-flight requests and queue work rather than allowing latency collapse under overload.
- Use a specialized model server when dynamic batching, multi-model serving, or accelerator optimization justifies it.
Deployment Lifecycle
Versioned API and model code
-> Unit, integration, contract, security, and AI evaluation tests
-> Build and scan immutable container
-> Deploy to staging
-> Smoke, compatibility, and load tests
-> Shadow or canary production traffic
-> Monitor HTTP, system, model, quality, outcome, and cost
-> Promote, pause, or roll back by image and model versionSecurity Checklist
- Authenticate and authorize every non-public endpoint.
- Validate body, path, query, header, file, and model output data.
- Use TLS and secure proxy-header configuration behind trusted infrastructure.
- Apply rate, concurrency, size, timeout, and cost limits.
- Keep secrets outside source and images and redact logs.
- Run containers with least privilege and maintain dependencies.
- Protect documentation, metrics, debug, and administrative endpoints.
- Test tenant isolation, injection, unsafe deserialization, file parsing, and denial-of-service cases.
Common Mistakes
| Mistake | Better Practice |
|---|---|
| Load model on every request | Load and validate once during application lifespan |
| Accept dict for every body | Use explicit request and response models |
| Use async for blocking inference | Match endpoint and serving architecture to workload |
| Return raw exceptions | Map failures to safe consistent responses |
| Run slow jobs in request process | Queue durable work and return a job ID |
| Start many workers blindly | Measure model memory, concurrency, and throughput |
| Expose docs and metrics publicly | Restrict operational endpoints |
| Monitor only status codes | Monitor model quality, drift, outcomes, and cost too |
Practical Exercise
- Create a /v1/predict endpoint with typed request and response schemas.
- Load a model or deterministic test double through lifespan.
- Add authentication, request IDs, safe errors, liveness, and readiness.
- Record structured logs and request, inference, and fallback metrics.
- Write unit, integration, schema, failure, and load tests.
- Containerize as a non-root service with pinned dependencies.
- Deploy to a safe staging environment and perform a canary test.
- Document the API contract, model version, limitations, monitoring, and rollback.
Best Practices
- Keep endpoints small, versioned, typed, and explicit about status behavior.
- Load approved model artifacts once and verify preprocessing and schema compatibility.
- Use deterministic validation before and after inference and route uncertainty safely.
- Design authentication, tenant authorization, rate limits, timeouts, and payload limits from the start.
- Separate long-running work through durable queues and status resources.
- Emit traceable logs and metrics while protecting sensitive data.
- Test contracts, model behavior, failures, concurrency, resources, and shutdown.
- Deploy immutable containers progressively and monitor service, model, outcome, and cost together.