idempotência em automações: A laptop screen showing programming code and debugging tools, ideal for tech topics.

Idempotência em Automações: Como Fazer Retries Sem Duplicar Desastres

Idempotência em automações é uma daquelas expressões que parecem ter sido inventadas por alguém que queria transformar uma boa prática simples em um boleto intelectual. Mas, na prática, ela responde a uma pergunta muito concreta: se o seu script falhar no meio e rodar de novo, ele vai corrigir o problema ou criar outro maior?

Eu aprendi isso do jeito tradicional: quebrando algo pequeno, ficando confiante demais, depois quebrando algo que já tinha usuário do outro lado. Nada como um retry inocente para transformar “só vou tentar novamente” em “parabéns, você acabou de enviar três cobranças, dois e-mails iguais e abriu quatro tickets para a mesma pessoa”. A tecnologia moderna chama isso de resiliência. O financeiro chama de confusão. O usuário chama de incompetência com razão.

Neste artigo, vou mostrar como desenhar automações idempotentes usando arquivos de estado, chaves únicas, locks, logs úteis e alguns padrões simples em Python e shell. A meta não é fazer arquitetura de astronauta. É impedir que uma automação bem-intencionada vire uma máquina de duplicar perrengue.

idempotência em automações: Focused view of a computer screen displaying code and debug information.
Quando o retry entra sem idempotência, o terminal vira uma máquina de fabricar déjà vu técnico. Foto: Daniil Komov / Pexels.

O problema real: retries são necessários, mas perigosos

Todo sistema minimamente sério falha. API sai do ar, banco demora, DNS resolve errado, token expira, rede dá aquela micro-engasgada que ninguém consegue reproduzir depois. Por isso a gente coloca retry. Faz sentido: se a operação falhou por uma instabilidade temporária, tentar de novo economiza intervenção manual.

O problema é que retry sem idempotência é só teimosia automatizada. Ele repete a operação sem saber se a primeira tentativa falhou antes, durante ou depois do efeito colateral. E é justamente aí que mora a tragédia.

Imagine uma automação que processa pedidos pendentes:

for order in $(pending_orders); do
  charge_customer "$order"
  send_confirmation_email "$order"
  mark_as_processed "$order"
done

Bonito. Curto. Didático. Também perigoso. Se o script cobra o cliente, envia o e-mail e cai antes de marcar como processado, na próxima execução o pedido continua “pendente”. O robô, coitado, não tem culpa. Ele só vê um item pendente e obedece. Aí cobra de novo. A culpa é sua. Dói, mas é libertador aceitar cedo.

O que idempotência significa sem a espuma acadêmica

Uma operação idempotente pode ser executada uma vez ou várias vezes e deixar o sistema no mesmo estado final. Não quer dizer que ela não faça nada. Quer dizer que repetir não multiplica o efeito.

Um exemplo bobo:

mkdir -p /var/backups/automente

Rodar esse comando uma vez cria o diretório. Rodar dez vezes mantém o diretório. Não cria dez diretórios, não dá erro se ele já existe, não entra em crise existencial. Isso é idempotente.

Agora compare com:

echo "backup iniciado" >> /var/log/backup.log

Cada execução adiciona uma nova linha. Isso não é ruim por si só, mas não é idempotente. Se o seu objetivo é registrar cada tentativa, ótimo. Se o seu objetivo é garantir um único registro por job, você precisa de outra estratégia.

Primeira regra: se existe efeito colateral, precisa existir identidade

O erro mais comum em automações frágeis é tratar eventos como “coisas soltas”. Um e-mail a enviar. Um pagamento a cobrar. Um arquivo a transformar. Uma linha a importar. Só que qualquer operação que possa ser repetida precisa de uma identidade estável.

Essa identidade pode ser:

  • o ID do pedido;
  • um hash do arquivo;
  • uma chave composta, como cliente_id + mês + tipo_de_cobrança;
  • um UUID criado no início do fluxo e reaproveitado até o fim;
  • uma chave de idempotência enviada para uma API externa.

Sem identidade, você não tem como perguntar “isso já foi feito?”. E, sem essa pergunta, seu retry é uma roleta com cron.

Um padrão simples: tabela de estado antes da ação

Para automações pequenas, um SQLite local já resolve muita coisa. Antes de executar uma ação perigosa, registre a intenção com uma chave única. Depois, atualize o estado. O banco vira a memória operacional do script.

import sqlite3
from datetime import datetime

conn = sqlite3.connect("automation_state.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS jobs (
    key TEXT PRIMARY KEY,
    status TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    last_error TEXT
)
""")

def reserve_job(key):
    try:
        conn.execute(
            "INSERT INTO jobs (key, status, updated_at) VALUES (?, ?, ?)",
            (key, "running", datetime.utcnow().isoformat())
        )
        conn.commit()
        return True
    except sqlite3.IntegrityError:
        return False

def finish_job(key):
    conn.execute(
        "UPDATE jobs SET status = ?, updated_at = ? WHERE key = ?",
        ("done", datetime.utcnow().isoformat(), key)
    )
    conn.commit()

O fluxo fica assim:

job_key = f"invoice:{customer_id}:{month}"

if not reserve_job(job_key):
    print("Job já reservado ou processado. Pulando.")
    return

try:
    charge_customer(customer_id, month)
    finish_job(job_key)
except Exception as exc:
    mark_failed(job_key, str(exc))
    raise

Isso já elimina uma classe inteira de duplicações. Não é perfeito — nada é, exceto o bug em produção, que sempre encontra perfeição no timing — mas muda a conversa. Agora o script tem memória.

Mas cuidado: “running” eterno também é bug

Se o processo cai depois de reservar o job e antes de terminar, você pode ficar com um estado running preso para sempre. A solução é registrar timestamps e decidir quando uma execução antiga pode ser considerada abandonada.

SELECT key
FROM jobs
WHERE status = 'running'
  AND updated_at < datetime('now', '-30 minutes');

Aqui entra julgamento. Um job financeiro talvez não deva ser reexecutado automaticamente depois de 30 minutos. Um job de conversão de imagem pode. Um envio de e-mail talvez precise checar o provedor antes de reenviar. Automação adulta não é “tentar até funcionar”. É saber quando tentar, quando pular e quando chamar um humano.

Box perrengue: uma vez eu vi uma automação reenviar mensagens porque o sistema marcava como “enviado” só depois da confirmação final. A API aceitava o envio, mas a resposta demorava. O script dava timeout, tentava novamente e pronto: o usuário recebia a mesma notificação três vezes. O erro não estava no provedor. Estava no desenho do fluxo. Timeout não significa que nada aconteceu. Significa apenas que você não recebeu a resposta a tempo. Essa diferença custa caro.

Use chaves de idempotência quando a API permitir

Muitas APIs modernas aceitam uma chave de idempotência em operações sensíveis. Stripe popularizou bastante esse padrão, mas a ideia aparece em vários serviços: você envia um identificador único junto com a requisição, e o servidor garante que repetição com a mesma chave não gera um novo efeito.

curl -X POST https://api.exemplo.com/charges   -H "Authorization: Bearer $TOKEN"   -H "Idempotency-Key: invoice-123-2026-05"   -H "Content-Type: application/json"   --data '{"customer_id":123,"amount":9900}'

Se a API tiver esse recurso, use. Não tente ser mais esperto do que uma garantia transacional do lado de lá. Se ela não tiver, você precisa compensar do seu lado: consultar antes, registrar depois, usar chaves únicas e aceitar que algumas operações exigem reconciliação.

Locks: úteis, mas não confunda com idempotência

Lock impede duas execuções simultâneas. Idempotência impede que repetição cause efeito duplicado. São parentes, não gêmeos.

Um lock com flock resolve o caso clássico do cron que começa de novo antes do anterior terminar:

#!/usr/bin/env bash
set -euo pipefail

LOCK=/tmp/importacao-clientes.lock

flock -n "$LOCK" bash -c '
  echo "Rodando importação..."
  python3 importar_clientes.py
'

Isso é ótimo para evitar concorrência acidental. Mas se o script roda hoje, cai depois de enviar metade dos dados e roda amanhã de novo, o lock não te salva. Ele só garante que a tragédia aconteça uma por vez, com disciplina britânica.

Como debugar uma automação que já duplica coisas

Quando você descobre duplicação, a tentação é abrir o código e sair colocando if igual quem tapa vazamento com fita isolante. Respira. Primeiro reconstrua a linha do tempo.

  1. Qual item foi duplicado?
  2. Qual era a chave natural dele?
  3. Quantas execuções do job ocorreram no período?
  4. Houve timeout, deploy, reinício de container ou erro de rede?
  5. O efeito colateral aconteceu antes ou depois do registro de estado?
  6. Existe log com correlação entre tentativa, item e resultado?

Se você não consegue responder a essas perguntas, o próximo bug não será corrigido; será negociado com o acaso.

Logs bons não são romances. São recibos. Eles precisam registrar a chave do item, a tentativa, o estado anterior, a ação tomada e o resultado.

{
  "event": "job_attempt",
  "job_key": "invoice:123:2026-05",
  "attempt": 2,
  "previous_status": "running",
  "action": "skip_pending_reconciliation",
  "reason": "previous attempt timed out after external call"
}

O padrão “verifique antes, escreva depois” nem sempre basta

Um antipadrão sutil é fazer:

if not already_sent(email_id):
    send_email(email_id)
    mark_as_sent(email_id)

Parece correto. Mas duas execuções paralelas podem passar pelo already_sent ao mesmo tempo, ambas verem “não enviado” e ambas enviarem. Esse é o famoso buraco entre a leitura e a escrita. Pequeno no código, enorme no estrago.

Prefira uma escrita atômica antes da ação, com restrição única:

CREATE TABLE sent_emails (
  email_key TEXT PRIMARY KEY,
  status TEXT NOT NULL,
  created_at TEXT NOT NULL
);
try:
    reserve_email(email_key)  # INSERT com chave única
except DuplicateKey:
    return "já reservado"

send_email(email_key)
mark_email_done(email_key)

Você troca “olhar e depois decidir” por “reservar ou sair”. Essa diferença é o que separa automação confiável de script adolescente com acesso ao cartão corporativo.

idempotência em automações: Detailed view of computer code highlighting syntax in colors on a screen.
O bug raramente está só na linha vermelha. Ele mora no contrato entre tentativas, estado e efeito colateral. Foto: Godfrey Atima / Pexels.

Checklist prático para automações idempotentes

Antes de colocar um job recorrente em produção, eu gosto de passar por esta lista. Ela é simples, meio chata e salva tardes inteiras. Portanto, naturalmente, quase ninguém usa até sangrar.

  • A operação tem uma chave única? Se não tem, invente uma baseada no domínio.
  • Existe registro de estado? Arquivo, SQLite, banco, fila, tanto faz; precisa existir.
  • A reserva é atômica? Duas execuções simultâneas não podem reservar o mesmo item.
  • O efeito colateral externo aceita chave de idempotência? Se aceita, use sempre.
  • O retry diferencia erro antes e depois da ação? Timeout não é prova de falha.
  • Existe reconciliação? Para casos ambíguos, consulte a fonte externa antes de repetir.
  • Os logs têm correlação? Sem job_key, debugging vira espiritismo com grep.
  • O job lida com estado preso? running eterno é cemitério de automação.

Um mini-exemplo completo em Python

Abaixo vai uma versão reduzida de um processador idempotente. Ele não depende de framework e já mostra a estrutura mental: reservar, executar, finalizar, registrar erro.

import sqlite3
import time
from datetime import datetime

conn = sqlite3.connect("jobs.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS processed_items (
    item_key TEXT PRIMARY KEY,
    status TEXT NOT NULL,
    attempts INTEGER NOT NULL DEFAULT 0,
    updated_at TEXT NOT NULL,
    last_error TEXT
)
""")
conn.commit()

def now():
    return datetime.utcnow().isoformat()

def reserve(item_key):
    try:
        conn.execute(
            "INSERT INTO processed_items (item_key, status, attempts, updated_at) VALUES (?, 'running', 1, ?)",
            (item_key, now())
        )
        conn.commit()
        return True
    except sqlite3.IntegrityError:
        row = conn.execute(
            "SELECT status FROM processed_items WHERE item_key = ?",
            (item_key,)
        ).fetchone()
        return row and row[0] == "failed"

def mark_done(item_key):
    conn.execute(
        "UPDATE processed_items SET status='done', updated_at=?, last_error=NULL WHERE item_key=?",
        (now(), item_key)
    )
    conn.commit()

def mark_failed(item_key, error):
    conn.execute(
        "UPDATE processed_items SET status='failed', attempts=attempts+1, updated_at=?, last_error=? WHERE item_key=?",
        (now(), str(error), item_key)
    )
    conn.commit()

def external_side_effect(item):
    # Aqui entraria API, e-mail, pagamento, deploy, importação etc.
    print(f"Processando {item['key']}")
    time.sleep(1)

def process(item):
    key = item["key"]
    if not reserve(key):
        print(f"Pulando {key}: já processado ou reservado")
        return
    try:
        external_side_effect(item)
        mark_done(key)
    except Exception as exc:
        mark_failed(key, exc)
        raise

Esse código ainda pode evoluir: backoff exponencial, reconciliação, métrica, lock distribuído, fila real. Mas ele já nasce com uma propriedade essencial: repetir a execução não significa repetir cegamente o efeito.

Links internos para continuar o raciocínio

Se você está montando esse tipo de automação, vale conectar este tema com outros problemas que já apareceram aqui no AutoMente. O artigo sobre observabilidade de logs para debugar produção ajuda a enxergar o que aconteceu quando o retry vira suspeito. O texto sobre automação de fluxo de trabalho com scripts de prioridade mostra como decidir ordem de execução sem depender de memória humana. E, se o seu problema envolve ambiente Linux, a auditoria automatizada de segurança com Lynis e Bash é um bom lembrete de que automação sem verificação é só velocidade na direção errada.

Também recomendo navegar pela categoria Log de Erros, porque ela existe justamente para falar da parte menos glamourosa e mais útil da tecnologia: entender por que as coisas quebram sem fingir que o stack trace é poesia.

Conclusão: automação boa sabe repetir sem piorar

O ponto central é simples: toda automação que pode falhar precisa saber o que acontece quando roda de novo. Se ela não sabe, você não tem resiliência; tem amnésia em loop.

Idempotência em automações não é luxo de sistema grande. É uma proteção básica para qualquer script que conversa com API, mexe em dados, envia mensagens, cobra clientes, altera infraestrutura ou dispara processos que alguém do outro lado vai sentir. Quanto mais silenciosa a automação, mais importante é ela ser previsível.

Minha regra pessoal é cruel, mas funciona: se eu ficaria com medo de apertar “rodar de novo”, o script ainda não está pronto. E se a única coisa impedindo uma duplicação é “normalmente não acontece”, então já aconteceu; só falta você descobrir.

CTA: qual automação você quer ver desmontada aqui no AutoMente? Backup, deploy, cobrança, scraping, triagem de e-mails, monitoramento, fila de jobs? Manda o perrengue. Se tiver cara de problema real, eu transformo em artigo — com código, cicatriz e pouca paciência para solução enfeitada.

Posts Similares