Since the 90s a classic bug always plagued web forms. You've probably seen it — the browser warning that says "Resubmitting this form will repeat the action." Your user placed an order, hit refresh, and now there are two orders. Or two emails. Or two charges.
Two orders. Two charges. One frustrated customer.
The solution was the Post/Redirect/Get (PRG) pattern. Elegant, simple, and mostly forgotten. It got absorbed into server-side frameworks as redirect helpers, so new developers never had to reach for it consciously. Then client-side JavaScript closed the loop completely — XHR callbacks, jQuery deferreds, async/await in every framework that followed. Mutations became self-contained operations. Spinners, optimistic updates, loading indicators removed the last visible seam between "do the thing" and "show the result." You stop thinking about POST-replay when there's no page to replay.
The pattern didn't disappear because it was solved. It disappeared because the stack made it invisible.
AI agents just reintroduced the same bug at a new layer. And most teams building on A2A, MCP, or custom agentic pipelines are about to meet it for the first time.
What PRG Actually Is
Without PRG, a web form works like this:
- User fills out the form, clicks Submit
- Browser sends a POST to the server
- Server charges the card, sends the email, creates the record
- Server responds with a success page
- User hits Refresh
- Browser replays the POST — everything runs again
The fix is simple: never respond to a POST with a page. Instead:
- User submits → browser sends POST
- Server does the work
- Server responds with a 302 Redirect to a result URL
- Browser follows it → sends a GET
- Server returns the result page
- User hits Refresh → browser replays the GET, which is harmless
The dangerous POST is now unreachable by refresh. The redirect is the gate between "do the thing" and "show the result of the thing."
Three things make it work. The POST fires exactly once — the redirect gets the user out of POST-land immediately. The redirect carries a stable ID, an order number or transaction ID, that anchors the result. And the GET is idempotent — hitting it a hundred times returns the same page and does nothing new.
HTTP/1.1 even shipped a status code for this — 303 See Other, meaning "retrieve the response to this request using GET." Most people used 302 because early browsers were inconsistent with 303, but the intent was in the spec by 1999. REST filled in the theory the following year: GET is safe and idempotent, POST is neither. Once you have that vocabulary the fix is obvious. The spec, the status code, and the theory all existed long before agent frameworks were a thing.
Agents Have the Exact Same Bug
A typical agentic loop:
- User asks the agent to place an order
- Agent calls
create_ordervia MCP or A2A - Network drops before the response arrives
- Agent has no idea if it worked
- Agent retries
create_order - Two orders. Two charges. One angry customer.
This isn't a theoretical edge case. It happens in production whenever a network timeout hits mid-tool-call, a container restarts during a long task, a rate limit kicks in and the agent backs off and retries, or an LLM re-samples and re-runs a tool it already called.
The agent, like the browser before it, doesn't know whether its last action landed. So it tries again.
The Mapping
The fix is the same fix. The names change, the layer changes, the problem doesn't.
| Web PRG | Agent equivalent |
|---|---|
| POST — dangerous, non-idempotent | Mutating tool call — charge_card, send_email, create_record
|
| 302 Redirect | Idempotency key anchoring the operation |
| Stable order ID in the URL | Deterministic operation ID tied to user intent |
| GET — safe, repeatable | Reading the stored result by that key |
The idempotency key is the redirect. It's what separates "do the thing" from "return what we got when we already did it."
When an agent calls create_order, it should pass a key like:
idempotency_key = hash(user_id + "place_order" + cart_id)
Server checks: seen this key before? Yes — return the stored result. No — process, store, respond.
The agent can now retry a hundred times. Every call after the first hits the key, finds the result, returns it. Nothing runs twice.
The rule is the same one as PRG: the key has to live above the retry loop. Generate it once, before the first attempt, from something stable — the user's intent, not the timestamp of the request. A key that changes on every retry defeats the whole point, same as generating a fresh form URL on every refresh would defeat PRG.
Where PRG Ends and Agents Get Harder
PRG protects the entry point. A web form is atomic from the user's perspective — it either went through or it didn't.
An agentic task can run for twenty minutes, touch ten different tools, and crash halfway through. An idempotency key at the entry point does nothing for you here. What you need is checkpointing — saving state at each meaningful step so a restart picks up where it left off instead of starting over.
Think of it as PRG applied recursively at every step. Each step gets its own idempotency key. Each step's result is stored before moving on. A restart re-reads completed steps rather than re-running them.
This is what Temporal's durable execution model does mechanically. It replays event history on restart but skips any step already recorded, so side effects never fire twice. The whole execution becomes a sequence of safe GETs once each step has been committed.
The complete picture for durable agents:
- Idempotency key at task entry — prevents duplicate task creation
- Step-level checkpoints — prevents re-execution of completed steps mid-task
- Idempotency keys on every mutating tool call — the innermost protection layer
If you can only do one, do number three. A lot of production pain is avoidable if every tool that writes external state is safe to call twice.
Why This Gets Skipped
Server-side frameworks absorbed PRG first — redirect-after-POST became the default path and nobody had to think about it. Then client-side JavaScript made it feel irrelevant. Whether it was an XHR callback, a jQuery deferred, or async/await in whatever framework you favoured, every mutation became a self-contained operation that never touched the browser's navigation history. You stop thinking about POST-replay when there's no page to replay.
Agent frameworks haven't absorbed this yet. MCP has no built-in concept of idempotency keys. A2A's task lifecycle doesn't enforce step-level checkpointing. The frameworks are young and the patterns are still being discovered in production — sometimes painfully — usually after a user gets charged twice or an email goes out three times or a database fills up with duplicate records nobody can explain.
This Keeps Getting Rediscovered
What's interesting is that nobody planned for any of this to converge.
Web developers arrived at PRG in the early 2000s because browsers forced the issue — form replay was a user-visible bug with no workaround other than restructuring the request flow.
Temporal came at it from distributed systems. Their durable execution model replays workflow history on restart but skips steps already recorded. The framing is different but the underlying guarantee is the same: a side effect that already happened doesn't happen again.
And a 2026 paper — Agent-First Tool APIs — from engineers running 85 tools in a production SaaS system, arrived at it from the agent reliability direction. They added idempotency_key_fields as a declared field in every tool's descriptor — not a recommendation, a required part of the contract — so the question of how a key is derived gets answered at design time, not during an incident.
Three separate communities, working on different problems, reaching the same structural answer. That pattern is usually worth paying attention to.
Before You Ship
For any agentic tool that mutates external state:
- [ ] Does every mutating tool accept an idempotency key?
- [ ] Is the key derived from the user's intent, not the request timestamp?
- [ ] Is the key generated before the first attempt and reused on every retry?
- [ ] Does the server store the result and return it on duplicate keys?
- [ ] For long-running tasks: are intermediate steps checkpointed?
The redirect is the idempotency key. The GET is the stored result. Same pattern. Different layer.



Top comments (0)