Idempotency#
What Is It?#
Idempotency means that running the same operation twice produces the same result as running it once. Nothing breaks, nothing duplicates, nothing changes the second time — because the system checks what already exists before acting.
Think of it like flipping a light switch to “on.” If the light is already on, flipping the switch again doesn’t turn on a second light or cause an error — it just stays on. Every operation in PSW works the same way: if the desired state already exists, it does nothing.
Why Does It Matter?#
PSW’s convergence engine runs automatically every 5 minutes. That means every operation — deploying apps, creating DNS records, generating secrets, wiring apps together — gets called over and over, even when nothing has changed.
Without idempotency, each run would:
- Create duplicate DNS records
- Restart apps that don’t need restarting
- Re-register integrations that already exist
- Generate new passwords, breaking existing connections
With idempotency, each run:
- Checks what already exists
- Compares it to the desired state
- Only makes changes when there’s a difference
- Reports “unchanged” when everything is already correct
This is what makes the GitOps model safe. You can push the same config twice, reboot a server, or manually trigger convergence — and nothing unexpected happens.
Where PSW Uses Idempotency#
Idempotency isn’t a single feature — it’s a design principle applied everywhere:
Convergence#
The convergence engine is the most obvious example. It runs every 5 minutes and compares the current git HEAD to the last deployed commit. If nothing changed, it does nothing. If something changed, it only deploys the affected targets .
App Deployment#
PSW’s deploy engine is idempotent by design. Before writing a file to a target, it computes a checksum of the new content and compares it to what’s already on disk. If they match, the file is skipped entirely — no write, no restart. Containers are only restarted when their configuration actually changed. Running the same deployment twice with no changes results in zero changes and zero restarts.
Wiring#
Every reconciler checks the current state before acting:
- “Is Sonarr already registered in Prowlarr?” → yes → do nothing
- “Does Backrest’s config already have the ntfy hook?” → yes → do nothing
- “Does the Forgejo OAuth source match the current Authelia config?” → no → update it
This is why wiring can safely run on every convergence cycle without creating duplicates.
Conventions#
The convention system generates config files (routes, SSO clients, scrape targets). If the generated file is identical to what’s already on disk, the aggregator doesn’t restart. Traefik only reloads when route files actually change. Prometheus only reloads when scrape configs change.
Providers#
All provider operations are safe to repeat:
- Creating a DNS record that already exists → no change
- Removing a DNS record that doesn’t exist → no error
- Reserving an IP that’s already reserved for the same target → no change
Secrets#
Secret generation
only creates secrets that don’t exist yet. If you run psw project generate-secrets twice, the second run finds all secrets already present and does nothing.
Bootstrap#
The bootstrap
process can be re-run safely. It tracks which steps completed in a progress file (.psw/bootstrap/progress.yml), so on re-run it skips already-completed steps and resumes from where it left off. Use --clean to force a fresh start.
The Pattern#
Every idempotent operation in PSW follows the same pattern:
1. Read the DESIRED state (from project files, metadata, config)
2. Read the ACTUAL state (from the running system, API, files)
3. Compare them
4. If different → make the change
5. If identical → report "unchanged" and move onThis is sometimes called the check-then-act pattern. It’s why convergence can run continuously without side effects, and why re-running any PSW command is always safe.
What It Looks Like in Practice#
When convergence runs with no changes, the report shows all operations as “unchanged”:
Phase 1: Reconcile — unchanged
Phase 2: Infrastructure — unchanged
Phase 3: Prepare — unchanged
Phase 4: Teardown — nothing to tear down
Phase 5: Deploy — unchanged (no diff since last commit)
Phase 6: Finalize — recordedWhen convergence runs after you add an app, only the relevant parts show changes:
Phase 2: Infrastructure — created target "media"
Phase 3: Prepare — prepared target "media"
Phase 5: Deploy — deployed jellyfin on "media"
— wiring: jellyfin unchanged, tdarr→jellyfin createdEverything else remains “unchanged” — because idempotency means the system only touches what needs touching.
Key Ideas#
- Safe to repeat — every operation can run multiple times without side effects
- Check before acting — always compare desired vs. actual state before making changes
- No duplicates — creating something that already exists is a no-op, not an error
- No errors on missing — removing something that doesn’t exist is a no-op, not a failure
- Continuous operation — enables the every-5-minutes convergence model without risk
- Foundation of GitOps — the reason pushing the same config twice is safe