28 de jul. de 2025·6 min de leitura

EMFILE muitos arquivos abertos Node: depure em produção

Erros EMFILE em Node geralmente vêm de handles vazando em apps gerados por IA. Veja causas comuns e verificações rápidas em produção para confirmar a correção.

EMFILE muitos arquivos abertos Node: depure em produção

O que “muitos arquivos abertos” realmente significa

O erro geralmente aparece como EMFILE: too many open files (ou ENFILE). Significa que seu app esgotou os file descriptors.

Um file descriptor é um pequeno identificador que o sistema operacional dá a um processo quando ele abre algo como um arquivo, um socket de rede, um arquivo de log ou um diretório. Quando o processo atinge o limite, novas aberturas falham.

Isso pode quebrar partes do seu app que nem parecem “arquivos”: chamadas de API, conexões com banco, uploads, server-side rendering, até leitura de arquivos de configuração. Por isso o mesmo incidente pode parecer 500s aleatórios até que você pegue a mensagem real nos logs: EMFILE.

Freqüentemente aparece só após horas ou dias porque vazamentos podem ser lentos. Uma requisição pode abrir um arquivo ou socket e esquecer de fechá-lo. Um vazamento é invisível ao começo. Dez mil requisições depois, a próxima é a que falha.

Apps Node gerados por IA têm mais chance de vazar recursos porque costumam juntar trechos sem um ciclo de vida claro. Sinais comuns são blocos finally ausentes, listeners adicionados a cada requisição, streams de arquivo sem tratadores de erro, ou “gambiarras” que abrem conexões novas ao invés de reutilizar as existentes.

Quando acontece, capture um pequeno snapshot imediatamente. Geralmente é suficiente para ligar o pico a um código e tráfego específicos:

  • a janela de tempo (primeiro erro e pico)
  • qual endpoint, job ou worker estava rodando
  • versão/commit do deploy e quaisquer mudanças de configuração
  • uma stack trace completa e logs próximos
  • nível de tráfego (normal, pico, ou job de background)

Como isso aparece em deploys Node

EMFILE raramente começa como uma falha óbvia e limpa. A maioria das equipes nota primeiro “500s aleatórios”, requisições presas, ou um container que para de aceitar conexões mesmo com CPU e memória aparentando normais.

Dois formatos de vazamento aparecem em produção:

  • Vazamentos por requisição falham rápido. Um estouro de tráfego provoca falhas em minutos porque cada requisição deixa um arquivo, socket, ou watcher aberto.
  • Vazamentos lentos falham tarde. O app roda por horas ou dias e então quebra após um gotejamento constante de recursos não fechados.

Nos logs e comportamento, muitas vezes parece com isto:

  • picos de 5xx que se recuperam após um restart
  • uploads ou processamento de imagens falhando primeiro (streams consomem descriptors rapidamente)
  • erros no banco ou Redis que parecem não relacionados (novos sockets não abrem)
  • “funciona localmente” mas falha sob tráfego real ou jobs cron
  • um pod está “amaldiçoado” enquanto outros parecem ok

Reinícios podem esconder a causa raiz. Se sua plataforma reinicia processos caídos rapidamente, você pode entrar num loop: o vazamento cresce, o app morre, o app volta “saudável”, e o vazamento recomeça.

Autoscaling também pode tornar tudo aleatório. Novas instâncias começam com contagem limpa de descriptors, então os erros desaparecem quando o tráfego se desloca. Depois o mesmo caminho de código roda de novo, e só alguns pods falham.

Um último cheque: se o erro acontece imediatamente no startup com pouco tráfego, talvez você esteja simplesmente atingindo um limite baixo de descriptors. Se aparece depois de rodar e piora em rotas ou jobs específicos (uploads, scraping, geração de PDF), geralmente é vazamento de aplicação.

Causas comuns em apps Node gerados por IA

Projetos Node gerados por IA muitas vezes funcionam em demo, mas batem em EMFILE assim que tráfego real, arquivos reais, ou jobs de longa duração aparecem. O padrão subjacente é simples: algo abre um arquivo ou conexão e não fecha, então o processo vai ficando sem file descriptors.

Um gatilho comum é código que lida com arquivos usando streams mas não os fecha em todos os caminhos. Por exemplo, um handler abre um read stream e retorna cedo em erro sem chamar destroy() ou esperar por finish/close.

Outra causa frequente são watchers de background “úteis” criados por scaffolds. Um protótipo pode iniciar watchers de arquivo para hot reload, geração de thumbnails ou tarefas de sync, e acabar rodando isso em produção também. Cada watcher usa descriptors e pode multiplicar entre workers.

Os vazamentos que aparecem com mais frequência

Estes são os culpados recorrentes em código gerado por IA:

  • Streams abertos dentro de loops (imports CSV, processamento em lote de imagens) sem backpressure, então centenas de arquivos ficam abertos ao mesmo tempo.
  • Clientes HTTP ou sockets TCP mantidos abertos para sempre, especialmente quando retries criam conexões novas sem encerrar as antigas.
  • Uso de pool de banco onde conexões são adquiridas e não liberadas em caminhos de erro (falta finally).
  • Processos filhos spawnados para conversão ou scraping, com pipes de stdout/stderr deixados abertos.
  • Gravadores de logs ou métricas que abrem um arquivo por requisição, ou rodam rotação de logs incorreta mantendo handles antigos.

Por que código gerado por IA piora isso

Código gerado costuma ter muitos retornos antecipados e blocos catch, mas sem limpeza consistente. Mistura também padrões (callbacks, promises, streams) numa mesma função, o que facilita perder um caminho de saída.

Maneiras rápidas de confirmar que é um vazamento de FD (sem adivinhação)

Quando você vê EMFILE too many open files Node, responda uma pergunta: você está batendo num limite baixo ou seu app está vazando file descriptors ao longo do tempo?

Primeiro, verifique o limite atual para o processo em execução.

# In the same environment as the Node process
ulimit -n

# Per-process limits (replace PID)
cat /proc/PID/limits | grep -i "open files"

Em seguida, meça quantos FDs o processo Node tem abertos agora e verifique novamente depois. Um vazamento parece com um número que continua subindo mesmo quando o tráfego está estável.

# Count open file descriptors for the process
ls -1 /proc/PID/fd | wc -l

Se possível, amostre ou plote essa contagem. Você procura por uma subida contínua que não cai após as requisições terminarem.

Para ver o que está sendo deixado aberto, pegue um snapshot rápido do lsof e procure repetições.

# High-level view of what the process is holding
lsof -p PID | head

# Quick pattern check (examples: uploads, temp files, sockets)
lsof -p PID | grep -E "(/tmp|uploads|\.log|TCP)" | head

Alguns padrões comuns:

  • milhares de nomes de arquivos temporários semelhantes (uploads não fechados)
  • arquivos de log repetidos (logger customizado reabrindo)
  • muitos sockets de saída (clientes HTTP não fechando)

Passo a passo: isolar e parar o vazamento

Auditoria gratuita de código EMFILE
Envie seu repositório e identificaremos a origem do EMFILE antes de você corrigir às cegas.

Trate EMFILE como um problema de taxa: algo está abrindo file descriptors mais rápido do que fecha. O objetivo é provar qual processo e qual funcionalidade faz a contagem subir, então enviar a menor correção segura.

Comece pelo tempo. Correlacione quando os erros começam com picos de tráfego, jobs cron, workers ou tarefas em lote. Se só acontece durante um import noturno, você já tem um forte suspeito.

Depois olhe o que está realmente aberto. Um vazamento causado por uploads geralmente mostra muitos arquivos regulares. Um padrão ruim de cliente HTTP mostra muitos sockets em estados semelhantes. Alguns setups de logging deixam pipes abertos.

Um fluxo prático de isolar primeiro:

  • Identifique o PID que está lançando erros e observe a contagem de FDs abertos a cada 10 a 30 segundos.
  • Capture um snapshot lsof e escaneie os caminhos ou endpoints remotos mais repetidos.
  • Desabilite um worker, job ou feature flag de cada vez e observe se a curva de FDs para de subir.
  • Adicione contadores mínimos ao caminho suspeito (aberturas vs fechamentos por requisição/job) e registre apenas agregados.
  • Faça um deploy com a correção direcionada e confirme que a inclinação fica plana sob a mesma carga.

Para instrumentação temporária, mantenha simples e segura. Para uma rota de upload, conte quantos streams você cria e quantos emitem close ou end. Para um worker de fetch, registre quantas respostas você inicia versus quantas consome totalmente.

Se desabilitar um único worker para e a contagem para de subir em minutos, provavelmente você achou o vazamento. Se continuar subindo, talvez haja múltiplas fontes ou uma biblioteca compartilhada usada em todo lugar.

Armadilhas que mantêm o vazamento vivo

Problemas de EMFILE persistem quando a limpeza só acontece no caminho feliz.

Um file handle, socket de rede ou cursor é criado, depois ocorre uma exceção, e a etapa de fechamento não roda. Se você só testa quando tudo dá certo, perde o vazamento.

Os culpados usuais

Isso aparece muito em código gerado por IA porque ele copia padrões mas pula as partes seguras:

  • Sem try/finally ao redor de qualquer coisa que você abre, então erros pulam o close.
  • Streams sem handlers de error, onde o caminho de erro pula a limpeza.
  • Watchers de arquivo como fs.watch ou chokidar deixados em produção.
  • Um novo cliente de banco criado por requisição em vez de usar um pool.
  • Shutdown que ignora SIGTERM ou não aguarda a limpeza, então conexões antigas ficam durante deploys.

Um exemplo concreto de como isso pega

Imagine um endpoint de upload que lê um arquivo temporário, envia para storage e depois deleta o temporário. Sob timeout, o upload falha no meio. Se o código não fecha o read stream num finally, esse handle do arquivo temporário pode ficar aberto. Faça isso repetidamente e o servidor atinge o limite.

Um bom teste é forçar o caminho de falha de propósito (cancelar a requisição, simular timeout) e verificar se a contagem de FDs para de subir depois que a requisição termina.

Como ler as pistas do que ficou aberto

O caminho mais rápido é olhar o que está realmente aberto, não só o que você suspeita.

Amostre um processo algumas vezes, com cerca de um minuto entre amostras:

  • Se a contagem de FDs sobe de forma constante com pouco tráfego, você está caçando um vazamento.
  • Se ela sobe em saltos acentuados, procure uma tarefa agendada (cron), um worker de fila, ou um job de background que acorda, faz trabalho e esquece de fechar.

Padrões que apontam a origem

O que você vê no lsof normalmente estreita a causa rápido:

  • Muitas entradas socket: chamadas HTTP de saída, conexões com banco, clientes Redis, webhooks, proxies, ou timeouts faltando.
  • Muitas entradas pipe: processos filhos (ferramentas de PDF, conversão de imagem, ffmpeg) onde stdout/stderr não são esvaziados ou o processo não é reaped.
  • Muitos arquivos reais: uploads, arquivos temporários, logs, read streams onde close nunca dispara.
  • Muitos caminhos semelhantes repetidos: um loop abrindo o mesmo tipo de recurso várias vezes.

Depois de identificar o tipo dominante, correlate com o tempo. Se o pico bate com um tick da fila, foque no código do worker, não no handler web. Se sockets dominam, verifique pooling e timeouts. Se arquivos dominam, verifique parsing de uploads e qualquer uso de createReadStream ou createWriteStream.

Mitigações seguras enquanto você trabalha na correção

Estabilize workers e uploads
Auditamos uploads, jobs de PDF e workers de fila — os pontos onde EMFILE geralmente começa.

Normalmente você precisa de duas frentes: manter o serviço no ar e comprar tempo para achar o vazamento.

Aumentar o limite de arquivos abertos pode reduzir crashes, mas trate como um buffer temporário. Se o vazamento continua crescendo, um limite maior só adia a falha. Faça uma mudança por vez, registre comportamento antes/depois e alerte se o uso de FDs continuar subindo.

Mitigações de baixo risco:

  • Reiniciar rapidamente, mas garanta que os logs sobrevivam tempo suficiente para debugar.
  • Fazer a checagem de saúde falhar quando o uso de FDs cruzar um limiar para que a instância seja rotacionada.
  • Rate-limit no endpoint ou job que causa picos de FDs.
  • Desabilitar a feature com vazamento atrás de uma flag (uploads, processamento de imagens, geração de PDF) até a correção ser enviada.

Timeouts são outra proteção útil. Muitos apps gerados por IA os esquecem, então chamadas HTTP lentas, queries de banco ou consumidores podem se acumular e manter sockets abertos por mais tempo que o esperado. Defina defaults razoáveis e limite retries.

Também torne o shutdown previsível: pare de aceitar novas requisições, finalize o trabalho em andamento, feche agentes HTTP keep-alive, feche pools de DB e pare workers.

Um exemplo realista: o worker de upload que manteve arquivos abertos

Uma equipe enviou um worker de upload gerado por IA que aceitava rajadas de PDFs, extraía texto e salvava resultados. Funcionava em testes, mas em produção caía após uma hora ocupada com EMFILE.

O worker usava fs.createReadStream() para cada PDF e dava pipe para um parser. No caminho feliz, o stream terminava e o handle do arquivo fechava. Mas no caminho de erro (PDF corrompido, timeout, exceção no parser), o código retornava cedo e nunca limpava o stream. Pior, não ouvia erros do stream, então algumas falhas nunca chegavam ao bloco catch.

O que mudou no patch

A correção foi pequena mas rigorosa: todo fluxo precisava fechar handles mesmo quando algo dava errado.

  • Anexar handlers de error a cada stream envolvido.
  • Usar um controle de fluxo que garanta limpeza (por exemplo, um try/finally).
  • No cleanup, chamar destroy() em streams que podem ainda estar abertos.

Uma versão simplificada ficou assim:

const rs = fs.createReadStream(path);
try {
  await parsePdf(rs); // throws on bad PDFs
} finally {
  rs.destroy(); // safe even if already ended
}

A prova em produção de que foi corrigido

Eles monitoraram uma métrica: o número de file descriptors abertos pelo processo Node.

Antes do patch, a contagem de FDs subia a cada rajada e nunca voltava ao baseline. Depois do patch, ela subia brevemente durante picos de upload e então voltava ao normal. Essa foi a confirmação real de que o vazamento havia sido resolvido.

Checklist rápido para confirmar a correção em produção

Acabe com o ciclo de reinícios
Consertamos a causa real por trás de 500s aleatórios, como streams, sockets e watchers vazando.

Você não precisa adivinhar se consertou um EMFILE. Precisa de alguns cheques que se mantenham consistentes sob tráfego real.

Depois do deploy, mantenha o mesmo ambiente e formato de carga que usou quando estava falhando (mesmos workers, mesmos jobs de background, mesmos consumidores de fila):

  • Contagem de FDs se estabiliza ao invés de subir: pequenas variações são normais; uma subida constante não é.
  • Nenhum EMFILE durante um ciclo completo de tráfego: observe um período de pico e depois um período mais calmo.
  • Pools de conexão se estabilizam após picos: conexões ativas e requisições enfileiradas devem retornar ao baseline.
  • Shutdown gracioso realmente fecha as coisas: reinicie uma instância e confirme que o processo antigo sai limpo.
  • Um alerta simples de FD: alerte bem abaixo do limite do SO para que regressões apareçam cedo.

Se a contagem de FDs estiver estável mas os erros continuarem, verifique limites do SO (ulimit), sidecars ou outros processos no mesmo host.

Próximos passos se continuar acontecendo

Se você aumentou limites e enviou um patch mas EMFILE volta, assuma que há um problema estrutural. Dois padrões geralmente significam “cavar mais fundo”: código que abre arquivos em muitos pontos sem um dono claro, e loops de background escondidos (pollers, watchers, workers) que rodam para sempre e acumulam handles abertos.

O que coletar para alguém diagnosticar rápido

Antes de mudar mais código, capture um conjunto pequeno de evidências do ambiente com falha:

  • uma janela curta de logs ao redor do primeiro EMFILE (com timestamps e nível de tráfego)
  • PID, versão do Node, limites do container e configuração nofile atual
  • um snapshot de FDs abertos para o PID (contagem e tipos dominantes: arquivos, sockets, pipes)
  • detalhes do deploy recente e o que mudou
  • o formato da carga (uploads, processamento de imagem, jobs cron, webhooks, consumidores de fila)

Depois, reproduza com carga similar à produção por 10–15 minutos e veja se a contagem de FDs sobe constantemente. Uma subida contínua quase sempre indica um vazamento.

Se a base de código foi gerada por IA e você não consegue achar rapidamente o “dono” de cada stream/socket, uma auditoria focada costuma ser mais rápida que patchar às cegas. FixMyMess (fixmymess.ai) foi feito exatamente para essa situação: diagnosticar e reparar protótipos Node gerados por IA rastreando vazamentos, apertando caminhos de limpeza e endurecendo o app para que aguente em produção.

Perguntas Frequentes

O que “EMFILE: too many open files” realmente significa no Node?

Significa que seu processo Node atingiu o limite de file descriptors. File descriptors não são apenas “arquivos”; eles também cobrem sockets de rede, pipes, diretórios e streams, então a falha pode aparecer como erros no banco, chamadas HTTP quebradas ou 500s aleatórios.

Por que o EMFILE aparece depois de horas ou dias em vez de imediatamente?

Normalmente indica um vazamento: algo é aberto repetidamente e não é fechado em todos os caminhos, especialmente nos caminhos de erro. Se acontecer logo após o startup com pouco tráfego, pode ser que o limite de arquivos abertos esteja configurado baixo para o processo ou container.

Como posso saber se é um vazamento versus um limite do SO?

Verifique se o número de descriptors abertos continua subindo ao longo do tempo. Se a contagem sobe de forma constante mesmo com tráfego estável, você está vazando; se a contagem está estável mas ainda há erros, o limite provavelmente é baixo demais para sua carga de trabalho.

Qual é a maneira mais rápida de confirmar um vazamento de FDs em produção?

Monitore a contagem de FDs abertos para o PID do Node por alguns minutos e amostre novamente depois. Um vazamento parece com a contagem não retornando ao baseline depois que requisições ou jobs terminam, mesmo que suba durante picos.

Quais são as causas mais comuns de EMFILE em apps Node gerados por IA?

Os problemas mais comuns são streams que não são destruídos em erros, falta de finally ao abrir conexões/arquivos, e watchers de background rodando em produção por engano. Handlers de upload, processamento de PDF/imagem e workers de fila são pontos quentes porque abrem muitos handles rapidamente.

O que devo procurar na saída do lsof para encontrar a origem?

Olhe o que ficou aberto: muitos arquivos regulares apontam para uploads ou temporários; muitos sockets apontam para chamadas HTTP de saída, clientes de banco ou Redis; muitos pipes apontam para processos filhos. Associar o tipo dominante ao momento do pico normalmente reduz para uma rota ou worker específico.

Por que reinícios ou autoscaling fazem o EMFILE parecer aleatório?

Reinícios resetam a contagem de FDs, então o serviço parece “consertado” por um tempo mesmo com o vazamento persistindo. Autoscaling pode esconder também, porque novas instâncias começam limpas enquanto apenas alguns pods de longa duração acumulam handles vazados até falhar.

O que posso fazer para manter o serviço funcionando enquanto corrijo de verdade?

Pare a subida da contagem de FDs desativando o job ou rota suspeita, ou limitando temporariamente a taxa do hotspot. Aumentar o limite de arquivos abertos é um amortecedor temporário, não uma correção; o objetivo real é garantir que a contagem volte ao normal depois do trabalho.

Qual é o padrão de código mais seguro para evitar que o EMFILE volte?

Torne a limpeza inevitável: sempre que abrir um stream, socket ou conexão de pool, garanta que ele seja fechado em um finally ou caminho de limpeza garantido. Também trate erros de stream explicitamente, pois erros não tratados frequentemente pulam a limpeza que você esperava.

Como verifico que a correção é real após o deploy?

Acompanhe um sinal simples: a contagem de FDs abertos para o processo Node deve subir durante os picos e depois retornar perto do baseline, não subir sem parar. Se seu código foi gerado por IA e você não consegue identificar rapidamente o dono de cada stream/socket, FixMyMess pode rodar uma auditoria gratuita e normalmente corrigir e reforçar o app em 48–72 horas.