FastAPI em Alta Performance: Ajustes Profissionais para Reduzir Latência, Aumentar Throughput e Escalar APIs de Verdade

Published on: 2026-01-18
Post image
pt fastapi-performance fastapi-tuning fastapi-otimizacao fastapi-alta-performance fastapi-producao fastapi-latencia-p99 fastapi-throughput fastapi-uvicorn gunicorn-fastapi-producao uvloop-fastapi httptools-fastapi orjson-fastapi

O ajuste fino de desempenho em aplicações com FastAPI envolve uma soma de escolhas pequenas que, juntas, reduzem a latência e aumentam a capacidade de atender mais requisições por segundo. Em sistemas reais, a diferença entre um serviço “rápido o suficiente” e um serviço consistentemente ágil costuma estar em detalhes como a forma de iniciar o servidor, o custo da serialização JSON e o reaproveitamento de conexões de banco e HTTP.

Este texto reúne práticas sólidas para reduzir o p99 (latência do percentil 99, isto é, o “rabo longo” das requisições mais lentas) e melhorar o throughput (vazão, geralmente medida em RPS: requisições por segundo). O foco é transformar a pilha típica de FastAPI em um caminho quente mais curto: menos trabalho por requisição, menos variação e menos surpresas sob carga.

Onde o tempo realmente é gasto em uma API FastAPI

Uma aplicação FastAPI gasta tempo em partes diferentes, e cada parte exige um tipo de otimização. O event loop (laço de eventos) coordena tarefas assíncronas e tende a sofrer quando algum trecho bloqueia a execução. A serialização de respostas em JSON costuma dominar CPU em APIs com payloads grandes ou muitos campos. A validação e a conversão de dados por modelos (especialmente via Pydantic) também geram custo por requisição.

Além disso, conexões repetidas com banco e serviços externos cobram um preço alto em handshake e negociação. O uso de pools (piscinas de conexão) reduz esse custo ao reaproveitar conexões abertas. Por fim, middlewares e logging adicionam trabalho em todas as rotas, mesmo quando parecem inocentes. O objetivo é encurtar cada etapa e evitar trabalho desnecessário no caminho crítico.

Servidor ASGI: modelo de workers, keep-alive e timeouts

O servidor é a primeira grande alavanca porque define como a aplicação aceita conexões e distribui trabalho. Em produção, é comum usar Gunicorn como gerenciador de processos e UvicornWorker para servir ASGI. O número de workers define quantos processos atendem em paralelo e, em cargas de I/O, normalmente pode ser maior do que a quantidade de CPUs. Parâmetros como keep-alive aumentam o reaproveitamento de conexões, reduzindo custo por requisição, principalmente em clientes “conversadores”.

Os timeouts protegem o sistema contra requisições presas e também evitam acúmulo de conexões mortas. O parâmetro max-requests força reciclagem periódica de workers, o que pode reduzir impacto de vazamentos de memória e fragmentação ao longo do tempo. Logs de acesso em texto, quando muito intensos, viram gargalo de CPU e I/O, então podem ser desativados em rotas quentes se houver logs estruturados no nível da aplicação. A seguir está um exemplo de comando de inicialização típico para produção.

gunicorn "app:app" \
  -k uvicorn.workers.UvicornWorker \
  -w 4 \
  -b 0.0.0.0:8000 \
  --keep-alive 75 \
  --timeout 60 \
  --graceful-timeout 30 \
  --max-requests 20000 \
  --max-requests-jitter 2000 \
  --access-logfile - \
  --error-logfile -

Loop e parser HTTP: uvloop e httptools para reduzir variância

O uvicorn pode usar implementações otimizadas em C para reduzir latência e, principalmente, diminuir variação no p99. O uvloop é uma implementação alternativa do event loop, conhecida por ter overhead menor em I/O. O httptools é um parser HTTP eficiente que reduz custo de parse e manipulação de conexões. Em conjunto, esses ajustes normalmente economizam milissegundos e deixam o “rabo” de latência mais estável.

Essas opções são ativadas na linha de comando do Uvicorn. O ganho exato depende da carga, do sistema operacional e do padrão de tráfego, mas a melhoria costuma ser consistente quando há muito I/O. Desativar o access log também reduz custo por requisição em cenários de alto volume. O exemplo abaixo mostra um comando direto de Uvicorn com essas configurações.

uvicorn app:app \
  --loop uvloop \
  --http httptools \
  --no-access-log

JSON: o ponto de CPU mais comum e como trocar o motor de serialização

Em APIs que retornam muito JSON, a serialização vira um dos maiores consumidores de CPU. A resposta padrão do FastAPI usa JSON via implementação tradicional, mas é possível trocar para ORJSON, uma biblioteca rápida que reduz custo de serialização e desserialização. O ganho por requisição pode parecer pequeno, mas em alta escala ele se traduz em mais throughput e menor p99. Para tornar isso simples, define-se ORJSONResponse como classe padrão de resposta.

Além da troca do serializer, reduzir o tamanho do payload também melhora latência e custo de CPU. Campos nulos, campos não definidos e estruturas profundas aumentam o trabalho de serialização e também o tráfego de rede. Em rotas com modelos Pydantic, opções como response_model_exclude_none ajudam a evitar envio de dados sem valor. A seguir estão exemplos práticos de configuração global e de exclusão de campos nulos.

from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)
from pydantic import BaseModel
from typing import Optional
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

class Usuario(BaseModel):
    id: int
    nome: str
    apelido: Optional[str] = None

app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/usuario", response_model=Usuario, response_model_exclude_none=True)
async def obter_usuario():
    return Usuario(id=1, nome="Ana", apelido=None)

Pydantic v2: validação rápida, mas com armadilhas em rotas quentes

O Pydantic v2 usa pydantic-core, uma base otimizada (implementada com componentes em Rust) que torna validação e parsing mais eficientes. Mesmo assim, validadores customizados complexos e transformações caras podem recolocar trabalho pesado na camada Python. Em rotas de leitura muito acessadas, validações elaboradas tendem a piorar p99 porque aumentam CPU por requisição e competem com a serialização. Uma prática comum é concentrar validações “pesadas” em fluxos de escrita ou em processamento assíncrono fora do caminho crítico.

Outra otimização aparece quando existe JSON já pronto vindo de cache ou banco. Em vez de converter bytes para dict Python e só então validar, métodos como model_validate_json podem reduzir alocações. Isso ajuda especialmente em respostas que reaproveitam snapshots de dados ou documentos armazenados como JSON. O exemplo abaixo ilustra a validação diretamente de bytes JSON.

from pydantic import BaseModel

class Usuario(BaseModel):
    id: int
    nome: str

raw_json_bytes = b'{"id": 10, "nome": "Bruno"}'

usuario = Usuario.model_validate_json(raw_json_bytes)
resultado = usuario.model_dump()

Dependências: custo por requisição e cache do que é estável

O sistema de dependency injection (injeção de dependências) do FastAPI é poderoso, mas cada dependência roda a cada requisição. Dependências pesadas, que abrem conexões ou criam objetos caros repetidamente, aumentam latência e podem saturar recursos sob carga. Quando uma dependência retorna configurações estáveis, uma abordagem eficaz é usar lru_cache para criar a configuração uma única vez por processo. Assim, a aplicação evita reconstruir o mesmo objeto a cada chamada.

Recursos como clientes HTTP e pools de banco não devem ser abertos dentro de dependências por requisição. O padrão mais estável é criar esses recursos uma vez no ciclo de vida da aplicação e armazenar em app.state. As dependências, então, apenas retornam referências já prontas, mantendo o caminho crítico curto. A seguir está um exemplo com settings cacheadas e acesso a objetos criados no startup.

from functools import lru_cache
from fastapi import Depends, FastAPI
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    db_dsn: str = "postgresql://usuario:senha@localhost:5432/app"
    cache_url: str = "redis://localhost:6379/0"

@lru_cache
def obter_settings() -> Settings:
    return Settings()

app = FastAPI()

def obter_db(app: FastAPI) -> object:
    return app.state.db

def obter_settings_dep(settings: Settings = Depends(obter_settings)) -> Settings:
    return settings

Lifespan: inicialização consistente e aquecimento antes da primeira requisição

O recurso de lifespan define o ciclo de vida da aplicação, permitindo criar e fechar recursos de forma controlada. Isso evita que a primeira requisição pague o custo de abrir pool de banco, criar cliente HTTP e preparar caches. Também reduz “spikes” de latência durante o aquecimento. Em produção, esse padrão melhora previsibilidade e evita condições de corrida ao inicializar recursos sob demanda.

No exemplo a seguir, são criados um pool de banco via asyncpg e um cliente HTTP com httpx.AsyncClient. O pool define tamanho mínimo e máximo para controlar concorrência e evitar saturação do banco. A opção statement_cache_size ajuda a reaproveitar prepared statements, o que costuma impactar o p99 quando há muita repetição de queries. Ao encerrar, recursos são fechados para evitar conexões penduradas.

from contextlib import asynccontextmanager
from fastapi import FastAPI
import asyncpg
import httpx
from functools import lru_cache
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    db_dsn: str = "postgresql://usuario:senha@localhost:5432/app"

@lru_cache
def obter_settings() -> Settings:
    return Settings()

async def aquecer_caches(app: FastAPI) -> None:
    # Aquecimento simples para reduzir picos iniciais
    async with app.state.db.acquire() as conexao:
        await conexao.execute("SELECT 1")

@asynccontextmanager
async def lifespan(app: FastAPI):
    settings = obter_settings()

    app.state.db = await asyncpg.create_pool(
        dsn=settings.db_dsn,
        min_size=10,
        max_size=10,
        statement_cache_size=512,
    )

    app.state.http = httpx.AsyncClient(http2=True, timeout=5.0)

    await aquecer_caches(app)

    yield

    await app.state.http.aclose()
    await app.state.db.close()

app = FastAPI(lifespan=lifespan)

Higiene de middleware: custo invisível em todas as rotas

Middleware envolve a aplicação inteira e executa em todas as requisições, inclusive nas rotas mais simples. Uma única camada mal projetada pode aumentar latência e, pior, aumentar a variância do p99 ao introduzir operações bloqueantes. Compressão, por exemplo, consome CPU e pode ser melhor resolvida em proxy reverso quando a arquitetura permite. Observabilidade e tracing também precisam de cuidado para não exportar dados de forma síncrona durante a requisição.

O CORS deve ser configurado com critérios mínimos e específicos, porque padrões amplos podem aumentar custo e risco. Logging em texto linha a linha tende a ser caro, principalmente com alto volume, e pode ser substituído por logs estruturados com buffering. Quando uma regra vale apenas para um subconjunto de rotas, ela pode ser aplicada no nível de roteador ou por dependência, evitando custo global. Esse tipo de limpeza costuma não aparecer em benchmarks simples, mas faz diferença sob carga real.

Assíncrono não significa “sem CPU”: evitando bloquear o event loop

Rotinas CPU-bound (limitadas por CPU), como criptografia pesada, compressão local, processamento de imagens ou cálculos intensos, bloqueiam o event loop quando executadas diretamente. Isso degrada o p99 porque o loop deixa de agendar I/O e outras requisições ficam na fila. Quando há necessidade de CPU no caminho de uma requisição, o padrão é delegar para um executor. Em Python, um ProcessPoolExecutor isola CPU em processos, evitando travar o loop do servidor.

Uma escolha comum é usar executor de processos para picos de CPU e manter o restante assíncrono. Isso preserva a responsividade mesmo com algumas requisições custosas. Também é possível usar threads para trabalhos leves, mas CPU forte em threads sofre com o GIL, então processos costumam ser mais estáveis para esse tipo de carga. O exemplo abaixo mostra o encaminhamento de uma função pesada para um pool de processos.

import asyncio
from concurrent.futures import ProcessPoolExecutor
from fastapi import FastAPI

app = FastAPI()
pool_processos = ProcessPoolExecutor()

def funcao_pesada_cpu(a: int, b: int) -> int:
    # Simula trabalho pesado de CPU
    total = 0
    for i in range(2_000_000):
        total = (total + a * b + i) % 1_000_003
    return total

async def calcular_pesado(a: int, b: int) -> int:
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(pool_processos, funcao_pesada_cpu, a, b)

@app.get("/score")
async def score(a: int, b: int):
    resultado = await calcular_pesado(a, b)
    return {"score": resultado}

Padrões de banco de dados que mais afetam p99 e throughput

O banco costuma ser o principal limitador de p99 quando há saturação de conexões, queries ineficientes ou serialização excessiva de resultados. O primeiro passo é manter reutilização de conexão com um pool global, evitando abrir e fechar conexões por requisição. Em seguida, reduzir o número de queries por endpoint tende a render ganhos imediatos, especialmente quando há N leituras pontuais que poderiam ser agrupadas. Consultas “set-based” (baseadas em conjunto) geralmente superam loops de queries pequenas.

Outra prática é selecionar apenas colunas necessárias, prática conhecida como projection, para reduzir I/O e o custo de serialização do Python. Índices corretos também impactam diretamente o p99, porque reduzem variação em picos de carga ao evitar scans. Em camadas ORM, escolhas como eager loading podem disparar joins caros e resultados maiores do que o necessário. Em endpoints quentes, SQL mais direto ou carregamentos seletivos costumam ser mais previsíveis do que abstrações genéricas.

Cache: duas camadas eficientes e cabeçalhos HTTP para dados públicos

Cache reduz carga de CPU, banco e serviços externos ao servir respostas repetidas mais rapidamente. Uma abordagem prática é usar duas camadas: cache local com TTL curto para amortecer rajadas e cache externo para compartilhamento entre instâncias. TTL de poucos segundos já reduz variância e melhora p99 em feeds e painéis muito acessados. Para cache externo, o ponto crítico é serializar e desserializar rápido, onde orjson também ajuda.

Quando os dados são públicos ou podem ser armazenados pelo cliente, cabeçalhos HTTP de cache adicionam ganhos sem complicar a aplicação. Diretivas como Cache-Control controlam tempo de vida e revalidação, reduzindo requisições repetidas. Em APIs, “stale-while-revalidate” permite servir algo ligeiramente antigo enquanto a atualização é feita em paralelo, quando isso é aceitável. O exemplo abaixo retorna uma resposta com headers de cache usando ORJSONResponse.

from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)

def obter_dados_publicos() -> dict:
    return {"status": "ok", "versao": 1}

@app.get("/publico")
async def publico():
    dados = obter_dados_publicos()
    resp = ORJSONResponse(dados)
    resp.headers["Cache-Control"] = "public, max-age=60, stale-while-revalidate=30"
    return resp

Ajustes pequenos em Python que somam: flags, GC e detalhes de alocação

Alguns ganhos vêm de reduzir overhead do interpretador e de alocações desnecessárias. Variáveis de ambiente como PYTHONOPTIMIZE podem remover asserts e docstrings, diminuindo um pouco o trabalho e o tamanho em memória em cenários específicos. Manter cache de bytecode ativo ajuda em tempo de import e inicialização, o que beneficia ambientes com reciclagem de processos. Em geral, o impacto desses ajustes é menor do que pools e serializer, mas eles ajudam na consistência.

O garbage collector (coletor de lixo) pode introduzir pausas quando há muita alocação de objetos temporários. Em endpoints extremamente quentes e com alocação baixa e previsível, às vezes se avalia desativar GC dentro de um escopo muito controlado, sempre garantindo reativação. Esse tipo de ajuste é delicado porque pode causar crescimento de memória e piorar o sistema ao longo do tempo. A seguir está um exemplo de escopo controlado, útil apenas quando há medições indicando benefício.

import gc
from fastapi import FastAPI

app = FastAPI()

@app.get("/rapido")
async def rapido():
    gc_ativo = gc.isenabled()
    try:
        if gc_ativo:
            gc.disable()  # Reduz pausas em rotas extremamente quentes (usar com cautela)
        return {"ok": True}
    finally:
        if gc_ativo:
            gc.enable()

Teste de carga: medindo p50, p95, p99 e detectando saturação

O ajuste fino só é confiável quando há um teste de carga repetível. Métricas como p50 mostram a experiência típica, enquanto p95 e p99 revelam o custo das piores requisições e de filas internas. Também é importante acompanhar taxa de erro sob carga sustentada, porque saturação pode se manifestar como timeouts e 5xx antes de aparecer como latência extrema. CPU alta com I/O baixo sugere gargalo de serialização ou validação, enquanto iowait alto aponta espera em disco ou rede.

Ferramentas de carga variam, mas o princípio é manter parâmetros constantes e comparar mudanças isoladas. Também é útil observar saturação do pool de banco, porque filas de aquisição de conexão explodem o p99. Em sistemas com tentativas e retries, a latência pode mascarar erros e amplificar tempestades de requisição. O exemplo abaixo mostra um comando simples de carga local para gerar concorrência e medir latência.

autocannon -c 200 -d 30 -p 10 http://localhost:8000/feed

Configuração integrada de exemplo: FastAPI com ORJSON, lifespan, pool e rotas eficientes

Uma configuração integrada ajuda a visualizar como as peças se conectam em um serviço real. A estrutura abaixo define FastAPI com ORJSONResponse como padrão, inicializa recursos no lifespan, reaproveita pool de banco e cliente HTTP, e mantém rotas com payload enxuto. Esse conjunto cobre a maior parte dos gargalos comuns: evitar criação repetida de clientes, reduzir custo de JSON e estabilizar o caminho crítico. A organização em app.state simplifica o acesso sem depender de construções caras por requisição.

O exemplo usa asyncpg como pool de PostgreSQL e httpx para chamadas externas. Em cenários sem banco ou sem chamadas HTTP, os mesmos princípios se aplicam com outros drivers e bibliotecas. O ponto central é manter recursos “quentes” e reutilizáveis, reduzindo handshake e alocações. A seguir está um exemplo completo e funcional, pronto para ser servido por Uvicorn ou Gunicorn.

from contextlib import asynccontextmanager
from functools import lru_cache
from typing import Optional

import asyncpg
import httpx
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from pydantic import BaseModel
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    db_dsn: str = "postgresql://usuario:senha@localhost:5432/app"
    http_timeout: float = 5.0

@lru_cache
def obter_settings() -> Settings:
    return Settings()

class UsuarioResposta(BaseModel):
    id: int
    nome: str
    apelido: Optional[str] = None

async def aquecer(app: FastAPI) -> None:
    async with app.state.db.acquire() as conexao:
        await conexao.execute("SELECT 1")

@asynccontextmanager
async def lifespan(app: FastAPI):
    settings = obter_settings()

    app.state.db = await asyncpg.create_pool(
        dsn=settings.db_dsn,
        min_size=10,
        max_size=10,
        statement_cache_size=512,
    )

    app.state.http = httpx.AsyncClient(http2=True, timeout=settings.http_timeout)

    await aquecer(app)

    yield

    await app.state.http.aclose()
    await app.state.db.close()

app = FastAPI(lifespan=lifespan, default_response_class=ORJSONResponse)

@app.get("/usuario/{usuario_id}", response_model=UsuarioResposta, response_model_exclude_none=True)
async def usuario(usuario_id: int):
    async with app.state.db.acquire() as conexao:
        row = await conexao.fetchrow(
            "SELECT id, nome, apelido FROM usuarios WHERE id = $1",
            usuario_id,
        )

    if row is None:
        return ORJSONResponse({"erro": "não encontrado"}, status_code=404)

    return UsuarioResposta(id=row["id"], nome=row["nome"], apelido=row["apelido"])

Conclusão: desempenho como soma de decisões pequenas e consistentes

O desempenho em FastAPI raramente depende de um único “truque”, e sim de um conjunto coerente de decisões: servidor bem configurado, loop e parser eficientes, JSON rápido e payload enxuto. A estabilização do p99 vem do controle de variância, o que inclui pools dimensionados, inicialização via lifespan e redução de trabalho global por middleware. Em rotas quentes, a disciplina de evitar alocações e validações caras é tão importante quanto otimizações de infraestrutura.

Quando essas camadas são alinhadas, a aplicação ganha throughput e previsibilidade sob carga, reduzindo filas internas e evitando que picos ocasionais dominem a experiência. A base sólida combina reutilização de conexões, serialização eficiente e limites claros para tarefas CPU-bound fora do event loop. O resultado final é uma pilha FastAPI mais resistente, com latência menor no pior caso e comportamento mais estável em produção.