Executando verificação de segurança...
11

Erros no FastAPI Que DESTROEM Sua Performance

A maioria dos problemas de performance do FastAPI não são causados pelo próprio FastAPI. Eles são causados por questões arquiteturais - consultas N+1 no banco de dados, índices faltando, estratégias ruins de cache. Eu cobri esses problemas maiores no meu post anterior sobre a velocidade do Python, e corrigir eles vai te dar melhorias de performance de 10-100x.

Mas digamos que você já otimizou sua arquitetura. Suas queries estão eficientes, você tá fazendo cache adequadamente e está usando async corretamente. Ainda existem algumas otimizações específicas do FastAPI que podem te dar ganhos significativos de performance - geralmente melhorias de 20-50% que vão somando.

O ponto é: essas otimizações não vão salvar um sistema mal projetado, mas podem tornar um sistema bem projetado significativamente mais rápido. Pense nelas como um polimento final em uma arquitetura já eficiente.

TL;DR:

  • Instale uvloop e httptools para event loops mais rápidos e parsing HTTP otimizado
  • Use ORJSONResponse para serialização JSON 20-50% mais rápida
  • Escolha async vs sync baseado na carga de trabalho - I/O deve ser async, CPU pesado pode ser sync
  • Otimize modelos Pydantic com configuração focada em performance
  • Faça cache de dependências caras para evitar computações repetidas
  • Faça stream de respostas grandes para reduzir uso de memória em 80-90%
  • Aumente o pool de threads apenas se você deve usar operações sync
  • Use background tasks para que usuários não esperem por operações não críticas
  • Execute múltiplos workers em produção para utilizar todos os núcleos da CPU
  • Ative compressão GZip para respostas grandes
  • Use fastapi run em produção ao invés de fastapi dev
  • Implemente middleware ASGI Puro ao invés de BaseHTTPMiddleware
  • Evite validação dupla entre parsing de requisição e modelos de resposta

Não Instalar uvloop e httptools

O event loop é o coração da programação assíncrona - é um processo contínuo que monitora e despacha eventos e tarefas. Pense nisso como um controlador de tráfego em um cruzamento movimentado, decidindo qual carro vai a seguir e gerenciando o fluxo. No caso do Python, o event loop gerencia todas suas operações async: quando iniciar consultas ao banco, quando processar requisições HTTP, quando lidar com respostas de APIs externas.

O event loop padrão do Python (asyncio) é escrito em Python e faz verificações segurança. Toda vez que ele agenda uma tarefa ou lida com I/O, tá executando código Python interpretado com toda a sobrecarga que vem junto.

O uvloop substitui isso por um event loop escrito em Cython (compilado para C) e baseado em libuv - a mesma biblioteca C que alimenta o Node.js. Em vez de Python interpretado gerenciando suas operações async, você troca isso por código C otimizado lidando com agendamento e gerenciamento de I/O.

A diferença de performance é enorme sob alta concorrência. Você pode até não notar com 10 request, mas com 1000+ requests concorrentes (bem comum em produção), uvloop pode lidar com 2-4x mais throughput porque a sobrecarga de gerenciar todas essas operações é muito menor.

Além disso, toda requisição HTTP precisa de parsing - extrair headers, decodificar o body, lidar com encoding. O parser HTTP padrão do Python (h11) prioriza correção e lida com casos extremos graciosamente, mas ainda é Python puro. O httptools usa um parser HTTP baseado em C que é 40% mais rápido, porém menos tolerante a requests malformados - perfeito para produção onde você controla as requisições do cliente.

Para aplicar essas duas mudanças é bem simples:

uv add uvloop httptools

O Uvicorn automaticamente detecta e usa eles se tiverem instalados - nenhuma mudança de código necessária.

Nota: uvloop não funciona no Windows.

Usar o Encoder JSON Padrão do Python

Toda vez que seu endpoint retorna dados, o Python precisa converter seus objetos (dicionários, listas, classes customizadas) em uma string JSON que pode ser enviada via HTTP. Este processo envolve percorrer toda a estrutura de dados, inspecionar cada valor para determinar seu tipo, e converter pra representação JSON apropriada.

O módulo json nativo do Python é completo, mas lento. Para cada valor único, ele pergunta: "Isso é uma string? Um número? Um booleano? Um objeto aninhado?" Ele lida com todo caso extremo que o Python pode gerar, inclui verificação extensiva de tipos, e gerencia problemas de encoding. Toda essa segurança vem com um custo de performance.

ORJSON é escrito em Rust (btw) com otimizações de performance. Ele usa instruções SIMD (Single Instruction, Multiple Data) para processar múltiplos valores simultaneamente, tem fast paths especializados para tipos de dados comuns, e faz verificação mínima de erros.

from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/data")
async def get_data():
    return {"message": "hello", "items": [1, 2, 3]}

A melhoria é mais relevante para respostas grandes com estruturas de dados profundamente aninhadas, APIs retornando arrays com centenas ou milhares de itens, respostas com muitos números ou datas. Para respostas pequenas (menos de 1KB), a diferença é mínima, mas não há desvantagem em usar ORJSON.

Misturar Funções Async e Sync Incorretamente

Isso é crítico e comumente mal compreendido. A escolha entre async def e def muda fundamentalmente como o FastAPI lida com seu endpoint, e fazer isso errado pode destruir a performance.

Como o FastAPI lida com cada abordagem:

  • Funções async def executam no event loop principal junto com outras requisições
  • Funções def são transferidas para um thread pool separado (limitado a 40 threads por padrão)

Quando usar async def:

  • Operações I/O-bound: chamadas ao banco de dados, requisições HTTP, operações de arquivo
  • Operações que chamam outras funções async
  • Trabalho computacional leve: parsing JSON, validação de dados, transformações simples

Quando usar def regular:

  • Operações CPU-intensivas: processamento de imagens, cálculos pesados, análise de dados
  • Chamar bibliotecas que não são compatíveis com async e fazem trabalho significativo
  • Operações que se beneficiariam de executar em um núcleo de CPU separado
import httpx

# BOM - async para operações I/O
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.example.com/users/{user_id}")
    return response.json()

# BOM - sync para trabalho CPU-intensivo
@app.post("/process-data")
def process_data(data: bytes):
    # Processamento CPU-intensivo
    result = heavy_computation(data)
    return {"result": result}

# RUIM - bloqueando o event loop com trabalho de CPU
@app.post("/process-data-bad")
async def process_data_bad(data: bytes):
    # Isso bloqueia todo o event loop!
    result = heavy_computation(data)
    return {"result": result}

Usar drivers de banco async como asyncpg (PostgreSQL) ou aiomysql (MySQL) com endpoints async pode resultar em throughput 3-5x melhor sob carga concorrente. Isso é porque todo seu pipeline de requisição se torna verdadeiramente assíncrono - sem operações bloqueantes.

Com 40 threads padrão, se você tiver 41 usuários fazendo requests pra endpoints sync simultaneamente, um usuário espera por uma thread ficar disponível. Com 1000 usuários concorrentes atingindo endpoints sync, 960 estão enfileirados esperando por threads.

Usar Pydantic Demais

Pydantic é excelente para validação de dados da API, mas usar em toda parte da sua aplicação cria sobrecarga significativa de performance que muitos desenvolvedores não percebem.

O problema é sutil: modelos Pydantic parecem classes normais, então é tentador usar como suas estruturas de dados primárias em toda sua aplicação. Isso cria o que é conhecido como "dívida de serialização/deserialização" - você está pagando custos de validação e conversão mesmo quando não precisa de validação:

  • Criação de objetos Pydantic é 6.5x mais lenta que dataclasses Python
  • Uso de memória é 2.5x maior devido ao armazenamento de metadados de validação
  • Operações JSON são 1.5x mais lentas através de serialização e deserialização

Essa sobrecarga se acumula rapidamente. Se você estiver criando milhares de objetos durante o processamento de requisições, usar modelos Pydantic internamente pode adicionar latência significativa.

Use Pydantic apenas em partes que você realmente precisa de validação:

from pydantic import BaseModel
from dataclasses import dataclass

# Bom - Pydantic para validação de API
class UserRequest(BaseModel):
    name: str
    email: str
    age: int

# Bom - Dataclass para processamento interno
@dataclass
class UserInternal:
    name: str
    email: str
    age: int
    created_at: datetime
    processed: bool = False

@app.post("/users")
async def create_user(user_request: UserRequest):
    # Pydantic valida a requisição entrante

    # Converte para dataclass interno para processamento
    user = UserInternal(
        name=user_request.name,
        email=user_request.email,
        age=user_request.age,
        created_at=datetime.utcnow()
    )

    # Faz processamento interno com dataclass leve
    process_user(user)

    return {"id": user.id}

# Ruim - Usando Pydantic em todo lugar
class UserPydantic(BaseModel):
    name: str
    email: str
    age: int
    created_at: datetime
    processed: bool = False

def process_user(user: UserPydantic):
    # Toda criação/acesso de objeto paga sobrecarga de validação
    updated_user = UserPydantic(
        **user.dict(),
        processed=True
    )
    return updated_user

A regra prática: valide uma vez na fronteira, depois use estruturas leves internamente. Isso te dá a segurança de validação onde importa sem pagar o custo de performance em toda sua aplicação.

Não Cachear Dependências

FastAPI automaticamente faz cache de resultados de dependências dentro de uma única requisição, mas você também pode criar dependências que persistem através de requisições.

Se múltiplas partes do seu endpoint precisam da mesma dependência com parâmetros idênticos, FastAPI a cria uma vez e a reutiliza dentro daquela requisição. Para recursos caros que não mudam frequentemente, você pode criar singletons que vivem por todo o tempo de vida da sua aplicação.

from functools import lru_cache
from fastapi import Depends

@lru_cache()
def get_settings():
    return {"api_key": "secret", "timeout": 30}

@app.get("/data")
async def get_data(settings = Depends(get_settings)):
    return {"config": settings}

Um singleton garante que apenas uma instância de um recurso existe durante o tempo de vida da sua aplicação. No FastAPI, @lru_cache() sem parâmetros cria comportamento similar a singleton - a função executa uma vez, e todas as chamadas subsequentes retornam o resultado em cache.

Não Fazer Streaming de Respostas Grandes

Pra grandes conjuntos de dados, carregar tudo na memória e retornar de uma vez pode esgotar a memória do servidor e criar experiência terrível para o usuário.

from fastapi.responses import StreamingResponse
import json

@app.get("/large-data")
async def stream_data():
    def generate():
        yield '{"items": ['
        for i in range(10000):
            if i > 0:
                yield ','
            yield json.dumps({"id": i, "name": f"item_{i}"})
        yield ']}'

    return StreamingResponse(generate(), media_type="application/json")

Ao invés de carregar 10.000 registros na memória (potencialmente 100MB+), fazer streaming dos processa um por um mantém o uso de memória abaixo de 1MB. Isso é uma redução de 99%.

Usar Thread Pool Pequeno para Operações Sync

Quando você usa uma função def regular no FastAPI, ela não executa no event loop principal. Ao invés disso, ela executa em um thread pool - uma coleção de worker threads que lidam com operações síncronas. Por padrão, o FastAPI usa apenas 40 threads.

Se 41 usuários atingem um endpoint sync simultaneamente, um espera por uma thread. Com 1000 usuários concorrentes, 960 ficam presos esperando. Isso cria degradação massiva no tempo de resposta.

Se você deve usar operações sync (drivers de banco legados, tarefas CPU-bound, bibliotecas de terceiros sem suporte async), você precisa de mais threads.

import anyio
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app):
    limiter = anyio.to_thread.current_default_thread_limiter()
    limiter.total_tokens = 100
    yield

app = FastAPI(lifespan=lifespan)

@app.post("/process-image")
def process_image(image_data: bytes):
    return expensive_image_processing(image_data)

Converta para async quando possível. Um endpoint async usando asyncio.to_thread() pode lidar com milhares de requisições concorrentes sem limitações do thread pool.

Usar um Único Worker em Produção

Um worker é um processo independente executando sua aplicação FastAPI. Por padrão, você obtém um worker usando um núcleo de CPU, mesmo em servidores com 8+ núcleos.

Seu processo FastAPI executa em um núcleo de CPU. Em um servidor de 4 núcleos, você está usando 25% do poder de processamento disponível. Sob carga, aquele único núcleo se torna um gargalo. Cada worker é um processo FastAPI independente. Com 4 workers em 4 núcleos, você pode lidar com 4x mais requisições concorrentes.

# Worker único (padrão) - usa um núcleo de CPU
uvicorn main:app --host 0.0.0.0 --port 8000

# Múltiplos workers - usa múltiplos núcleos de CPU
uvicorn main:app --workers 4 --host 0.0.0.0 --port 8000

# Melhor: Use Gunicorn para gerenciar workers Uvicorn
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

# Abordagem moderna: FastAPI CLI
fastapi run main.py --workers 4

Gunicorn adiciona gerenciamento de processos, reinicializações automáticas de worker em crashes, shutdowns graciosos, melhor monitoramento de recursos.

Para escolher o número de workers você pode seguir este processo:

  1. Comece com um worker por núcleo de CPU
  2. Monitore sob carga realística
  3. Apps I/O-pesados podem se beneficiar de mais workers que núcleos
  4. Apps CPU-pesados podem sofrer com muitos workers (sobrecarga de mudança de contexto)

Não Comprimir Respostas Grandes

GZip encontra padrões repetidos no seu JSON e os substitui por referências mais curtas. JSON é altamente compressível porque contém chaves repetidas, valores similares, e estrutura previsível.

Uma resposta de API de 500KB pode comprimir para 80KB (84% menor), transformando um download de 2 segundos em conexões lentas em 0.3 segundos.

from fastapi.middleware.gzip import GZipMiddleware

app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)

@app.get("/large-data")
async def get_large_data():
    return {"data": [{"id": i, "name": f"Item {i}"} for i in range(100)]}

Comprimir respostas minúsculas na verdade as torna maiores devido à sobrecarga de compressão. O limite de 1000 bytes garante que apenas respostas que se beneficiam sejam comprimidas. GZip ajuda mais com arrays JSON grandes ou objetos, respostas de API com muito texto, estruturas de dados repetidas, ele não ajuda com dados já comprimidos (imagens, vídeos), respostas muito pequenas ou dados binários.

Usar o BaseHTTPMiddleware

O BaseHTTPMiddleware tem sobrecarga de performance devido a como ele envolve requisições e respostas. Para aplicações de alto tráfego, middlewares ASGI Puros fornecem performance até 40% melhor.

import time
from starlette.types import ASGIApp, Scope, Receive, Send

# Mais lento: abordagem BaseHTTPMiddleware
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

# Mais rápido: middleware ASGI Puro
class ProcessTimeMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        start_time = time.time()

        async def send_with_time(message):
            if message["type"] == "http.response.start":
                process_time = time.time() - start_time
                headers = list(message.get("headers", []))
                headers.append([b"x-process-time", str(process_time).encode()])
                message["headers"] = headers
            await send(message)

        await self.app(scope, receive, send_with_time)

app.add_middleware(ProcessTimeMiddleware)

É mais complexo mas evita a sobrecarga de envolvimento de requisição/resposta do BaseHTTPMiddleware.

Validar Dados Múltiplas Vezes

Se você está usando type hints ou response_model, não há necessidade de criar manualmente modelos Pydantic no seu endpoint - o FastAPI lida com isso automaticamente, fazer ambos causa validação dupla.

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str

# RUIM - validação dupla
@app.get("/users/{user_id}")
async def get_user_bad(user_id: int) -> User:
    user_data = {"id": user_id, "name": "John"}
    user = User(**user_data)  # Primeira validação
    return user  # Segunda validação pelo FastAPI

# BOM - validação única
@app.get("/users/{user_id}")
async def get_user_good(user_id: int) -> User:
    user_data = {"id": user_id, "name": "John"}
    return user_data  # Apenas uma validação pelo FastAPI

Se você tem um type hint ou response_model, retorne dados brutos (dicts, objetos de banco) e deixe o FastAPI lidar com a criação do modelo pydantic. Validação dupla pode adicionar 20-50% de sobrecarga ao processamento de resposta.

Monitore Suas Otimizações

Não adivinhe - meça o impacto das suas otimizações:

import time
import logging

logger = logging.getLogger(__name__)

@app.middleware("http")
async def log_requests(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time

    logger.info(f"{request.method} {request.url.path} - {process_time:.4f}s")
    return response

Foque em otimizar endpoints que lidam com mais tráfego ou levam mais tempo para processar.

A Realidade

Essas otimizações fornecem melhorias significativas de performance - tipicamente 20-50% para aplicações bem arquitetadas. Mas elas não vão te salvar de problemas fundamentais de design.

Se sua API está lenta por causa de consultas N+1 no banco de dados, índices ausentes, ou cache ruim, corrija isso primeiro. Eles te darão melhorias de 10-100x que superam qualquer otimização específica do FastAPI.

Problemas de arquitetura vão matar sua performance. Otimizações do FastAPI vão melhorar sua performance.

Acerte as coisas grandes primeiro, depois melhore com essas técnicas.

Artigo escrito originalmente em inglês no blog do FastroAI e traduzido com ajuda de IA pra postar aqui


Quer um template FastAPI pronto para produção com todas essas otimizações já incluídas? Confira o FastroAI - inclui tudo deste post mais autenticação, integração com banco de dados, e configuração de deployment.

Carregando publicação patrocinada...
1
0