Atomic File Writes no Node.js evitam que leitores encontrem arquivos parcialmente gravados. O padrão escreve o conteúdo em um arquivo temporário no mesmo diretório, sincroniza os dados quando necessário e substitui o destino com rename.
Uma chamada direta a writeFile pode truncar o arquivo antes de terminar. Se o processo cair, a configuração, índice ou checkpoint pode ficar vazio ou incompleto.
Padrão básico
import { open, rename, rm } from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
async function writeAtomic(filePath, data) {
const directory = path.dirname(filePath);
const temporary = path.join(
directory,
`.${path.basename(filePath)}.${crypto.randomUUID()}.tmp`
);
let handle;
try {
handle = await open(temporary, 'wx', 0o600);
await handle.writeFile(data);
await handle.sync();
await handle.close();
handle = null;
await rename(temporary, filePath);
} finally {
await handle?.close().catch(() => {});
await rm(temporary, { force: true }).catch(() => {});
}
}O modo wx evita sobrescrever um temporário existente. Um nome aleatório reduz colisão.
Mesmo filesystem
O rename atômico depende de origem e destino estarem no mesmo filesystem. Criar o temporário em /tmp e mover para outro volume pode gerar EXDEV e exigir cópia, que não é atômica.
Atomicidade não é durabilidade
Rename atômico significa que leitores veem o arquivo antigo ou novo, não metade. Isso não garante que dados sobreviverão a perda de energia. Para maior durabilidade, sincronize arquivo e, em cenários exigentes, o diretório pai.
Sincronizando o diretório
const dirHandle = await open(directory, 'r');
try {
await dirHandle.sync();
} finally {
await dirHandle.close();
}O suporte e comportamento variam por plataforma. Teste no ambiente real.
Permissões
Crie o temporário com permissões restritas. Depois do rename, ele mantém seu modo. Se o destino precisa de outro modo, aplique explicitamente antes da publicação.
Preservando metadados
Substituir o arquivo pode alterar owner, mode, ACLs e atributos. Quando isso importa, leia e reaplique metadados de forma controlada. Não copie permissões inseguras automaticamente.
JSON
const payload = JSON.stringify(value, null, 2) + '\n';
await writeAtomic(configPath, payload);Valide o objeto antes de gravar. Depois, opcionalmente releia e faça parse para detectar erro de serialização ou filesystem.
Checkpoints
Checkpoints devem incluir versão e checksum:
{
"version": 1,
"sequence": 1500,
"updatedAt": "2026-07-26T12:00:00Z",
"checksum": "sha256:..."
}Na inicialização, rejeite versão desconhecida ou checksum inválido.
Concorrência
Atomic write impede arquivo parcial, mas não evita lost update. Dois processos podem ler a mesma versão e publicar resultados diferentes. Use lock, compare versão ou mantenha a verdade em um banco.
if (current.version !== expectedVersion) {
throw new Error('Conflito de versão');
}Leitores
Leitores devem abrir o caminho final normalmente. Um descritor já aberto pode continuar apontando para o arquivo antigo após rename. Reabra quando precisar da nova versão.
Windows
Semântica de rename e arquivos abertos difere do Unix. Antivírus e processos podem bloquear substituição. Teste retries limitados para erros transitórios, sem loop infinito.
Backup
Para arquivos importantes, mantenha uma versão anterior:
- grave e sincronize o temporário;
- mova o destino atual para backup;
- mova o temporário para destino;
- sincronize diretório;
- remova backups antigos conforme retenção.
Mais etapas significam mais estados de recuperação. Documente o algoritmo de startup.
Recuperação de temporários
No startup, procure temporários antigos. Não publique automaticamente sem validar. Remova depois de uma idade segura ou encaminhe para quarentena.
Streaming
Para dados grandes, escreva em stream no temporário, aguarde pipeline, sincronize e renomeie:
await pipeline(source, createWriteStream(temporary, {
flags: 'wx',
mode: 0o600
}));Garanta que o descritor foi fechado antes do rename quando a plataforma exigir.
Cancelamento
Se a operação for cancelada, destrua o stream e remova o temporário. Não toque no arquivo final até o conteúdo estar completo.
Disk full
ENOSPC pode ocorrer durante a escrita ou sync. Mantenha o destino antigo intacto e alerte. Monitore espaço e inodes.
Configuração observada
Watchers podem receber eventos do temporário e do rename. Ignore o padrão *.tmp e processe somente o caminho final. Consulte fs.watch no Node.js.
Segurança de caminho
O caminho deve vir de configuração confiável. Resolva a raiz, bloqueie traversal e avalie symlinks. Um atacante não deve redirecionar o rename para arquivo sensível.
Observabilidade
Monitore duração, bytes, falhas por código, sync, conflitos, temporários removidos, espaço disponível e idade do último checkpoint.
Testes
- escrita bem-sucedida;
- queda simulada antes do rename;
- erro durante write;
- disk full;
- dois escritores;
- destino aberto por leitor;
- temporário antigo;
- filesystem diferente;
- permissões;
- Windows e Linux.
Erros comuns
- temporário em outro volume;
- confundir atomicidade com durabilidade;
- não fechar antes do rename;
- temporário com permissão ampla;
- não limpar falhas;
- ignorar concorrência;
- não testar Windows;
- publicar temporário sem validação;
- usar caminho controlado pelo usuário.
Conclusão
Atomic File Writes no Node.js protegem leitores contra conteúdo parcial usando temporário e rename. O padrão é adequado para configurações geradas, checkpoints e pequenos índices locais.
Use o mesmo filesystem, sincronize conforme o requisito de durabilidade e trate concorrência separadamente. Mantenha o arquivo antigo intacto até a nova versão estar completa e validada.
Consulte a documentação de fsPromises.rename e a documentação de FileHandle.sync.




