---
title: "A fila de locks que derruba o PostgreSQL antes da sua migration terminar"
author: "Pedro Dalben"
date: "2026-09-30T11:02:51Z"
category: "DevOps & Cloud"
reading_time: "7 min"
canonical: "https://pedrodalben.com.br/blog/a-fila-de-locks-que-derruba-o-postgresql-antes-da-sua-migration-terminar"
---

# A fila de locks que derruba o PostgreSQL antes da sua migration terminar

> Como comandos rápidos de DDL travam pools de conexão inteiros em produção e quais práticas evitam paradas não planejadas no deploy.

**Autor:** Pedro Dalben (CTO no Integrar Plus)  
**Publicado em:** 30 de September de 2026  
**URL Canônica Oficial:** [https://pedrodalben.com.br/blog/a-fila-de-locks-que-derruba-o-postgresql-antes-da-sua-migration-terminar](https://pedrodalben.com.br/blog/a-fila-de-locks-que-derruba-o-postgresql-antes-da-sua-migration-terminar)  

---


A maioria dos incidentes graves de banco de dados durante deploys não acontece porque uma migration demorou vinte minutos para reescrever uma tabela. Acontece quando uma alteração que deveria levar dois milissegundos entra em produção, fica presa atrás de uma leitura qualquer e congela todas as conexões da aplicação até a infraestrutura inteira cair.

O lançamento de ferramentas como o safenotsafe.dev reacendeu uma discussão velha entre quem cuida de banco de dados. De um lado, defensores de linters estáticos afirmam que regras sintáticas no pipeline de CI eliminam o risco de deploys desastrosos. De outro, engenheiros de plataforma apontam que a segurança de um comando DDL depende do estado vivo do catálogo, do volume dos dados e dos tipos originais das colunas, tornando qualquer análise puramente baseada em texto incompleta por definição.

Eu acho que essa briga erra o alvo principal.

Analisadores de sintaxe ajudam a filtrar bobagens óbvias no pull request, como tentar validar uma chave estrangeira pesada em um único passo ou criar índices bloqueantes em tabelas grandes. Só que eles criam uma ilusão perigosa de tranquilidade. O maior perigo para quem opera o próprio PostgreSQL em servidores dedicados ou VPS não está no tempo de execução do comando em si, mas na fila de espera por locks exclusivos.

### A mecânica do travamento em cascata

Para alterar qualquer propriedade de uma tabela, como adicionar uma coluna nova ou trocar um valor padrão, o PostgreSQL precisa obter um lock do tipo `AccessExclusiveLock`. Esse é o nível mais restritivo do banco. Ele impede qualquer leitura ou escrita concorrente naquela relação até que a transação termine.

Em tese, adicionar uma coluna sem valor padrão ou com valor padrão estático no PostgreSQL moderno leva fração de milissegundo. O comando apenas atualiza os metadados no catálogo interno. O estrago surge quando esse comando encontra tráfego real.

Se houver uma consulta analítica, um relatório lento ou um `SELECT` esquecido rodando há quatro segundos na mesma tabela, o `ALTER TABLE` não consegue adquirir o lock imediatamente. Ele entra na fila do PostgreSQL e fica aguardando a leitura anterior terminar.

Aqui está o detalhe que muitos desenvolvedores ignoram: para evitar que a transação de DDL sofra inanição infinita, o PostgreSQL bloqueia todas as consultas subsequentes que chegam para aquela tabela, mesmo que sejam simples leituras via chave primária.

```
Transação 1 (SELECT lento): [ Executando... 5s ]
Transação 2 (ALTER TABLE):  [ Esperando lock exclusivo... ]
Transação 3 (SELECT id = 1): [ BLOQUEADO pela Transação 2 ]
Transação 4 (INSERT ...):    [ BLOQUEADO pela Transação 2 ]
```

Em um sistema com dezenas de requisições web por segundo, três segundos de espera significam dezenas de conexões presas na aplicação. Se você roda Rails com servidores como Puma, cada thread que fica parada esperando o banco bloqueia um worker inteiro. Em menos de trinta segundos, o pool de conexões do PgBouncer ou do próprio Rails esgota. O servidor web para de aceitar novas requisições e o usuário recebe erros de timeout HTTP 502 ou 504 no navegador.

A migration era segura segundo qualquer linter estático. O comando em si durou um milésimo de segundo quando finalmente rodou. Mesmo assim, o sistema ficou fora do ar.

### O parâmetro esquecido: lock_timeout

Nenhuma ferramenta de análise estática substitui uma configuração defensiva de sessão. A primeira linha de defesa contra o travamento em cascata é garantir que nenhuma migration espere indefinidamente para conseguir acesso exclusivo.

O PostgreSQL possui a variável `lock_timeout`. Ela define quanto tempo uma instrução pode esperar na fila para adquirir um lock antes de abortar a operação com erro.

```sql
SET lock_timeout = "2s";
ALTER TABLE faturas ADD COLUMN status_entrega varchar(20);
```

Se o banco estiver ocupado e não liberar o lock em dois segundos, a transação morre imediatamente, a fila é desfeita e o tráfego da aplicação segue funcionando normalmente. A migration falha, o deploy é interrompido no pipeline, mas a produção continua de pé para os clientes.

Para quem usa Ruby on Rails, essa proteção pode ser aplicada globalmente ou via gems como a strong_migrations, mas vale a pena entender o que está acontecendo por baixo dos panos no arquivo de migração:

```ruby
class AddStatusEntregaToFaturas < ActiveRecord::Migration[8.0]
  disable_ddl_transaction!

  def change
    execute "SET lock_timeout TO '1500ms'"
    add_column :faturas, :status_entrega, :string
  ensure
    execute "RESET lock_timeout"
  end
end
```

Se falhar por timeout, você pode tentar rodar o deploy novamente em um horário de menor concorrência ou utilizar scripts que tentam obter o lock em intervalos curtos com retentativas controladas. Uma migration falhar no CI ou no script de release é um incômodo leve; derrubar o pool de conexões ao vivo é um incidente de severidade alta.

### Criando índices e constraints sem surpresas

Além de controlar o tempo de fila, certas alterações estruturais exigem divisão explícita em etapas. O exemplo mais comum é a criação de índices.

Rodar um simples `CREATE INDEX` em uma tabela com milhões de registros solicita um lock do tipo `ShareLock`. Ele permite que leituras continuem acontecendo, mas bloqueia qualquer escrita (`INSERT`, `UPDATE`, `DELETE`). Se a criação do índice demorar três minutos, sua aplicação ficará três minutos sem conseguir registrar nada no banco.

A regra operacional é usar a opção concorrente fora de transações:

```sql
-- Nunca faça dentro de um bloco BEGIN/COMMIT
CREATE INDEX CONCURRENTLY idx_faturas_cliente_id ON faturas (cliente_id);
```

O modificador `CONCURRENTLY` exige duas varreduras completas na tabela para construir a estrutura sem impedir escritas, o que consome mais tempo total de CPU e I/O, mas reduz drasticamente o nível de bloqueio.

O mesmo cuidado se aplica à criação de chaves estrangeiras. Adicionar uma constraint de foreign key diretamente trava tanto a tabela de origem quanto a tabela de destino para validar cada linha existente. A saída limpa é separar o registro da regra e a validação dos dados em duas fases:

```sql
-- Fase 1: cria a constraint sem validar dados passados (lock curto)
ALTER TABLE faturas 
  ADD CONSTRAINT fk_faturas_clientes 
  FOREIGN KEY (cliente_id) 
  REFERENCES clientes(id) 
  NOT VALID;

-- Fase 2: valida os registros sem bloquear leituras ou escritas
ALTER TABLE faturas 
  VALIDATE CONSTRAINT fk_faturas_clientes;
```

A cláusula `NOT VALID` garante que novos registros inseridos a partir daquele momento respeitem a integridade referencial, enquanto a validação em si roda em segundo plano sem exigir locks exclusivos pesados sobre a tabela inteira.

### Limitações e o que ainda pode falhar

Seria ingênuo dizer que seguir essas três regras elimina todas as armadilhas operacionais do PostgreSQL. Existem casos em que o comportamento depende do tipo de dado original da coluna. Converter um campo de texto livre para um tipo enumerado, por exemplo, pode forçar uma reescrita física de todos os blocos no disco, gerando contenção violenta de I/O mesmo se o lock for obtido com rapidez.

Além disso, rodar migrations seguras contra réplicas de leitura antes do deploy principal ajuda a capturar incompatibilidades de dados, mas não reproduz fielmente a pressão de conexões do nó primário.

Ferramentas automáticas de revisão de schema têm o seu valor para alertar sobre erros primários no código, mas o que mantém um banco estável durante deploys diários é o respeito aos mecanismos internos de locking. Tratar cada DDL como uma operação potencialmente hostil ao pool de conexões é a única postura que protege a produção no longo prazo.

## Fontes

- [https://safenotsafe.dev/](https://safenotsafe.dev/)
