Idempotency in network automation¶
Across the automation articles on this site one property keeps surfacing as the thing that decides whether
an approach is safe: idempotency. The approach-selection article
calls it "the deciding property," Ansible's changed=0 is held up as
proof of it, and consuming REST APIs turns on which HTTP methods have it.
This article pulls that thread into one place: what idempotency means, why some tools give it to you for free
while others don't, and how to build it into your own code.
What idempotency actually means¶
An operation is idempotent when applying it repeatedly has the same effect as applying it once. Run the same automation against a device twice and the device ends in the same state both times — the second run adds no duplicate lines, raises no error, and reports that nothing changed. That last signal is the practical test: a correctly written, idempotent configuration run reports no change on its second pass because the device already matches the state you declared.
Crucially, idempotency is about the end state of the system, not the response you get back. A DELETE that
returns 204 No Content the first time and 404 Not Found the second is still idempotent — the resource is
gone either way. Judge idempotency by where the system lands, not by whether the two responses are byte-for-byte
identical.
Procedural versus convergent¶
The reason some tools are idempotent and others aren't comes down to imperative versus declarative.
- Imperative / procedural automation describes the steps: "log in, enter config mode, type these commands." A raw script that sends CLI does exactly what you coded, every time, whether or not the change is already in place. Run it twice and it can double-apply, or fail on the second pass because the thing it was about to create already exists.
- Declarative / convergent automation describes the desired end state — "VLAN 10 named SALES should exist" — and lets the tool work out the difference between that and reality, applying only what's missing. Declarative tools (Terraform, Ansible resource modules) are idempotent by design: they converge toward the state you declared, so a re-run is safe and is a no-op once reality already matches.
This is why declarative approaches are the default for configuration management. Idempotent convergence means you can run automation on a schedule to enforce state and correct drift, without fear that re-running will cause damage. A one-shot procedural script can't make that promise.
Idempotency in your configuration tools¶
Ansible delivers idempotency through its resource modules. A module such as ios_vlans or ios_interfaces
manages a feature as structured data and reconciles your declared config against what's on the box — you own
none of the "is it already there?" diffing logic yourself. The proof is in a repeat run:
$ ansible-playbook vlans.yml # first run
changed: [csr1]
PLAY RECAP
csr1 : ok=1 changed=1 unreachable=0 failed=0
$ ansible-playbook vlans.yml # second run, nothing to do
PLAY RECAP
csr1 : ok=1 changed=0 unreachable=0 failed=0
changed=0 on the second pass is the signal that the task is genuinely idempotent. If repeat runs keep
reporting changes, the task isn't converging — often a sign of raw ios_config without proper matching rather
than a resource module. You can preview convergence before touching production by combining --check (simulate)
with --diff (show the exact delta that would be applied). One caveat: the module's state key changes the
reconciliation. merged is additive and safe; overridden makes the whole feature match your input, silently
removing anything you didn't list. Both are idempotent — they just converge to different end states.
Terraform builds idempotency into its whole model. terraform plan computes the difference between your
declared configuration, its recorded state, and the real world; terraform apply makes only that difference.
Apply an unchanged configuration and the plan is empty — nothing happens. That same mechanism is what surfaces
drift: if someone changes a device out of band, the next plan shows it as a diff to reconcile.
Idempotency over HTTP¶
When you drop to the API layer — RESTCONF, or any REST controller — idempotency becomes a property of the HTTP
method you choose. RFC 9110 §9.2.2 defines the
idempotent methods as PUT, DELETE, and the safe methods (GET, HEAD). POST is not idempotent: each
call can create new state, which is why a duplicated POST produces duplicate resources.
| Method | Idempotent? | Typical effect |
|---|---|---|
GET / HEAD |
Yes (and safe) | Read only, no change |
PUT |
Yes | Replace the resource at this URI with exactly this body |
DELETE |
Yes | Remove the resource; gone stays gone |
PATCH |
Not guaranteed | Merge a partial change (depends on the patch) |
POST |
No | Create / invoke; may produce a new resource each time |
In RESTCONF terms this maps directly: PUT replaces a resource to a known state (idempotent — send it twice,
same result), while PATCH merges. Prefer a PUT to a fixed end state when you want a repeatable, convergent
write.
The practical consequence shows up in retries. Only retry operations that are safe to repeat. A transient
429 or 5xx on a GET, PUT, or DELETE can be retried with backoff, because repeating it yields the same
end state. Blindly retrying a POST, though, can create duplicates when the first request actually succeeded but
its response was lost. When you must make a POST repeatable, either check first whether the resource
already exists, or send an idempotency key — a client-generated unique value the server uses to recognise a
retry and return the original result instead of acting twice (the pattern Stripe popularised and the subject of
an IETF Internet-Draft, Idempotency-Key,
still in progress rather than a finalised standard).
# Make a "create" safe to re-run: check before you POST.
import requests
def ensure_loopback(session: requests.Session, base: str, unit: int, ip: str) -> None:
url = f"{base}/loopbacks/{unit}"
# PUT is idempotent — replacing to a known state is safe to repeat.
body = {"unit": unit, "ipv4": ip}
if session.get(url, timeout=10).status_code == 200:
session.put(url, json=body, timeout=10).raise_for_status() # already exists: converge it
else:
session.post(f"{base}/loopbacks", json=body, timeout=10).raise_for_status() # create once
Why it pays off¶
Idempotency is what makes the rest of modern practice possible. Because a convergent run is safe to repeat, you can schedule it to continuously enforce intended state and correct drift back toward your source of truth — the basis of GitOps, where the repository holds intended state and a pipeline reconciles devices toward it. It makes automated retries safe, so transient network failures don't force a human to reason about "did the first attempt half-apply?" And it makes change review honest: a dry run that reports no change is a genuine guarantee that nothing will move.
Making your own code idempotent¶
When no maintained module covers your case and you're writing custom logic, build idempotency in deliberately:
- Prefer a maintained resource module or provider before hand-rolling CLI — idempotency and drift handling come built in, and you won't reinvent them (badly).
- Converge, don't append. Read current state, compute the difference, and apply only what's missing — rather than blindly issuing "add" commands.
- Choose idempotent verbs. Favour
PUTto a known state overPOSTwhere the API allows it. - Guard non-idempotent calls with a check-first lookup or an idempotency key so a retry can't duplicate work.
- Test by running twice. The cheapest idempotency test is to apply your automation, apply it again, and assert the second run changes nothing.
Key takeaways¶
- Idempotency = applying an operation repeatedly has the same effect as applying it once — judged by the system's end state, not by identical responses.
- Declarative tools are idempotent by design (they converge to a desired state); procedural scripts are not (they replay steps and can double-apply).
- In Ansible,
changed=0on a repeat run is the proof; preview with--check --diff. In Terraform, an emptyplanis the same signal, and a non-empty one is drift. - Over HTTP,
GET/PUT/DELETEare idempotent andPOSTis not (RFC 9110); retry only idempotent methods, and makePOSTsafe with a check-first lookup or an idempotency key. - Idempotency is what makes scheduled enforcement, drift correction, and safe retries possible — build it into custom code on purpose, and test it by running twice.
Sources: RFC 9110 — HTTP Semantics, §9.2.2 Idempotent Methods, RFC 8040 — RESTCONF Protocol, draft-ietf-httpapi-idempotency-key-header — The Idempotency-Key HTTP Header Field, Ansible network resource modules, Terraform: plan and apply.