Skip to content
Writing
FastAPIPythonPydanticMicroservices

FastAPI & Python 3.12 Email Agent with Pydantic Validation

FastAPI and Pydantic v2 provide unmatched performance for Python backends. Learn how to build an async transactional email microservice with allowlist guards.

High-Concurrency Async Architecture & GIL Bypass

In high-throughput Python backends, legacy blocking libraries like smtplib or synchronous HTTP packages (requests) throttle server event loops. When thousands of autonomous AI agents or customer requests generate outbound transactional notifications simultaneously, blocking I/O starves worker processes.

Modern Python 3.12 microservices combine FastAPI with asynchronous HTTP clients (httpx.AsyncClient) and Pydantic v2. Because Pydantic v2 compiles its core validation rules directly to Rust, schema parsing executes at sub-millisecond speeds, allowing a single lightweight container to process 10,000+ dispatches per minute.

1. Defining Type-Safe Pydantic v2 Schemas

Pydantic models enforce strict RFC compliance on destination email addresses, validate subject lengths, and handle structured template variables before outbound traffic touches network sockets.

PYTHON
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional, Dict, Any
import re

class EmailDispatchPayload(BaseModel):
    to: EmailStr = Field(..., description="Target recipient email address")
    subject: str = Field(..., min_length=1, max_length=150, description="Email subject line")
    text_content: Optional[str] = Field(None, min_length=1)
    html_content: Optional[str] = Field(None, min_length=1)
    idempotency_key: Optional[str] = Field(None, max_length=64)
    metadata: Dict[str, Any] = Field(default_factory=dict)

    @field_validator("subject")
    @classmethod
    def sanitize_subject_line(cls, v: str) -> str:
        # Prevent CRLF header injection attacks
        if "\r" in v or "\n" in v:
            raise ValueError("Subject line must not contain carriage return or newline characters")
        return v.strip()

2. FastAPI Service Implementation with Connection Pooling

To minimize TLS handshake latencies, maintain a single shared httpx.AsyncClient inside the FastAPI lifespan context. This enables persistent HTTP/2 connection pooling directly to SadaSend edge nodes.

PYTHON
from fastapi import FastAPI, HTTPException, status, Depends
from contextlib import asynccontextmanager
import httpx
import os

http_client: httpx.AsyncClient | None = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global http_client
    # Initialize persistent HTTP/2 pool with keep-alive
    http_client = httpx.AsyncClient(
        base_url="https://api.sadasend.com",
        timeout=httpx.Timeout(5.0, connect=2.0),
        limits=httpx.Limits(max_keepalive_connections=50, max_connections=200),
    )
    yield
    await http_client.aclose()

app = FastAPI(title="SadaSend Python Gateway", lifespan=lifespan)

@app.post("/v1/dispatch", status_code=status.HTTP_202_ACCEPTED)
async def dispatch_transactional_email(payload: EmailDispatchPayload):
    api_key = os.getenv("SADASEND_API_KEY")
    if not api_key:
        raise HTTPException(status_code=500, detail="SADASEND_API_KEY environment variable unconfigured")

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    if payload.idempotency_key:
        headers["Idempotency-Key"] = payload.idempotency_key

    try:
        response = await http_client.post(
            "/emails",
            headers=headers,
            json={
                "to": payload.to,
                "subject": payload.subject,
                "text": payload.text_content,
                "html": payload.html_content,
            },
        )
    except httpx.TimeoutException:
        raise HTTPException(status_code=504, detail="Upstream email gateway timeout")

    if response.status_code == 403:
        raise HTTPException(
            status_code=403,
            detail="Recipient domain rejected by SadaSend edge security allowlist"
        )
    if not response.is_success:
        raise HTTPException(status_code=response.status_code, detail=response.text)

    return response.json()

Production Resilience Comparison: Python Email Patterns

Metricsmtplib (Built-in)Celery + Redis WorkerFastAPI + AsyncClient + SadaSend
I/O ModelSynchronous (Blocks Thread)Asynchronous QueueAsynchronous Native Event Loop
Egress ProtocolRaw TCP Port 587/465Redis IPC + SMTPHTTPS Port 443 with HTTP/2
Connection Setup300-800ms TLS Handshake/reqVariable Queue Latencylow-latency Persistent Pool
Firewall VulnerabilityHigh (Port 25/587 blocked)MediumZero (Standard HTTPS egress)
Idempotency SupportManual Database LockManual Key DedupNative Gateway Header Support
Free plan

Building AI agents that send email?

Scoped API keys, per-key recipient allowlists, approval mode and a hosted MCP server with ten tools — on the free plan, without a card.