Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Child Process no Node.js: Guia Prático

Atualizado em: 30 de julho de 2026

Terminal Linux usado em desenvolvimento e administração de aplicações Node.js

Aplicações Node.js nem sempre executam todo o trabalho dentro do próprio processo. Em muitos projetos, é necessário chamar uma ferramenta de linha de comando, converter arquivos, executar um script, iniciar outro programa ou separar uma tarefa em um processo independente. O módulo Child Process no Node.js oferece as APIs nativas para realizar essas operações.

Processos filhos possuem memória e ciclo de vida próprios. Essa separação aumenta o isolamento, mas também exige controle de argumentos, streams, sinais, tempo limite e encerramento. Um comando construído com entrada não confiável pode criar uma vulnerabilidade de execução remota, enquanto um processo sem timeout pode permanecer ativo indefinidamente.

Neste guia, você aprenderá as diferenças entre exec(), execFile(), spawn() e fork(), como consumir saída progressivamente, enviar mensagens por IPC, cancelar processos, evitar injeção de comandos e testar integrações com programas externos.

O que é um processo filho?

Um processo filho é um novo processo criado pelo processo Node.js principal. Ele possui identificador próprio no sistema operacional, espaço de memória separado e canais de entrada e saída. Dependendo da API usada, o processo pode executar um comando do shell, um arquivo binário ou outro módulo Node.js.

O módulo é carregado sem dependências externas:

const { spawn, exec, execFile, fork } = require('node:child_process');

A documentação oficial de child_process descreve todas as opções, eventos e versões baseadas em callbacks. Para revisar a plataforma, consulte o que é Node.js e o que é JavaScript.

Quando usar child_process?

O módulo é adequado para executar ferramentas já disponíveis no sistema, como conversores, compiladores, utilitários de imagem, clientes de banco, scripts administrativos e programas escritos em outra linguagem. Ele também pode isolar tarefas que precisam de permissões, variáveis ou dependências diferentes.

Não use um processo externo apenas para evitar uma função JavaScript simples. Criar processos possui custo de inicialização, memória e comunicação. Para cálculos intensivos dentro do ecossistema JavaScript, avalie Worker Threads no Node.js. Para trabalho durável e distribuído, uma fila pode ser mais apropriada.

Executando um comando com exec()

exec() abre um shell e executa uma string completa. A saída é armazenada em memória e entregue ao callback quando o comando termina:

const { exec } = require('node:child_process');

exec('node --version', (error, stdout, stderr) => {
  if (error) {
    console.error('Falha:', error.message);
    return;
  }

  if (stderr) {
    console.error('Saída de erro:', stderr);
  }

  console.log('Versão:', stdout.trim());
});

Essa API é conveniente para comandos curtos e saída pequena. Como utiliza um shell, operadores como redirecionamento, pipes e expansão de variáveis podem funcionar. A mesma característica aumenta o risco de injeção quando partes da string vêm do usuário.

Promisificando exec()

Com util.promisify(), a chamada pode ser usada com async e await:

const util = require('node:util');
const { exec } = require('node:child_process');

const execAsync = util.promisify(exec);

async function getVersion() {
  const { stdout } = await execAsync('node --version', {
    timeout: 5000
  });

  return stdout.trim();
}

Defina um timeout para impedir que o comando fique aberto indefinidamente. Também controle o tamanho máximo do buffer quando a saída puder crescer.

Por que exec() pode ser perigoso?

Considere um endpoint que concatena o nome de um arquivo:

exec(`convert ${req.body.file} output.png`);

Se o valor contiver caracteres interpretados pelo shell, um atacante pode acrescentar outro comando. Escapar corretamente para diferentes shells e sistemas operacionais é complexo. A defesa preferencial é não construir comandos com strings não confiáveis.

Use execFile() ou spawn() com argumentos separados. Valide também caminhos, extensões, tamanho e permissões. O guia sobre segurança em aplicações web apresenta cuidados complementares.

Executando um programa com execFile()

execFile() executa diretamente um arquivo, sem abrir shell por padrão:

const { execFile } = require('node:child_process');

execFile(
  process.execPath,
  ['--version'],
  { timeout: 5000 },
  (error, stdout, stderr) => {
    if (error) {
      console.error(error);
      return;
    }

    console.log(stdout.trim());
  }
);

Os argumentos são elementos separados do array. Isso reduz a possibilidade de interpretação acidental pelo shell. Ainda é necessário validar cada valor, pois o programa chamado pode aceitar opções perigosas ou acessar caminhos indevidos.

Usando spawn() para saída progressiva

spawn() retorna imediatamente um objeto ChildProcess. As saídas padrão são streams, permitindo processar dados enquanto o programa executa:

const { spawn } = require('node:child_process');

const child = spawn(process.execPath, ['script.js'], {
  stdio: ['ignore', 'pipe', 'pipe']
});

child.stdout.on('data', chunk => {
  process.stdout.write(`FILHO: ${chunk}`);
});

child.stderr.on('data', chunk => {
  process.stderr.write(`ERRO: ${chunk}`);
});

child.on('close', code => {
  console.log('Processo finalizado:', code);
});

Essa API é adequada para processos longos, logs contínuos e grandes volumes de saída. Como os dados chegam em chunks, uma linha pode ser dividida entre dois eventos. Use um parser que mantenha o restante incompleto entre leituras.

Diferença entre exit e close

O evento exit indica que o processo terminou. O evento close acontece depois que os canais de entrada e saída foram fechados. Quando você precisa garantir que toda a saída foi consumida, normalmente aguarde close.

child.on('exit', (code, signal) => {
  console.log('Saiu:', { code, signal });
});

child.on('close', code => {
  console.log('Streams fechadas:', code);
});

Também registre o evento error, emitido quando o processo não consegue ser criado ou sofre uma falha de inicialização.

Enviando dados pela entrada padrão

O objeto stdin é uma Writable Stream:

const child = spawn('some-program', ['--json']);

child.stdin.write(JSON.stringify({ task: 'resize' }));
child.stdin.end();

Ao escrever grandes volumes, respeite backpressure. O artigo sobre Streams no Node.js explica write(), drain e pipelines.

Controlando stdio

A opção stdio define como os descritores serão conectados:

  • pipe cria uma stream acessível pelo processo principal;
  • inherit compartilha o terminal atual;
  • ignore descarta o canal;
  • um stream ou descritor existente pode ser redirecionado;
  • ipc cria um canal de mensagens entre processos Node.js.
spawn('npm', ['test'], {
  stdio: 'inherit',
  shell: false
});

Com inherit, a saída aparece diretamente no terminal, mas não pode ser capturada da mesma maneira pelo processo pai.

fork() para outro módulo Node.js

fork() é uma variação de spawn() preparada para iniciar outro módulo Node.js com canal IPC:

const { fork } = require('node:child_process');

const worker = fork('./worker-process.js');

worker.send({
  type: 'calculate',
  taskId: 'task-1',
  payload: [1, 2, 3]
});

worker.on('message', message => {
  console.log('Resposta:', message);
});

No arquivo filho:

process.on('message', message => {
  if (message.type !== 'calculate') return;

  const result = message.payload.reduce(
    (total, value) => total + value,
    0
  );

  process.send({
    taskId: message.taskId,
    result
  });
});

As mensagens são serializadas. Objetos enormes aumentam o custo de memória e CPU. Para buffers e compartilhamento eficiente dentro do mesmo processo, Worker Threads podem ser melhores.

Tratando códigos de saída

Por convenção, código zero representa sucesso e valores diferentes indicam falha. Entretanto, cada programa define seus próprios códigos. Consulte a documentação da ferramenta e não trate toda saída em stderr como falha, pois alguns programas escrevem avisos nesse canal mesmo quando concluem corretamente.

child.on('close', code => {
  if (code === 0) {
    resolve();
  } else {
    reject(new Error(`Processo terminou com código ${code}`));
  }
});

Timeout e cancelamento

Algumas APIs aceitam signal. Um AbortController permite cancelar o processo:

const controller = new AbortController();

const child = spawn(process.execPath, ['long-task.js'], {
  signal: controller.signal
});

setTimeout(() => {
  controller.abort();
}, 10000);

O artigo sobre AbortController no Node.js detalha timeouts, sinais compostos e motivos de cancelamento.

Ao cancelar, confirme se o programa criou arquivos parciais, processos descendentes ou recursos externos. Encerrar o processo direto não desfaz automaticamente esses efeitos.

Encerrando com kill()

child.kill() envia um sinal ao processo:

if (!child.killed) {
  child.kill('SIGTERM');
}

No Unix, SIGTERM solicita encerramento e permite limpeza. Se o processo não responde dentro de uma janela, uma política pode usar SIGKILL. O comportamento varia no Windows, e encerrar uma árvore completa de processos pode exigir estratégia específica.

Variáveis de ambiente

Quando a opção env é informada, inclua as variáveis que o programa realmente precisa:

const child = spawn(process.execPath, ['script.js'], {
  env: {
    NODE_ENV: 'production',
    PATH: process.env.PATH
  }
});

Não envie todos os segredos do processo pai por conveniência. Tokens, credenciais de banco e chaves podem ficar disponíveis para programas que não precisam deles.

Diretório de trabalho e caminhos

A opção cwd altera o diretório de trabalho do processo:

spawn('git', ['status', '--short'], {
  cwd: '/srv/project'
});

Use caminhos absolutos controlados pela aplicação. Não aceite um diretório fornecido diretamente pelo cliente sem validação, pois o processo pode acessar arquivos fora da área permitida.

Limitando saída em memória

exec() e execFile() acumulam a saída. Se o programa gerar mais dados que o limite, a operação falha. Para saída potencialmente grande, prefira spawn() e encaminhe os chunks para um arquivo, parser ou stream de destino.

Evite armazenar logs completos de processos externos sem política de tamanho e retenção. Uma ferramenta pode produzir megabytes por segundo durante uma falha.

Processos filhos em APIs

Não permita que cada requisição crie processos ilimitados. Defina concorrência máxima, fila curta, timeout e validação rígida. Quando a capacidade estiver esgotada, responda com 429 ou 503 em vez de sobrecarregar o servidor.

Para endpoints de longa duração, pode ser melhor criar uma tarefa assíncrona e devolver um identificador. O cliente consulta o status depois, enquanto um worker limitado processa a fila.

Observabilidade

Registre programa, argumentos não sensíveis, duração, código de saída, sinal de encerramento, bytes de saída e motivo do cancelamento. Nunca grave tokens, senhas ou payloads confidenciais completos.

Monitore quantidade de processos ativos, tempo na fila, falhas de inicialização, timeouts e memória total. Integre as operações aos traces descritos no guia de OpenTelemetry no Node.js.

Como testar

Separe a função que monta os argumentos da função que inicia o processo. Assim, regras de validação podem ser testadas sem executar um programa real. Nos testes de integração, use um script controlado que produza saída, erro, atraso e códigos de saída diferentes.

Inclua cenários com executável inexistente, timeout, saída grande, cancelamento, argumentos com espaços, processo que ignora SIGTERM e mensagens IPC inválidas.

Erros comuns

  • Concatenar entrada em exec(): cria risco de injeção de comandos.
  • Não registrar error: falhas de criação ficam sem tratamento.
  • Usar exec para saída enorme: o buffer em memória atinge o limite.
  • Não definir timeout: processos travados permanecem ativos.
  • Criar um processo por requisição sem limite: CPU e memória se esgotam.
  • Enviar todos os segredos no env: aumenta a superfície de exposição.
  • Confundir stderr com falha: alguns programas escrevem avisos nesse canal.
  • Ignorar arquivos parciais: o cancelamento deixa resultados incompletos.

Boas práticas para produção

  • Prefira execFile() ou spawn() com argumentos separados.
  • Mantenha shell: false quando não houver necessidade.
  • Valide programa, argumentos, caminhos e extensões.
  • Defina timeout, limite de concorrência e tamanho de saída.
  • Trate error, exit e close.
  • Use streams para grandes volumes.
  • Envie apenas variáveis de ambiente necessárias.
  • Implemente encerramento gradual e limpeza.
  • Monitore processos ativos e códigos de saída.
  • Teste em todos os sistemas operacionais suportados.

Conclusão

O módulo Child Process no Node.js conecta aplicações JavaScript a programas externos e outros processos Node.js. exec() é conveniente para comandos pequenos, execFile() reduz a dependência do shell, spawn() oferece streams progressivas e fork() facilita comunicação IPC.

Use essa capacidade com limites claros. Argumentos separados, validação, timeout, concorrência controlada e tratamento de sinais são essenciais para evitar vulnerabilidades e processos abandonados. Com essas práticas, ferramentas externas podem fazer parte da arquitetura sem comprometer a estabilidade do servidor.

Os 10 Melhores Cursos de Programação de 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita