O módulo node:child_process permite iniciar programas externos e outros processos Node.js. Ele é usado para executar ferramentas de linha de comando, integrar binários, separar tarefas, criar workers baseados em processo e automatizar operações do sistema.
Processos filhos possuem memória isolada e falham separadamente da aplicação principal. Em contrapartida, consomem mais recursos que Worker Threads e exigem cuidado especial com argumentos, sinais, streams e segurança.
Quatro APIs principais
spawn: inicia um comando e trabalha com streams;exec: executa por meio de shell e acumula a saída;execFile: executa um arquivo diretamente e acumula a saída;fork: inicia outro processo Node.js com canal de IPC.
A escolha correta evita vulnerabilidades e problemas de memória.
spawn para saída contínua
import { spawn } from 'node:child_process';
const child = spawn('node', ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'],
});
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk) => {
process.stdout.write(chunk);
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => {
process.stderr.write(chunk);
});
child.on('error', (error) => {
console.error('Falha ao iniciar', error);
});
child.on('close', (code, signal) => {
console.log({ code, signal });
});spawn é apropriado para saída grande, processos longos e pipelines, pois não precisa armazenar todo o conteúdo em memória.
exec e o risco do shell
import { exec } from 'node:child_process';
exec('node --version', (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
console.log(stdout);
});exec interpreta a string em um shell. Nunca concatene entrada do usuário:
// Inseguro
exec(`convert ${userFilename} output.png`);Caracteres especiais podem executar comandos adicionais. Prefira spawn ou execFile com array de argumentos.
execFile
import { execFile } from 'node:child_process';
execFile('node', ['--version'], (error, stdout) => {
if (error) throw error;
console.log(stdout);
});Como o arquivo é executado diretamente por padrão, há menos exposição a expansão de shell. Ainda assim, valide caminhos, permissões e argumentos aceitos pelo programa.
Promisificando execFile
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const { stdout, stderr } = await execFileAsync('node', ['--version'], {
timeout: 5000,
maxBuffer: 1024 * 1024,
});maxBuffer limita a saída acumulada. Para volumes imprevisíveis, use spawn.
fork e IPC
fork é especializado em processos Node.js e cria um canal para mensagens:
import { fork } from 'node:child_process';
const child = fork(new URL('./worker.js', import.meta.url), [], {
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
});
child.on('message', (message) => {
console.log('Resposta', message);
});
child.send({ type: 'calculate', payload: 100 });No filho:
process.on('message', (message) => {
if (message.type === 'calculate') {
process.send?.({ result: message.payload * 2 });
}
});Mensagens são serializadas. Não envie objetos enormes nem dependa de referências compartilhadas.
Tratando eventos
Eventos importantes:
spawn: processo iniciado;error: falha ao iniciar ou enviar sinal;exit: processo terminou;close: streams foram fechados;disconnect: canal IPC fechado;message: mensagem recebida.
Trate error e close. Um comando inexistente pode emitir erro sem o fluxo esperado de saída.
Exit code
Código zero normalmente indica sucesso, mas o contrato depende do programa. Registre código, sinal, duração e uma parte limitada do stderr.
function run(command, args) {
return new Promise((resolve, reject) => {
const child = spawn(command, args);
let stderr = '';
child.stderr.on('data', (chunk) => {
if (stderr.length < 16_000) stderr += chunk;
});
child.on('error', reject);
child.on('close', (code, signal) => {
if (code === 0) resolve();
else reject(new Error(`Falha code=${code} signal=${signal}: ${stderr}`));
});
});
}Timeout e AbortSignal
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const child = spawn('programa', ['--modo', 'batch'], {
signal: controller.signal,
});
await waitForClose(child);
} finally {
clearTimeout(timer);
}Confirme como o programa reage ao sinal. Alguns processos criam descendentes que continuam executando.
Sinais e encerramento
child.kill('SIGTERM');Enviar um sinal não garante encerramento imediato. Aplique um prazo e, se necessário, escalone para uma finalização mais forte. No Windows, sinais e grupos de processos possuem diferenças.
Processos descendentes
Quando um comando inicia outros processos, encerrar apenas o pai pode deixar órfãos. Evite shell:true sem necessidade, entenda a árvore criada e use ferramentas específicas para gerenciamento de grupos quando a aplicação for multiplataforma.
stdio
Opções comuns:
pipe: cria stream;inherit: compartilha o terminal;ignore: descarta;ipc: canal de mensagens.
spawn('npm', ['test'], {
stdio: 'inherit',
});Em servidores, herdar stdout pode gerar logs sem estrutura. Pipe permite limitar, transformar e correlacionar.
Backpressure
Se você escreve no stdin do filho, respeite o retorno de write:
if (!child.stdin.write(chunk)) {
await new Promise((resolve) => child.stdin.once('drain', resolve));
}
child.stdin.end();Ignorar backpressure pode aumentar memória.
Pipeline
import { pipeline } from 'node:stream/promises';
const gzip = spawn('gzip', ['-c']);
await pipeline(inputStream, gzip.stdin);
await pipeline(gzip.stdout, outputStream);Trate também o exit code do processo. Um pipeline concluído não garante que o binário tenha finalizado com sucesso.
Ambiente e diretório
spawn('node', ['script.js'], {
cwd: '/app/jobs',
env: {
...process.env,
NODE_ENV: 'production',
JOB_ID: jobId,
},
});Não passe segredos desnecessários. Variáveis podem ficar visíveis a ferramentas do sistema e dumps. Use um ambiente mínimo quando possível.
PATH e executáveis
Ambientes de produção, containers e serviços systemd podem ter PATH diferente do terminal. Use imagem previsível, valide dependências no startup ou configure caminhos absolutos controlados.
Shell true
shell:true é útil para sintaxe de shell, mas amplia riscos de injeção e diferenças entre plataformas. Evite quando puder representar o comando e os argumentos diretamente.
Pool de processos
Para tarefas frequentes, criar processos repetidamente custa memória e startup. Mantenha um pequeno pool com fork ou use uma fila externa. Implemente limite de tarefas, reinício após falha e reciclagem para controlar vazamentos.
Observabilidade
Registre:
- nome permitido do comando;
- duração;
- exit code e sinal;
- timeout;
- bytes de stdout e stderr;
- fila e concorrência;
- reinicializações.
Não registre argumentos que contenham tokens ou dados pessoais.
Containers
O processo Node.js deve encaminhar sinais e colher filhos encerrados. Use um init leve quando necessário e teste shutdown no container. Limites de CPU e memória se aplicam à soma dos processos.
Worker Threads ou child_process
Use Worker Threads quando deseja paralelismo de CPU com menor custo e possível transferência de memória. Use processos quando precisa de isolamento forte, outro executável, ambiente separado ou proteção contra falhas nativas.
Erros comuns
- concatenar entrada do usuário em exec;
- usar exec para saída grande;
- não tratar error e close;
- ignorar exit code;
- não definir timeout;
- deixar processos órfãos;
- passar todo process.env;
- não limitar concorrência;
- ignorar backpressure;
- assumir comportamento idêntico entre sistemas.
Fluxo recomendado
Prefira spawn ou execFile, use arrays de argumentos, limite saída e tempo, trate sinais e valide o resultado. Para CPU em JavaScript, compare com Worker Threads. Para streams, aplique perf_hooks e métricas antes de otimizar.
Consulte a documentação oficial de child_process e a referência oficial de spawn.



