← Voltar para o Blog

"Too many open files": entendendo ulimit e descritores

O erro aparece sob carga, some quando você reinicia e volta no próximo pico. O limite existe por um bom motivo — e aumentá-lo no lugar errado não resolve nada.

Equipe EasyOps Cloud · · 7 min de leitura

Too many open files. O erro aparece no pico da tarde, derruba parte das requisições e some depois de um reinício. Na semana seguinte, volta.

É um dos erros mais mal resolvidos do Linux, porque a busca no fórum devolve um comando de ulimit que quase sempre é aplicado no lugar errado — e a pessoa conclui que "aumentou e não adiantou".

Vale entender o mecanismo, porque ele explica tanto a correção quanto o motivo de o limite existir.

O que conta como arquivo aberto

O nome engana. Um descritor de arquivo não é apenas arquivo em disco. O kernel usa o mesmo mecanismo para praticamente tudo que tem entrada e saída:

  • Arquivo comum, aberto para leitura ou escrita.
  • Conexão de rede. Cada socket TCP é um descritor. É daqui que vem a maior parte do consumo em servidor.
  • Pipe entre processos.
  • Socket unix, usado para falar com banco local, com o Docker, com o PHP-FPM.
  • Timer, evento e notificação de arquivo — bibliotecas modernas usam bastante.

Isso muda a intuição. Um servidor web com 5 mil conexões simultâneas tem no mínimo 5 mil descritores só de socket de entrada, mais um por conexão ao backend, mais os arquivos de log, mais os sockets do banco. O total sobe rápido.

Onde os limites moram

Existem três camadas independentes, e confundi-las é a razão da maior parte da confusão:

CamadaEscopoOnde se ajusta
Limite do processoPor processoulimit, systemd, limits.conf
Limite do sistemaToda a máquinafs.file-max (sysctl)
Alocados agoraContadorfs.file-nr

Conferir cada uma:

# Do seu shell atual
ulimit -n           # limite flexível
ulimit -Hn          # limite rígido, o teto que você pode subir

# Do sistema inteiro
sysctl fs.file-max
cat /proc/sys/fs/file-nr    # alocados, livres, máximo

Na prática, o limite do sistema quase nunca é o problema em máquina moderna — ele costuma estar na casa dos milhões. O que barra é o limite por processo, geralmente em 1024 por herança histórica.

Por que aumentar no shell não resolve

Este é o ponto que faz a diferença. O ulimit altera o limite do shell atual e dos processos que ele criar dali em diante. Um serviço iniciado pelo systemd não descende do seu shell — ele descende do init, e não vê nada do que você configurou.

O mesmo vale para /etc/security/limits.conf: ele é aplicado pelo PAM, durante o login. Serviço do systemd não faz login, então também não é afetado.

O resultado clássico: ulimit -n mostra 65535, o serviço continua morrendo em 1024, e ninguém entende.

Confira o limite do processo que interessa, não o do seu shell:

pid=$(systemctl show -p MainPID --value meuapp)
cat /proc/$pid/limits | grep -i 'open files'

Essa linha é a verdade. Se ela mostra 1024, é ali que precisa ser corrigido.

A correção certa por tipo de processo

Serviço do systemd — o caso mais comum:

# /etc/systemd/system/meuapp.service.d/limites.conf
[Service]
LimitNOFILE=65535
sudo systemctl daemon-reload
sudo systemctl restart meuapp

O arquivo em .service.d/ é um complemento: ele sobrevive à atualização do pacote que fornece a unit original, o que editar a unit direto não faz.

nginx tem duas camadas, e as duas precisam concordar:

worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

O worker_connections precisa ser menor que o worker_rlimit_nofile, porque cada conexão de cliente costuma consumir dois descritores quando há proxy — um para o cliente e outro para o backend.

Sessão interativa e processos iniciados por login:

# /etc/security/limits.d/99-nofile.conf
*    soft nofile 65535
*    hard nofile 65535
root soft nofile 65535
root hard nofile 65535

Container Docker:

docker run --ulimit nofile=65535:65535 minha-imagem

Limite baixo ou vazamento?

Antes de aumentar, vale saber se o número é legítimo. Aplicação que vaza descritor vai estourar qualquer limite que você configurar — só demora mais.

# Quantos cada processo tem abertos, os dez maiores
sudo lsof -n 2>/dev/null | awk '{print $2}' | sort | uniq -c \
  | sort -rn | head -10

# Do processo específico, sem lsof
ls /proc/$pid/fd | wc -l

O diagnóstico está no formato da curva, não no valor absoluto:

  • Sobe com o tráfego e cai quando ele cai — comportamento normal. O limite está mesmo baixo.
  • Sobe e nunca desce, mesmo com a máquina ociosa — vazamento. Algo abre e não fecha.

Para confirmar o vazamento, olhe o que está aberto:

sudo ls -l /proc/$pid/fd | awk '{print $NF}' | sort | uniq -c | sort -rn | head

Centenas de descritores para o mesmo arquivo, ou milhares de sockets em estado CLOSE_WAIT, são a assinatura. CLOSE_WAIT em particular significa que o outro lado encerrou e a sua aplicação nunca chamou o fechamento — é bug de código, não de configuração.

ss -tan state close-wait | wc -l

O primo do problema: portas efêmeras

Existe um segundo limite que produz sintoma parecido e exige correção diferente. Em vez de Too many open files, o erro é Cannot assign requested address ou uma recusa de conexão que aparece só sob carga.

A causa é o esgotamento de portas de origem. Toda conexão que o seu servidor inicia — para o banco, para uma API externa, para o backend atrás do proxy — consome uma porta local, tirada de uma faixa limitada. Depois de fechada, a conexão permanece em TIME_WAIT por alguns segundos, e a porta segue reservada nesse intervalo.

Um serviço que abre e fecha conexões em alta frequência acumula milhares de sockets nesse estado e esgota a faixa:

ss -tan state time-wait | wc -l
sysctl net.ipv4.ip_local_port_range

A correção que a internet sugere é ampliar a faixa e reduzir o tempo de espera. Isso ajuda, mas trata o sintoma:

# /etc/sysctl.d/99-rede.conf
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_tw_reuse = 1

A correção real é reaproveitar conexão em vez de criar uma nova a cada operação. Isso significa keepalive no proxy — como no bloco de upstream descrito nos erros de nginx como reverse proxy — e um pool de conexões configurado na aplicação, em vez de conectar e desconectar por requisição.

Vale distinguir os dois problemas antes de mexer: Too many open files é limite de descritor do processo; esgotamento de porta é recurso do sistema inteiro. Aumentar o LimitNOFILE não resolve o segundo, e ampliar a faixa de portas não resolve o primeiro.

Os valores que fazem sentido

Não existe número universal, mas há faixas razoáveis:

  • Aplicação web comum: 8192 a 65535. Cobre com folga a maioria dos casos.
  • Proxy reverso ou balanceador: 65535 ou mais, porque cada conexão custa dois.
  • Banco de dados: siga a recomendação do fornecedor; Postgres e MySQL têm cálculos próprios ligados ao número de conexões.

Exagerar tem custo, ainda que pequeno: cada descritor consome memória de kernel, e um limite altíssimo permite que um processo com vazamento consuma bem mais antes de ser barrado. O limite existe para conter, não apenas para atrapalhar.

Confirmar que pegou

Depois de qualquer mudança, valide no processo real — nunca no shell:

systemctl restart meuapp
pid=$(systemctl show -p MainPID --value meuapp)
grep 'open files' /proc/$pid/limits

E acompanhe o uso ao longo do tempo, que é o que evita a próxima surpresa:

watch -n5 "ls /proc/$pid/fd | wc -l"

Se o número cresce em linha reta com a máquina parada, o problema é a aplicação, e nenhum ajuste de limite vai resolver — só adiar. É o mesmo raciocínio que vale para memória: um limite bem posto contém o estrago, mas quem consome demais de forma anômala precisa ser corrigido, como no caso do OOM killer.

Se o erro aparece junto com lentidão sob carga, vale conferir também se não é saturação de outra ordem — o diagnóstico de latência separa fila de conexão de aplicação lenta, e as duas coisas costumam aparecer juntas no mesmo pico.

Uma última observação que economiza tempo em ambiente com container: o limite que vale é o do processo dentro do container, e ele é herdado do daemon do Docker, não do seu shell nem do limits.conf do host. Se a aplicação containerizada estoura em 1024 enquanto o host mostra 65535, o ajuste precisa ir na definição do container — com --ulimit nofile ou no compose — e o container precisa ser recriado, porque reiniciar não reaplica.

Vale ainda registrar o número em algum lugar visível. Descritor aberto é uma das poucas métricas que preveem falha com antecedência confiável: a curva sobe de forma constante e o erro acontece quando ela cruza a linha. Com o gráfico, a correção acontece antes do incidente em vez de durante.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta