Idempotent operations
An operation is idempotent if running it twice produces the same outcome as running it once. Sending an email is *not* idempotent — run it twice and the customer gets two emails. Setting a flag to True *is* — it's already true the second time.
This matters because automation gets re-triggered. A cron job double-fires, a worker crashes mid-run and restarts, someone clicks the button twice. If your script isn't safe to re-run, every one of those becomes a duplicate charge, a doubled notification, or corrupted state.
The pattern is a state guard: before doing the work, check whether it's already been done.
- Keep a record of completed work — a seen-set of processed IDs, usually persisted to a file or database.
- Before acting, ask
is_done(task_id). If yes, skip. - If no, do the work *and then* record the ID so the next run skips it.
The order is load-amount: do the work first, *then* mark it done. If you mark it done before the work succeeds and the work crashes, you've recorded a lie.
Syntax
# Guard pattern
if is_done(task_id):
return # already handled — skip
result = do_the_work()
mark_done(task_id) # record only after success
# seen-set persisted as a JSON list
done = json.load(f) # e.g. ['job-1', 'job-2']
return task_id in done # the guard checkWorked examples
A seen-set guard, persisted to disk
import json
def is_done(state_file, task_id):
try:
with open(state_file) as f:
return task_id in json.load(f)
except FileNotFoundError:
return False # nothing recorded yet
def mark_done(state_file, task_id):
try:
with open(state_file) as f:
done = json.load(f)
except FileNotFoundError:
done = []
if task_id not in done: # no duplicates in the set
done.append(task_id)
with open(state_file, 'w') as f:
json.dump(done, f)
print(is_done('/tmp/state.json', 'job-1')) # False
mark_done('/tmp/state.json', 'job-1')
print(is_done('/tmp/state.json', 'job-1')) # True
# Output:
# False
# Truerun_once: do it the first time, skip the rest
import json
def _done(f):
try:
with open(f) as fh: return json.load(fh)
except FileNotFoundError: return []
def run_once(state_file, task_id, fn):
if task_id in _done(state_file):
return {'skipped': True, 'result': None}
result = fn()
done = _done(state_file)
done.append(task_id)
with open(state_file, 'w') as fh: json.dump(done, fh)
return {'skipped': False, 'result': result}
sent = []
def send_invoice():
sent.append('INV-99')
return 'sent'
print(run_once('/tmp/inv.json', 'INV-99', send_invoice))
print(run_once('/tmp/inv.json', 'INV-99', send_invoice))
print('times actually sent:', len(sent))
# Output:
# {'skipped': False, 'result': 'sent'}
# {'skipped': True, 'result': None}
# times actually sent: 1Re-running the whole batch is harmless
processed = set()
def handle(order_id):
if order_id in processed:
return 'skip'
processed.add(order_id)
return 'charged'
batch = ['A', 'B', 'A', 'C', 'B']
print([handle(o) for o in batch])
# Output:
# ['charged', 'charged', 'skip', 'charged', 'skip']Common mistakes & gotchas
Marking done before the work succeeds
If you record the ID *before* doing the work and the work then crashes, the next run skips it — the work never happens but the system thinks it did. Always do the work first, mark done only on success.
An in-memory set forgets on restart
A set() held in a variable resets to empty the moment the process restarts — exactly the situation idempotency is meant to survive. The seen-set has to be persisted (file, DB, cache) to guard across runs.
Idempotency keys that aren't actually unique
The guard is only as good as the ID. Keying on customer_name when two customers share a name, or on a timestamp that two events share, lets real duplicates through and wrongly skips distinct work. Use a stable, genuinely-unique key per unit of work.
Why it matters
Every scheduled job, webhook handler, and message consumer in production will be delivered or triggered more than once — that's a guarantee, not an edge case. Designing operations to be idempotent is what lets you re-run, retry, and recover without leaving a trail of duplicate charges and doubled emails behind you.