07 de jan. de 2026·7 min de leitura

Statement timeouts para parar consultas fugitivas rapidamente

Aprenda como statement timeouts interrompem consultas fugitivas antes que esgotem conexões. Defina limites sensatos, cancele trabalho travado e mantenha sua app estável.

Statement timeouts para parar consultas fugitivas rapidamente

Por que consultas fugitivas derrubam apps aparentemente normais

Uma consulta fugitiva é uma requisição ao banco que roda muito mais tempo do que você espera. Pode ser um índice faltando, um filtro que força varredura completa da tabela ou um join que explode em milhões de linhas. O resto da aplicação pode estar saudável, mas essa única consulta mantém uma conexão ocupada até terminar.

Em um app web típico, cada requisição pega uma conexão de um pool limitado. Se uma consulta lenta segura uma conexão por 30 a 120 segundos, alguns usuários acessando o mesmo endpoint podem consumir todas as conexões disponíveis. Quando o pool acaba, até requisições rápidas não conseguem uma conexão, então enfileiram, dão timeout ou falham.

Os sintomas parecem um colapso geral mesmo que apenas uma consulta esteja ruim: páginas travam e depois falham, a latência sobe no site inteiro, 500s aumentam conforme workers ficam bloqueados, jobs de background se acumulam e a CPU do banco dispara enquanto o throughput cai.

Isso aparece muito depois de entregar um protótipo gerado por IA. Muitas ferramentas de IA geram demos funcionais que ignoram detalhes de produção, como índices, padrões seguros de consulta e limites. Um recurso que “funcionou” com 200 linhas pode colapsar com 200.000.

Um exemplo concreto: uma página de busca adiciona um filtro flexível como "status contém" ou "nome começa com". Em produção vira um match por curinga numa tabela grande. Uma pessoa exporta resultados, a consulta roda por um minuto, e mais cinco pessoas fazem o mesmo. De repente o pool de conexões está esgotado e o resto da app parece caída.

Timeouts de statement importam porque colocam um teto rígido no dano que uma consulta ruim pode causar.

Statement timeouts: o que fazem e o que não fazem

Um statement timeout é um limite de quanto tempo o banco vai gastar executando uma única instrução SQL. Se a consulta ultrapassa esse limite, o banco a interrompe e retorna um erro em vez de deixá-la continuar consumindo CPU, segurando locks e prendendo uma conexão.

Isso é diferente de limites fora do banco. Um timeout no app ou no load balancer pode fazer o cliente parar de esperar, mas a consulta pode continuar rodando no banco.

Na prática:

  • Timeouts no banco (statement timeouts) interrompem o trabalho SQL no servidor.
  • Timeouts de requisição (app) cessam a espera, mas o banco pode ainda estar ocupado.
  • Timeouts no load balancer cortam a requisição de rede; o banco pode continuar a menos que você também cancele a consulta.

Quando uma consulta longa é abortada, o banco libera o que pode, mas o comportamento depende do contexto. Se a consulta segurava locks, esses locks são liberados quando a instrução é cancelada e revertida. Se você estava dentro de uma transação, muitos bancos marcam a transação como falhada, o que significa que você deve dar rollback antes de fazer qualquer outra operação na mesma conexão. Isso importa porque uma consulta com timeout pode deixar uma conexão efetivamente “envenenada” até que você a limpe.

No lado do app, normalmente você verá um erro como “query canceled” ou “statement timeout.” Trate isso como uma falha esperada: capture o erro, faça rollback da transação se necessário e decida se vai tentar novamente. Não reexecute automaticamente a mesma consulta lenta sem mudar algo.

Usados corretamente, statement timeouts evitam que uma consulta ruim vire exaustão do pool de conexões.

Onde aplicar timeouts: banco, app ou ambos

Use ambos quando puder. Um limite no banco para de fato interromper o trabalho mesmo se seu app travar, enquanto um limite no app mantém seus servidores responsivos e libera threads de requisição.

Timeouts no banco (a parada definitiva)

Defina statement timeouts no banco como uma proteção. Se uma consulta rodar mais do que o permitido, o banco a cancela, protegendo recursos compartilhados como CPU, locks e conexões.

Uma abordagem prática é definir um padrão global sensato e só aumentá-lo para roles ou jobs que realmente precisem. Muitas equipes usam:

  • Um padrão global para o tráfego normal da app
  • Um limite maior para um papel de admin usado para suporte e backfills
  • Um papel separado para jobs de relatório com um orçamento maior

Também decida o escopo. Um timeout pode ser aplicado por conexão (cobre tudo naquela conexão) ou por transação (útil quando você quer limites mais apertados para um fluxo específico).

Timeouts no app (protetor da experiência do usuário)

Seu app ainda deve ter seu próprio deadline para que requisições não fiquem pendentes esperando o banco. Quando o app der timeout, ele deve cancelar a consulta e retornar uma mensagem clara ao usuário, tipo “Esta busca demorou demais. Tente restringir os filtros.”

Para casos especiais, use overrides por consulta em vez de enfraquecer seus padrões. Mantenha esses caminhos explícitos: migrações longas, correções pontuais de dados ou um relatório mensal.

Aqui está a falha que você tenta evitar: um novo filtro “contém” dispara uma varredura lenta numa tabela grande. Sem limites, 20 usuários clicam, todas as conexões ficam presas e a app inteira parece fora do ar. Com timeouts no banco mais cancelamento pela app, essas requisições falham rápido, o pool se recupera e você pode consertar a consulta com segurança.

Escolha um timeout que corresponda à carga real

Um timeout deve proteger sua app sem quebrar o tráfego normal. A maneira mais fácil de errar é escolher um número no achismo. Escolha valores a partir do que seus usuários realmente fazem e adicione pequenas margens intencionais.

Comece com os tempos reais de p95 e p99

Extraia durações de consultas de logs ou das estatísticas do banco e agrupe por endpoint ou tipo de job. Se sua requisição típica da API termina em 120 ms no p95 e 400 ms no p99, um limite de 2 a 3 segundos geralmente é suficiente. Isso pega a rara consulta fugitiva enquanto dá espaço para picos normais.

Se você ainda não tem dados de tempo, comece conservador e aperte depois que tiver distribuições reais.

Use limites diferentes para tipos diferentes de trabalho

A maioria das apps tem pelo menos três classes de trabalho de banco, e elas não devem compartilhar o mesmo teto:

  • Requisições voltadas ao usuário: limites curtos e rígidos
  • Jobs em background: limites mais longos, mas ainda limitados
  • Telas de admin e relatórios: limites mais longos, mas protegidos por controle de acesso e paginação

Mantenha as regras simples. Se um relatório precisa de 30 segundos, tudo bem, mas não rode isso com as mesmas configurações de login ou checkout.

Permita exceções, mas com guardrails

Algumas operações são conhecidamente pesadas: backfills, exports, relatórios de fim de mês. Dê a elas timeouts maiores explícitos, mas exija guardrails como filtros, intervalos de data, paginação ou um limite de linhas.

Exemplo: um relatório “Todos os clientes” sem filtro de data funciona em staging, mas leva minutos em produção e prende conexões. Um timeout maior para relatórios mais um range obrigatório de data evita esse modo de falha.

Passo a passo: adicionar timeouts e cancelamento de consultas

Enviar com padrões mais seguros
Refatoramos código bagunçado e preparamos sua app para releases estáveis e monitoramento.

Comece protegendo o próprio banco. Um padrão seguro significa que uma única consulta ruim não pode ficar lá para sempre e travar conexões. No Postgres, isso costuma ser statement_timeout definido no nível do banco ou do role, assim vale mesmo se um dev esquecer de colocar timeout no código.

-- Example: Postgres
ALTER ROLE app_user SET statement_timeout = '5s';
-- Or for a whole database
ALTER DATABASE app_db SET statement_timeout = '5s';

Em seguida, adicione um timeout mais apertado para ações do usuário no app. Uma pessoa clicando espera uma resposta rápida. Se a requisição atingir o timeout, falhe rápido com uma mensagem clara e deixe-a tentar de novo depois, em vez de esperar e gradualmente esgotar seu pool de conexões.

Para trabalho em background (workers, cron), use um timeout diferente. Jobs costumam tocar mais linhas, então podem receber mais tempo, mas ainda precisam de um teto rígido para que uma execução travada não bloqueie toda a fila.

Uma sequência gerenciável:

  • Defina um timeout padrão no banco ou por role que seja seguro, não perfeito.
  • Aplique um timeout por requisição para endpoints web e APIs.
  • Aplique um timeout por job para workers e tarefas agendadas.
  • Use overrides por consulta apenas quando souber explicar o porquê (por exemplo, um relatório mensal).
  • Verifique em staging com volume de dados e concorrência realistas.

Overrides por consulta são onde equipes se complicam. Trate-os como exceções: registre quando são usados, mantenha o escopo (defina só para essa transação e depois reset), e revisite depois.

Por fim, teste o comportamento de cancelamento, não apenas o tempo. Em staging, rode uma consulta que você sabe que será lenta (como um filtro sem índice) e confirme três coisas: a requisição termina, a consulta para no banco e a conexão volta para o pool.

Tornar o cancelamento confiável do lado do app

Um timeout só ajuda se realmente liberar a conexão do banco. Defina um deadline no nível da requisição na borda do seu app (handler HTTP, runner de jobs, worker de fila) e passe-o até a chamada ao banco. Assim, quando a requisição acabar, a consulta também acaba.

A primeira armadilha é o comportamento do driver. Alguns drivers param de esperar pelo resultado, mas o banco continua executando a consulta. Em produção isso é quase tão ruim quanto não ter timeout, porque a conexão continua ocupada. Teste sua stack forçando uma consulta lenta e verifique duas coisas: o app retorna rápido e o banco mostra que a consulta foi cancelada (não continua rodando em background).

Quando você cancelar, retorne algo que os usuários entendam e que seu código possa agir. “Esta requisição demorou demais, por favor tente novamente” normalmente é suficiente. Separe erros de timeout de falhas reais (erros de sintaxe, permissão) para que monitoramento e retries fiquem honestos.

Retries precisam de regras. Caso contrário dobram a carga durante um incidente:

  • Repetir apenas leituras quando for seguro e você não tiver começado a streamar a resposta.
  • Não reexecutar writes a menos que tenha chaves de idempotência ou uma estratégia de “exatamente uma vez”.
  • Adicione jitter e um pequeno limite (por exemplo, 1 a 2 tentativas), não retries infinitos.
  • Nunca retentar por erros de autenticação ou consultas malformadas.

Registre o suficiente para debugar sem vazar segredos. Capture rota ou nome do job, valor do timeout, tempo decorrido, uma impressão da consulta (hash ou template) e um request ID. Evite logar SQL bruto com dados de usuário, tokens ou strings de conexão.

Erros comuns que fazem timeouts falharem

Timeouts servem para proteger sua app, mas alguns erros de configuração podem transformá-los em falhas ruidosas ou esconder o problema real.

Confiar apenas em timeouts de requisição web é o modo clássico de falha. Se o navegador ou o load balancer desiste após 30 segundos, a consulta no banco pode continuar rodando. Essas consultas “órfãs” mantêm conexões ocupadas mesmo depois do usuário ter saído.

Outro erro comum é definir timeouts muito baixos. Um timeout geral de 200 ms soa seguro, mas pode disparar retries constantes, páginas incompletas e tickets de suporte. Você quer parar verdadeiros runaways, não punir casos normais lentos (cache frio, tenants grandes, picos temporários).

Transações são outra armadilha. Uma consulta com timeout dentro de uma transação pode deixar a transação em estado falho. Se você não tratar corretamente e der rollback, pode segurar locks, bloquear outras requisições e criar um acúmulo que parece que o banco travou.

Finalmente, evite um único timeout para tudo. Páginas interativas precisam de limites apertados, mas exports, backfills e relatórios de admin são diferentes. Dê jobs longos seu próprio caminho e seu próprio timeout maior, para que usuários normais fiquem protegidos.

Como identificar os maiores agressores antes que te derrubem

Transforme um protótipo em produção
FixMyMess diagnostica e corrige código gerado por IA para que suporte tráfego real com segurança.

Statement timeouts são uma rede de segurança, mas você ainda precisa saber quais consultas estão batendo nessa rede.

Comece coletando evidências perto do ponto de falha. Em vez de logar toda consulta (muito ruidoso), foque em consultas lentas e consultas “quase no timeout”. Muitos bancos podem logar consultas mais lentas que um limiar, e também ajuda amostrar requisições que rodaram mais que 70 a 90% do seu timeout. Esse recorte frequentemente mostra os mesmos padrões que depois causam outages.

Observe dois sinais do lado do app junto com logs do banco: com que frequência as consultas são canceladas e se o pool de conexões está saturado. Um aumento no número de cancelamentos junto com um pool próximo do máximo significa que os timeouts estão evitando um crash, mas por pouco.

Monitore consistentemente e alerte quando esses indicadores ficarem altos por vários minutos:

  • Consultas lentas acima de um limiar fixo (e uma contagem separada para consultas próximas ao timeout)
  • Número de consultas canceladas (por endpoint ou tipo de job)
  • Utilização do pool de conexões e tempo de espera por uma conexão livre
  • Taxa de erro e latência p95 para endpoints que consultam o banco
  • Principais fingerprints de consulta (mesma forma, parâmetros diferentes)

Quando armazenar consultas para investigação, guarde o padrão, não dados pessoais. Mantenha placeholders (WHERE email = ?) e o plano ou índice usado, mas evite logar o email real, tokens ou payload completo.

Cenário exemplo: um filtro lento que derruba a app

Um fundador lança uma página simples de busca de “Clientes” com filtros como “Nome da empresa contém …” e “Assinou depois de …”. Em testes parece ok porque o banco é pequeno.

Em produção, um usuário digita um termo comum como “a” e aperta Enter. A app envia uma consulta que não consegue usar índice para o filtro “contém”. O banco varre uma tabela enorme, ordena um grande conjunto de resultados e prende uma conexão aberta.

A cadeia de falhas é previsível:

  • Uma requisição roda por minutos porque varre milhões de linhas.
  • Mais pessoas fazem a mesma busca e cada requisição pega outra conexão.
  • O pool enche, então até endpoints rápidos (login, checkout, admin) começam a dar timeout.
  • A app parece fora do ar, mas o problema real são algumas consultas fugitivas.

Com statement timeouts e cancelamento pela app, você pode definir um limite compatível com sua UX, como 3 a 10 segundos para uma página de busca. Quando a consulta bate o limite, o banco a interrompe. A requisição falha rápido com uma mensagem clara e a conexão volta ao pool.

O benefício chave não é que a consulta fique rápida. É que uma consulta ruim não pode monopolizar recursos tempo suficiente para prejudicar todo o resto.

Depois que o incêndio é apagado, corrija o padrão de forma adequada: adicione o índice certo, mude o filtro para algo que use índice ou mova buscas por “contém” para uma coluna dedicada de busca.

Checklist rápido antes de enviar para produção

Validar sua estratégia de timeouts
Revisamos seus timeouts, estratégias de retry e transações com um revisor especialista.

Antes de fazer deploy, faça uma última checagem para garantir que uma única consulta ruim não possa prender seu pool e derrubar a app.

  • Defina um statement timeout padrão sensato para caminhos de requisição comuns. Mantenha-o baixo o suficiente para proteger o sistema e alto o suficiente para que páginas normais não falhem.
  • Use um timeout separado e mais longo para jobs confiáveis como exports e relatórios, escopados a esses endpoints ou workers.
  • Confirme que o deadline da requisição no app e o timeout do banco funcionam juntos e que o cancelamento é real. Quando uma requisição é abortada, a consulta deve parar e a conexão deve retornar rapidamente.
  • Trate erros de timeout de forma limpa (mensagem clara, código de erro seguro) e evite loops de retry que reexecutam a mesma consulta cara.
  • Monitore consultas próximas do timeout e consultas canceladas para que você corrija os maiores culpados cedo.

Um teste rápido: inicie uma requisição propositalmente lenta (por exemplo, um relatório com um intervalo de data amplo) e cancele-a no navegador. Observe a atividade do banco e os logs do app. Se a consulta continuar rodando depois que a requisição sumiu, você ainda tem uma lacuna no cancelamento.

Próximos passos: estabilizar o banco sem reescrever tudo

Se você já viu uma consulta fugitiva derrubar a app, trate isso como um projeto de segurança. O objetivo é manter o sistema utilizável mesmo quando uma consulta é lenta, um filtro é amplo ou um job fica preso.

Comece auditando onde o tempo pode se estender: endpoints com muitos filtros opcionais, relatórios que varrem grandes intervalos, jobs em background que se espalham e qualquer coisa que rode em agenda. Depois endureça um fluxo real ponta a ponta, como o dashboard que carrega em todo login. Dê a ele um timeout realista, garanta que cancele limpo e confirme que uma consulta lenta não pode bloquear o pool.

Se você herdou código gerado por IA, parta do princípio que há armadilhas até provar o contrário. Dois problemas comuns são N+1 queries (um loop que roda centenas de consultas pequenas) e filtros sem limites (uma busca vazia que retorna a tabela inteira).

Se quiser uma revisão externa de um protótipo que quebra sob tráfego real, FixMyMess (fixmymess.ai) se concentra em transformar apps geradas por IA em software pronto para produção, incluindo diagnosticar caminhos de consultas lentas, corrigir lógica e endurecer a segurança.