"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:
| Camada | Escopo | Onde se ajusta |
|---|---|---|
| Limite do processo | Por processo | ulimit, systemd, limits.conf |
| Limite do sistema | Toda a máquina | fs.file-max (sysctl) |
| Alocados agora | Contador | fs.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áximoNa 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=65535sudo systemctl daemon-reload
sudo systemctl restart meuappO 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 65535Container Docker:
docker run --ulimit nofile=65535:65535 minha-imagemLimite 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 -lO 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 | headCentenas 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 -lO 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_rangeA 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 = 1A 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/limitsE 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.