FastAPI em Produção: 7 Ajustes Essenciais em Uvicorn e Gunicorn para Escala e Estabilidade

Published on: 2025-12-29
Post image
pt uvicorn gunicorn fastapi-em-producao servidor-asgi performance-fastapi tuning-gunicorn uvicorn-performance backend-python api-escalavel engenharia-de-software-backend

Uvicorn e Gunicorn são servidores muito usados para colocar aplicações Python em produção, especialmente APIs construídas com FastAPI e outros frameworks que seguem o padrão ASGI (Interface Assíncrona de Gateway do Servidor). Em produção, a diferença entre um serviço “funciona” e um serviço “aguenta tráfego real” costuma estar em algumas configurações que alteram filas, concorrência, uso de CPU e comportamento de rede.

O ajuste de desempenho, nesse contexto, não depende de dezenas de parâmetros, mas de poucos controles que afetam diretamente throughput (volume de requisições atendidas por segundo), latência (tempo de resposta) e estabilidade sob picos. A seguir estão sete configurações que costumam gerar ganhos reais, com explicações claras e comandos completos para Uvicorn e Gunicorn.

1) Workers alinhados ao modelo de concorrência e ao CPU

Worker é um processo que executa a aplicação e atende requisições. Mais workers podem aumentar o paralelismo, mas também aumentam o consumo de memória e o custo de alternância de contexto no sistema operacional. Em aplicações ASGI assíncronas, a concorrência dentro de um mesmo processo ocorre via event loop (laço de eventos), o que reduz a necessidade de muitos workers em cargas de I/O. Em rotas com trabalho pesado de CPU (serialização grande, criptografia, cálculos), mais workers podem ajudar, mas o ideal é reduzir CPU dentro da rota ou isolar esse trabalho.

Uma heurística prática em ASGI é começar com workers igual ao mínimo entre 4 e o número de núcleos de CPU, e então medir com teste de carga. Em cargas majoritariamente de I/O (banco, cache, chamadas HTTP), poucos workers assíncronos costumam vencer a regra antiga de WSGI síncrono “(2×CPU)+1”. Em cargas com CPU alto, workers extras podem reduzir fila, mas o ganho tende a estabilizar cedo. O objetivo é equilibrar fila de requisições, CPU e memória, evitando saturar o host com processos demais.

Os comandos abaixo mostram como configurar workers nos dois modos, e o trecho apresentado ilustra o formato típico de execução em produção.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080
uvicorn app.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8080

2) Keep-alive para reduzir custo de handshake sem prender conexões

Keep-alive é o tempo em que uma conexão TCP fica aberta e reutilizável para novas requisições. Reutilizar conexões reduz custo de handshake (especialmente com TLS), evita penalidades de início lento e diminui trabalho total por requisição. Em contrapartida, keep-alive alto demais pode manter sockets ocupados por poucos clientes, gerando injustiça sob pico. O ponto de equilíbrio costuma ser um tempo moderado, que favorece clientes “falantes” sem esgotar recursos.

Valores práticos comuns ficam entre 5 e 15 segundos. O servidor na borda (balanceador, proxy reverso) deve ter keep-alive compatível ou ligeiramente maior, para evitar fechamentos prematuros. Em cenários de rajadas, reduzir keep-alive pode melhorar picos de latência porque libera sockets mais rapidamente. A mudança costuma aparecer sobretudo em p95/p99, onde contenção de conexões é mais visível.

Os exemplos abaixo mostram as flags equivalentes em Gunicorn e Uvicorn, mantendo o serviço responsivo sob tráfego variável.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080 \
  --keep-alive 10
uvicorn app.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8080 \
  --timeout-keep-alive 10

3) Backlog para absorver picos curtos sem mascarar sobrecarga contínua

Backlog é o tamanho da fila de conexões pendentes antes de serem aceitas pelo servidor. Em termos simples, é uma “área de espera” controlada pelo kernel entre o tráfego chegando e a aplicação efetivamente chamando accept(). Backlog pequeno pode causar quedas e recusas durante picos, porque a fila estoura antes que os workers consigam aceitar novas conexões. Backlog grande demais pode atrasar a percepção de falha, porque o balanceador ainda consegue abrir conexão, mesmo quando o serviço já está afogado.

Um ponto de partida frequente em serviços ocupados é 2048, que costuma ser várias vezes o padrão de algumas configurações. Esse aumento ajuda a absorver oscilações curtas, como picos de alguns segundos. Ele não resolve sobrecarga sustentada, pois apenas empilha mais conexões aguardando. O ajuste correto aparece quando as quedas em pico diminuem sem aumentar demais o tempo de fila e a latência de cauda.

A seguir estão comandos equivalentes para configurar backlog em ambos os servidores.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080 \
  --backlog 2048
uvicorn app.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8080 \
  --backlog 2048

4) Timeouts coerentes com o comportamento real e com desligamentos

Timeout define quanto tempo uma requisição pode ficar em execução antes de ser interrompida. Um timeout muito baixo derruba requisições legítimas em momentos de lentidão do banco ou do downstream, gerando erros falsos. Um timeout muito alto prende workers com requisições “travadas”, piorando filas e degradando o serviço inteiro. O ideal é definir um orçamento de tempo realista para a operação e falhar de forma previsível quando esse orçamento estoura.

No Gunicorn, dois parâmetros importantes são timeout (mata requisições presas) e graceful-timeout (tempo extra para finalizar requisições em andamento durante reinício). Em plataformas com orquestração, desligamentos também envolvem sinais e janelas de término, o que exige alinhamento entre servidor, probe e tempo de término do processo. Serviços com conexões longas (SSE e WebSocket) precisam de estratégia de desligamento que priorize finalizar conexões de maneira ordenada durante deploy. Ajustes típicos ficam em 30 segundos de timeout e 10 segundos de janela de graça, variando conforme o perfil.

O comando abaixo exemplifica esse orçamento com Gunicorn, preservando previsibilidade em reinícios e evitando workers presos por tempo indefinido.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080 \
  --timeout 30 \
  --graceful-timeout 10

5) Max-requests para conter vazamentos lentos de memória com jitter

Max-requests força o reinício controlado de um worker após ele atender um certo número de requisições. Esse padrão é útil para mitigar vazamentos pequenos e progressivos, fragmentação de memória e crescimento lento de RSS, mesmo quando não existe um bug óbvio. Reiniciar workers periodicamente “reseta” o consumo, mantendo a curva de memória mais previsível. Sem cuidados, porém, vários workers podem reiniciar ao mesmo tempo e causar perda de capacidade momentânea.

Para evitar reinícios sincronizados, utiliza-se jitter, um valor aleatório adicional que espalha os reinícios ao longo do tempo. Uma heurística prática é escolher um número que gere reciclagem a cada 15 a 60 minutos em carga estável, ajustando conforme a taxa de requisições. Quando há um gráfico em “dente de serra” no consumo de memória, reduzir max-requests costuma diminuir a amplitude. No Uvicorn puro, esse mecanismo não é nativo, então o uso de Gunicorn como gerenciador de processos é a forma comum de obter reciclagem.

O exemplo abaixo mostra uma configuração equilibrada, com jitter suficiente para evitar “manadas” de reinício.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080 \
  --max-requests 2000 \
  --max-requests-jitter 200

6) Seleção da pilha assíncrona: httptools e uvloop

Em servidores ASGI, parte do custo por requisição vem do parse de HTTP e do agendamento de tarefas no laço de eventos. uvloop é uma implementação de event loop mais rápida, e httptools é um parser de HTTP eficiente, ambos usados para reduzir overhead em alta concorrência. A diferença aparece mais em cenários com muitas conexões simultâneas e volume alto, onde micro-otimizações somam. Em tráfego pequeno, a percepção pode ser mínima, porque o gargalo costuma estar em I/O externo.

No Uvicorn, a seleção pode ser explícita por flags, garantindo que a execução está usando esses componentes. No Gunicorn com worker do Uvicorn, o uvloop costuma ser adotado automaticamente quando instalado, mas o comportamento depende das dependências no ambiente. Um ganho consistente depende também de manter bibliotecas de I/O assíncrono corretas e evitar bloquear o event loop com operações síncronas longas. O foco é reduzir custo fixo por requisição e melhorar latência de cauda em picos.

O comando abaixo exemplifica a ativação explícita no Uvicorn, com workers e pilha de rede otimizados.

uvicorn app.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8080 \
  --http httptools \
  --loop uvloop

7) Logging sob carga: útil para incidentes sem virar gargalo

Logs em excesso podem virar um gargalo justamente quando a carga sobe, porque escrita em stdout, formatação e volume de I/O aumentam o tempo total por requisição. Ao mesmo tempo, logs pobres dificultam diagnóstico de picos de p99, erros intermitentes e degradação de dependências. Um formato de log eficiente equilibra dados essenciais, como método, rota, status e tempo de resposta. Em produção, nível de log debug tende a ser caro e ruidoso, aumentando latência e custo.

No Gunicorn, o formato de access log pode incluir latência com %(L)s, o que facilita enxergar a distribuição de tempos. Uma prática comum é manter logs de acesso, mas reduzir verbosidade e considerar amostragem em períodos de pico, preservando 4xx/5xx integralmente. No Uvicorn, habilitar access log e manter nível info costuma ser suficiente para observabilidade básica. O resultado esperado é manter capacidade sob carga sem perder rastreabilidade operacional.

Os comandos abaixo mostram um formato enxuto e informativo, com tempo de requisição incluso.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080 \
  --access-logfile - \
  --access-logformat '%(h)s "%(r)s" %(s)s %(b)s %(L)s' \
  --log-level info
uvicorn app.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8080 \
  --access-log \
  --log-level info

Comandos mínimos de produção: Gunicorn com worker Uvicorn e Uvicorn puro

Uma forma prática de consolidar as sete configurações é manter uma linha de comando padrão, alterando principalmente o número de workers conforme CPU e limite de recursos. Gunicorn é frequentemente usado como gerenciador de processos, agregando reciclagem por max-requests e controle de timeouts de forma central. Uvicorn puro costuma ser atraente em containers simples, quando reciclagem não é necessária ou é delegada ao orquestrador. Em ambos os casos, as flags abaixo representam um conjunto consistente para throughput, latência e estabilidade.

As duas receitas a seguir mostram configurações completas e coerentes, cobrindo workers, keep-alive, backlog, timeouts e logging.

gunicorn app.main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8080 \
  --keep-alive 10 \
  --backlog 2048 \
  --timeout 30 \
  --graceful-timeout 10 \
  --max-requests 2000 \
  --max-requests-jitter 200 \
  --access-logfile - \
  --access-logformat '%(h)s "%(r)s" %(s)s %(b)s %(L)s' \
  --log-level info
uvicorn app.main:app \
  --workers 4 \
  --host 0.0.0.0 \
  --port 8080 \
  --http httptools \
  --loop uvloop \
  --timeout-keep-alive 10 \
  --backlog 2048 \
  --access-log \
  --log-level info

Padrão de Dockerfile multi-stage para execução previsível

Em ambientes com containers, um Dockerfile enxuto e previsível evita diferenças entre ambientes e reduz variáveis na hora de diagnosticar desempenho. Uma imagem baseada em Python slim com dependências de servidor instaladas costuma ser suficiente para muitas APIs. Manter o comando final com Gunicorn e worker do Uvicorn centraliza as configurações críticas e habilita reciclagem por max-requests. O resultado é um padrão replicável, com poucas peças móveis e fácil ajuste por CPU disponível.

O exemplo abaixo apresenta um Dockerfile completo e funcional, mantendo a configuração do servidor no comando de inicialização. Os identificadores e comentários estão em português e focam apenas no essencial.

FROM python:3.11-slim AS base

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# Instala servidores e extras de performance (inclui uvloop/httptools via padrão do uvicorn)
RUN pip install --no-cache-dir uvicorn[standard] gunicorn

WORKDIR /app
COPY app/ /app/app

# Inicia com Gunicorn gerenciando processos e Uvicorn como worker ASGI
CMD ["gunicorn", "app.main:app",
     "--worker-class", "uvicorn.workers.UvicornWorker",
     "--workers", "4",
     "--bind", "0.0.0.0:8080",
     "--keep-alive", "10",
     "--backlog", "2048",
     "--timeout", "30",
     "--graceful-timeout", "10",
     "--max-requests", "2000",
     "--max-requests-jitter", "200",
     "--access-logfile", "-",
     "--access-logformat", "%(h)s \"%(r)s\" %(s)s %(b)s %(L)s",
     "--log-level", "info"]

Conclusão: poucos ajustes mudam o fluxo de requisições e o custo por resposta

O desempenho de aplicações ASGI com FastAPI depende de como conexões entram no processo, como são enfileiradas e como o event loop compartilha tempo entre tarefas. As sete configurações apresentadas atuam justamente nesses pontos: workers equilibram paralelismo e custo de CPU, keep-alive reduz handshakes sem prender sockets, backlog absorve picos curtos e timeouts evitam requisições presas. A reciclagem por max-requests estabiliza memória ao longo do tempo e o uso de uvloop e httptools diminui overhead em alta concorrência. Um logging enxuto completa o conjunto, preservando capacidade em pico e visibilidade operacional.