Idempotency is the property that performing an operation once has exactly the same effect as performing it many times. It is the single most useful concept when building systems that retry, resume, or run concurrently.
Why it matters
Distributed systems retry. Networks time out, mobile clients resend, schedulers run a job twice because the first run was still in progress, and operators re-run a deployment script after a failure. Without idempotency, every retry is a potential duplicate payment, duplicated row, double-counted stock unit, or a server provisioned twice.
NON-idempotent:
POST /transfer {"to": "DE12...", "amount": 100}
-> times out, client retries, customer is debited twice.
Idempotent (with an idempotency key):
POST /transfer
Idempotency-Key: 3f9a1c8e-...
{"to": "DE12...", "amount": 100}
-> first call transfers, retries return the SAME response,
never a second transfer.
By definition
- GET, HEAD, OPTIONS, TRACE — safe and idempotent by design (no side effects).
- PUT — idempotent: "make the resource look exactly like this". Repeating it converges to the same state.
- DELETE — idempotent: the resource is gone after the first call; the second returns 404 (or 204), still no additional effect.
- POST — not idempotent by default: each call may create a new resource.
- PATCH — depends on the payload.
{"count": 5}is idempotent;{"count": "+1"}is not.
Patterns that make writes idempotent
1. Natural key / upsert
INSERT INTO server (name, ip) VALUES ('web01', '10.0.0.11')
ON DUPLICATE KEY UPDATE ip = VALUES(ip);
-- Repeating never creates a second row.
2. Idempotency key table
CREATE TABLE idempotency (
key CHAR(36) PRIMARY KEY,
request TEXT NOT NULL,
response TEXT NOT NULL,
created DATETIME NOT NULL
);
-- First request: process, store response.
-- Retry: same key found -> return the stored response, do nothing.
3. Conditional write (optimistic locking)
UPDATE konto SET saldo = saldo - 100, version = version + 1
WHERE id = 42 AND version = 7;
-- 0 rows affected -> someone else already applied it.
4. Declarative configuration
-- Instead of "add rule", state the full desired rule set.
-- Applying the same state twice changes nothing.
This is exactly why Ansible, Terraform and Kubernetes work.
5. Unique constraint as a guard
ALTER TABLE bestellung ADD CONSTRAINT uq_ext UNIQUE (external_id);
-- A duplicate attempt fails loudly instead of silently doubling data.
Idempotent by design: configuration management
# Shell script - NOT idempotent, fragile on repeat
useradd deploy
mkdir /srv/app
systemctl start nginx
# Second run: "user already exists", "directory exists" -> errors.
# Ansible - idempotent, describes a STATE
- ansible.builtin.user: { name: deploy, state: present }
- ansible.builtin.file: { path: /srv/app, state: directory, mode: "0755" }
- ansible.builtin.service: { name: nginx, state: started, enabled: true }
# Second run: everything reports "ok", nothing changes.
# A second run that reports "changed" is a bug worth fixing.
Related concepts — often confused
- Idempotent vs. pure: A pure function has no side effects; an idempotent operation may have side effects but their effect does not accumulate.
DELETE /user/1is not pure but is idempotent. - Idempotent vs. safe: Safe means no side effect at all (GET). Idempotent allows side effects, just not cumulative ones.
- Idempotent vs. concurrent-safe: Different problems. Two simultaneous idempotent calls can still race; that needs locking or a uniqueness constraint.
Checklist
- Can every retried write request be safely repeated?
- Do API clients send an idempotency key for payment, order and provisioning endpoints?
- Are there unique constraints backing the "must not duplicate" business rules — not just application checks?
- Does the deployment tooling describe states rather than issuing commands?
- Do scheduled jobs tolerate being run twice (overlapping runs)?
- Is the retry policy bounded, with exponential backoff and jitter?
Related: Cache, REST, systemd, Ansible (DE).