Article image
Ilton Junior
Ilton Junior05/10/2026 14:12
Compartilhe

🚨 Sua API está lenta e você nem percebe: os erros mais comuns em backends Python

    🚨 Sua API está lenta e você nem percebe: os erros mais comuns em backends Python

    Se tem uma coisa que pode quebrar um sistema em produção sem necessariamente gerar um erro é performance ruim escondida.

    A API responde 200 OK.

    O sistema está funcionando.

    Ninguém recebe uma exceção.

    Mas, por baixo dos panos:

    • uma requisição demora 800 ms quando poderia levar 50 ms;
    • o banco está recebendo milhares de queries desnecessárias;
    • a aplicação escala mal;
    • o custo de infraestrutura aumenta;
    • e ninguém percebe até o problema virar incidente.

    Se você trabalha com Python, seja com FastAPI, Django ou Flask, entender performance vai muito além de escolher um framework "rápido".

    O problema normalmente não é simplesmente Python.

    É como o sistema foi construído.

    🔥 1. O problema real: não é Python, é como você usa

    Quando uma aplicação está lenta, é comum alguém apontar imediatamente para a linguagem:

    "Python é lento."

    Na maioria das APIs, essa não deveria ser a primeira conclusão.

    Os maiores gargalos normalmente estão em:

    • acesso ao banco de dados;
    • queries ineficientes;
    • chamadas para serviços externos;
    • serialização e processamento de dados;
    • arquitetura;
    • falta de cache;
    • conexões mal gerenciadas;
    • operações bloqueantes;
    • falta de observabilidade.

    Em aplicações web, grande parte do tempo pode ser consumida esperando por I/O.

    Ou seja:

    Requisição
      ↓
    Aplicação
      ↓
    Banco de dados
      ↓
    API externa
      ↓
    Cache
      ↓
    Resposta
    

    O problema muitas vezes não está no tempo que o Python leva para executar uma instrução.

    Está no tempo que a aplicação passa esperando outras coisas.

    🧨 2. O assassino silencioso: N+1 Query

    Um dos problemas mais comuns em aplicações que utilizam ORM é o famoso N+1 Query.

    Imagine:

    orders = Order.objects.all()
    
    for order in orders:
      print(order.customer.name)
    

    Dependendo da configuração e do relacionamento, podemos acabar fazendo:

    1 query para buscar os pedidos
    +
    1 query para cada customer
    

    Se existirem 100 pedidos:

    1 + 100 = 101 queries
    

    A aplicação funciona.

    Os dados aparecem.

    Mas o banco está trabalhando muito mais do que deveria.

    💥 Uma possível solução

    No Django:

    orders = Order.objects.select_related("customer")
    
    for order in orders:
      print(order.customer.name)
    

    Agora podemos resolver o relacionamento de forma muito mais eficiente.

    Para relacionamentos apropriados, o ORM pode utilizar uma estratégia equivalente a um JOIN, reduzindo drasticamente a quantidade de consultas.

    Também existem situações em que prefetch_related() é mais adequado, especialmente para relacionamentos muitos-para-muitos e reversos.

    Regra prática

    Antes de otimizar Python, descubra:

    Quantas queries essa operação realmente está executando?

    Às vezes, o problema que parecia estar no código Python está escondido no banco.

    ⚡ 3. Falta de cache: por que consultar o banco toda hora?

    Imagine que sua API recebe milhares de requisições para consultar exatamente os mesmos dados.

    Se todas elas fizerem:

    API → Banco → API → Usuário
    

    podemos estar desperdiçando recursos.

    É aí que soluções como Redis entram.

    Uma estratégia simples pode ser:

    Request
     ↓
    Redis?
     ┌─┴─┐
    Sim  Não
     ↓    ↓
    Retorna  Banco
          ↓
        Redis
          ↓
        Retorna
    

    Exemplo simplificado:

    import redis
    
    r = redis.Redis()
    
    def get_user(user_id):
      cached = r.get(f"user:{user_id}")
    
      if cached:
          return cached
    
      user = db.get_user(user_id)
    
      r.set(
          f"user:{user_id}",
          serialize(user),
          ex=60
      )
    
      return user
    

    O objetivo não é colocar Redis em absolutamente tudo.

    Cache também adiciona complexidade.

    É necessário pensar em:

    • TTL;
    • invalidação;
    • consistência;
    • tamanho dos dados;
    • estratégia de chave;
    • taxa de acerto do cache;
    • comportamento quando o cache estiver indisponível.

    Mas, quando bem utilizado, o cache pode reduzir significativamente a carga sobre o banco e melhorar a latência.

    🧱 4. Serialização pesada: o custo que passa despercebido

    Outro problema comum está na transformação dos dados.

    A aplicação possui um objeto.

    Esse objeto precisa virar JSON.

    Parece simples.

    Mas imagine uma resposta contendo:

    • dezenas de campos;
    • relacionamentos aninhados;
    • listas dentro de listas;
    • objetos relacionados;
    • informações que o cliente nem utiliza.

    Podemos acabar gastando processamento e memória desnecessariamente.

    Um endpoint como:

    GET /users/123
    

    talvez não precise retornar:

    {
      "id": 123,
      "name": "Ilton",
      "email": "ilton@example.com",
      "address": {...},
      "orders": [...],
      "payments": [...],
      "history": [...],
      "notifications": [...],
      "permissions": [...],
      "metadata": {...}
    }
    

    se o consumidor só precisa de:

    {
      "id": 123,
      "name": "Ilton"
    }
    

    Estratégia

    Retorne o necessário, não tudo que você consegue retornar.

    Evite serializers gigantes quando eles não são necessários.

    Evite relacionamentos profundamente aninhados.

    E meça o custo da serialização quando houver suspeita de gargalo.

    Em performance, simplicidade frequentemente ganha.

    🔄 5. Sync vs Async: não caia no hype

    "Use async porque é mais rápido."

    Não é tão simples.

    async não transforma automaticamente uma aplicação em uma aplicação mais rápida.

    Ele é especialmente útil quando temos operações de I/O concorrentes e utilizamos bibliotecas compatíveis com programação assíncrona.

    Por exemplo:

    async def get_data():
      return await db.query()
    

    Mas isso só faz sentido se db.query() realmente for uma operação assíncrona.

    Se você colocar código bloqueante dentro de uma função assíncrona:

    async def get_data():
      return requests.get(url)
    

    você pode bloquear o fluxo de execução.

    Ou seja:

    async
    +
    bibliotecas realmente async
    +
    I/O concorrente
    =
    potencial para maior throughput
    

    Mas:

    async
    +
    código bloqueante
    =
    dor de cabeça
    

    A pergunta correta não é:

    "Devo usar async?"

    É:

    "Meu workload se beneficia de concorrência assíncrona?"

    🗃️ 6. O banco de dados pode ser o maior gargalo

    Uma API pode ter um código Python extremamente bem escrito e ainda assim ser lenta por causa do banco.

    Alguns pontos básicos fazem uma diferença enorme.

    Índices

    Imagine uma consulta:

    SELECT *
    FROM users
    WHERE email = 'ilton@example.com';
    

    Se email não tiver um índice adequado, o banco pode precisar analisar uma grande quantidade de registros.

    Um índice pode ajudar:

    CREATE INDEX idx_user_email
    ON users(email);
    

    Mas atenção:

    Índice não é botão mágico de performance.

    Índices também ocupam espaço e possuem custo de manutenção em operações de escrita.

    O ideal é analisar as queries e o plano de execução.

    🔌 Pool de conexões

    Abrir uma nova conexão com o banco para cada requisição pode ser caro.

    Em vez disso, podemos utilizar um connection pool.

    Request 1 ──┐
    Request 2 ──┤
    Request 3 ──┼──→ Connection Pool ──→ Banco
    Request 4 ──┤
    Request 5 ──┘
    

    As conexões são reutilizadas.

    Isso reduz overhead e permite controlar melhor a quantidade de conexões simultâneas.

    📄 Paginação

    Outro problema clássico:

    SELECT *
    FROM orders;
    

    Se existem milhões de registros, trazer tudo de uma vez não é uma boa ideia.

    Uma abordagem inicial pode ser:

    SELECT *
    FROM orders
    LIMIT 50;
    

    Em APIs reais, também precisamos pensar na estratégia de paginação.

    OFFSET pode funcionar bem para conjuntos menores, mas pode se tornar caro quando precisamos navegar por páginas muito profundas.

    Para grandes volumes, estratégias como cursor pagination ou paginação baseada em uma coluna indexada podem ser mais eficientes.

    O objetivo é simples:

    Não carregue 1 milhão de registros quando o usuário precisa visualizar 50.

    📊 7. Se você não mede, você está chutando

    Performance não deveria ser baseada em sensação.

    Não adianta alguém dizer:

    "Essa API parece lenta."

    Precisamos medir.

    Algumas métricas importantes:

    Latência

    Quanto tempo uma requisição leva.

    Por exemplo:

    P50 → 80 ms
    P95 → 180 ms
    P99 → 900 ms
    

    Olhar apenas para a média pode esconder problemas.

    Uma API pode ter média de 100 ms enquanto uma parcela das requisições leva vários segundos.

    Throughput

    Quantidade de requisições processadas em determinado período.

    1.000 requests/s
    

    Isso ajuda a entender a capacidade do sistema.

    Taxa de erro

    HTTP 5xx
    HTTP 4xx
    Timeouts
    Falhas externas
    

    Recursos

    Também podemos acompanhar:

    • CPU;
    • memória;
    • conexões;
    • utilização do banco;
    • filas;
    • cache hit rate;
    • tempo de consultas.

    🔎 8. Observabilidade ajuda a encontrar o gargalo

    Logs, métricas e tracing são fundamentais para investigar performance.

    Ferramentas como:

    • Datadog;
    • New Relic;
    • OpenTelemetry;
    • Prometheus;
    • Grafana;

    podem ajudar a entender o comportamento da aplicação.

    Imagine que uma requisição leva 2 segundos.

    Sem observabilidade:

    API: 2 segundos
    

    Com tracing:

    Request
    │
    ├── API: 2.000 ms
    │
    ├── PostgreSQL: 1.650 ms
    │
    ├── Redis: 20 ms
    │
    └── API externa: 300 ms
    

    Agora temos uma direção clara.

    O problema provavelmente não está no Python.

    Está no banco.

    E isso muda completamente a investigação.

    🧠 9. O que separa um desenvolvedor que apenas corrige de um que investiga?

    Uma diferença importante é a forma de pensar.

    Desenvolvedor iniciante

    "Minha API está lenta."

    Desenvolvedor intermediário

    "Vou procurar onde está o gargalo."

    Desenvolvedor mais experiente

    "Vou medir a latência, identificar o componente responsável, entender a causa e validar a melhoria com métricas."

    Essa diferença é importante.

    O objetivo não é sair aplicando otimizações aleatórias.

    É:

    Problema
     ↓
    Medição
     ↓
    Hipótese
     ↓
    Teste
     ↓
    Otimização
     ↓
    Nova medição
    

    Performance sem medição vira opinião.

    🚀 10. Então, por onde começar?

    Se sua API está lenta, eu começaria investigando nesta ordem:

    1. Queries
     ↓
    N+1?
    Índices?
    Plano de execução?
    
    2. Banco
     ↓
    Pool?
    Locks?
    Carga?
    Conexões?
    
    3. Cache
     ↓
    Existe algo sendo consultado repetidamente?
    
    4. Serialização
     ↓
    Estamos retornando dados demais?
    
    5. I/O externo
     ↓
    Alguma API está lenta?
    
    6. Concorrência
     ↓
    Existe trabalho independente que pode ser executado concorrentemente?
    
    7. Infraestrutura
     ↓
    CPU?
    Memória?
    Rede?
    Containers?
    

    Não existe uma ordem universal para todos os sistemas, mas essa abordagem ajuda a evitar o clássico:

    "Vamos colocar Redis."

    Sem nem saber se o problema está no Redis, no banco ou em uma query mal construída.

    🎯 O ponto principal

    Performance não é simplesmente fazer o código executar mais rápido.

    É entender onde o tempo está sendo gasto.

    Uma API rápida não necessariamente possui um código cheio de otimizações mirabolantes.

    Muitas vezes ela simplesmente:

    • executa menos queries;
    • utiliza índices corretamente;
    • reutiliza conexões;
    • evita processamento desnecessário;
    • utiliza cache quando faz sentido;
    • não retorna dados excessivos;
    • utiliza concorrência quando necessário;
    • mede o comportamento em produção.

    🧠 O que realmente separa pleno de sênior?

    Talvez não seja:

    "Minha API funciona."

    Mas:

    "Eu consigo explicar onde ela gasta tempo, quanto custa escalar e qual é o impacto de cada otimização."

    Essa mudança de mentalidade é importante.

    Porque otimizar código não significa simplesmente deixar uma função mais rápida.

    Significa entender o sistema inteiro.

    Código
    ↓
    API
    ↓
    Banco
    ↓
    Cache
    ↓
    Serviços externos
    ↓
    Infraestrutura
    ↓
    Usuário
    

    Performance é uma propriedade do sistema, não apenas de uma função.

    🚀 Resumo direto ao ponto

    Se sua API está lenta, investigue:

    🧨 Queries

    N+1, joins desnecessários e consultas mal construídas.

    ⚡ Cache

    Redis e outras estratégias quando existe acesso repetitivo aos mesmos dados.

    🧱 Serialização

    Retorne somente o necessário.

    🗃️ Banco

    Índices, connection pool, locks, planos de execução e paginação.

    🔄 Async

    Use quando o workload realmente se beneficia de I/O concorrente.

    📊 Métricas

    Meça latência, throughput, erros e consumo de recursos.

    🔎 Observabilidade

    Use logs, métricas e tracing para descobrir onde o tempo está sendo gasto.

    💬 Conclusão

    Performance não deveria ser uma preocupação apenas quando o sistema começa a quebrar.

    Mas também não significa sair otimizando tudo antes de existir um problema.

    A abordagem mais saudável é:

    Construa de forma simples, meça, encontre os gargalos e otimize o que realmente importa.

    No mundo real, quem resolve problemas de performance não está simplesmente "deixando o código mais rápido".

    Está reduzindo latência.

    Está aumentando capacidade.

    Está evitando incidentes.

    Está diminuindo custos de infraestrutura.

    E, principalmente, está tornando o sistema mais previsível.

    No final, a pergunta não deveria ser apenas:

    "Minha API funciona?"

    Mas também:

    "Quanto tempo ela leva, quanto custa para escalar e o que acontece quando a carga aumenta?"

    Porque uma API que funciona com 10 requisições por segundo é fácil.

    O verdadeiro desafio começa quando são 1.000, 10.000 ou 100.000.

    E é aí que performance deixa de ser detalhe e passa a ser engenharia de software.

    Compartilhe
    Comentários (0)