Pros and Cons¶
Idempotency isn't free. Understanding the tradeoffs helps you decide where to invest.
At a Glance¶
| Pros | Cons |
|---|---|
| Safe Reruns - Click rerun, walk away | Implementation Complexity - Every op needs guards |
| Partial Failure Recovery - Matrix jobs retry cleanly | Performance Overhead - Extra API calls add up |
| Simplified Debugging - Debug without changing state | Hidden Failures - Silent skips mask real errors |
| Scheduled Job Safety - Handle duplicate triggers | State Consistency Challenges - Detecting state is hard |
| Reduced Cognitive Load - System tracks state | Not Always Possible - Some ops can't be idempotent |
Pros¶
Safe Reruns¶
The primary benefit. When something fails, you click "Re-run jobs" and walk away. No fear. No manual state inspection. No "did it already do X?" questions.
# Safe to rerun: checks before acting
- name: Create PR if not exists
run: |
EXISTING=$(gh pr list --head $BRANCH --json number --jq 'length')
if [ "$EXISTING" -eq 0 ]; then
gh pr create --title "Update" --body "Automated update"
else
echo "PR already exists, skipping creation"
fi
Partial Failure Recovery¶
Matrix jobs processing 40 repositories. Job 37 fails due to rate limiting. With idempotent operations, you rerun the entire matrix. The 36 successful repos detect "no changes needed" and skip. Job 37 retries and succeeds.
Without idempotency, you'd need to figure out which repos succeeded, exclude them, and run only the failures.
Simplified Debugging¶
When operations are idempotent, you can add logging, rerun, and see exactly what happens without changing state. Debug freely.
- name: Debug and retry
run: |
set -x # Enable debug output
# Safe to rerun with debugging enabled
./idempotent-script.sh
Scheduled Job Safety¶
Cron-triggered workflows might run twice due to GitHub Actions quirks, overlapping schedules, or manual triggers during scheduled windows. Idempotent operations handle this gracefully.
Reduced Cognitive Load¶
Engineers don't need to track "did this already run?" or maintain mental models of partial state. The system handles it.
Cons¶
Implementation Complexity¶
Every operation needs guards. Create-if-not-exists. Update-or-create. Check-before-act. This adds code, conditions, and potential bugs in the guard logic itself.
# Simple but not idempotent
- run: git commit -m "Update"
# Idempotent but more complex
- run: |
if [ -n "$(git status --porcelain)" ]; then
git commit -m "Update"
else
echo "No changes to commit"
fi
Performance Overhead¶
Checking "does this already exist?" before every operation adds API calls, database queries, or filesystem checks. For high-volume operations, this overhead accumulates.
# Fast but not idempotent
- run: gh pr create ...
# Slower but idempotent (extra API call)
- run: |
if ! gh pr list --head $BRANCH --json number | jq -e 'length > 0'; then
gh pr create ...
fi
Hidden Failures¶
Silent Skips Can Mask Real Failures
When operations silently skip because "already done," you might miss actual failures. Did the PR not get created because it exists, or because the creation failed and the error was swallowed?
# Dangerous: masks errors
- run: |
gh pr create ... || echo "PR might already exist"
# Better: explicit state checking
- run: |
if gh pr list --head $BRANCH --json number | jq -e 'length > 0'; then
echo "PR exists"
else
gh pr create ... # Fails loudly if creation fails
fi
State Consistency Challenges¶
Idempotency assumes you can reliably detect current state. But what if:
- The PR exists but was closed?
- The file exists but has wrong content?
- The branch exists but diverged?
Naive idempotency checks might skip operations that actually need to run.
Not Always Possible¶
Inherently Non-Idempotent Operations
Some operations can't be made idempotent:
- Sending notifications (can't unsend)
- Incrementing counters (can't detect if already incremented)
- Time-sensitive operations (state changes between check and act)