CLAUDE LABJP
DEFAULT — Sonnet 5 is now the default model for Pro, Team Standard, and Enterprise seats, with a native 1M-token context window and adaptive thinking on by defaultREWIND — The /rewind command can now restore a conversation after /clear. At a checkpoint you choose whether to roll back the code, the conversation, or bothORGDEFAULT — Organization default models are supported. When you have not picked a model yourself, /model shows it as Org default or Role defaultATTACH — File attachments in chat are now clickable. Cmd or Ctrl-click reveals the file in Finder or ExplorerBASH — Bash mode gained live file path autocomplete, and auto mode now spells out the reason when it declines an actionEXPIRE — The claim window for the $100 promotional credit closed on August 2. Credits already claimed expire September 17, so any evaluation run needs to sit inside that windowDEFAULT — Sonnet 5 is now the default model for Pro, Team Standard, and Enterprise seats, with a native 1M-token context window and adaptive thinking on by defaultREWIND — The /rewind command can now restore a conversation after /clear. At a checkpoint you choose whether to roll back the code, the conversation, or bothORGDEFAULT — Organization default models are supported. When you have not picked a model yourself, /model shows it as Org default or Role defaultATTACH — File attachments in chat are now clickable. Cmd or Ctrl-click reveals the file in Finder or ExplorerBASH — Bash mode gained live file path autocomplete, and auto mode now spells out the reason when it declines an actionEXPIRE — The claim window for the $100 promotional credit closed on August 2. Credits already claimed expire September 17, so any evaluation run needs to sit inside that window
Articles/API & SDK
API & SDK/2026-06-14Advanced

Making Claude Agent SDK Tools Idempotent — Stopping Double Execution with Deterministic Keys and an Outbox

An implementation log for stopping a Claude Agent SDK retry or session resume from processing the same payment twice. Three patterns — deterministic idempotency keys, an outbox, and a lightweight wrapper — with runnable code and production metrics.

claude-agent-sdk6idempotency6outboxreliability17production111

Premium Article

One morning I found the same invoice ID printed twice in a payment agent's log, and my stomach dropped.

The amount was small and Stripe's Idempotency-Key had blocked the second charge, so there was no real damage. But tracing it back, the SDK's timeout retry had called the same tool twice, and in between, two rows had landed in our own database. If Stripe hadn't caught it, the customer would have been billed twice.

As agents start handling side effects, this class of incident quietly multiplies. Claude Agent SDK has session resume and tool retries built in, which is exactly what makes it robust for long tasks and transient failures. The flip side is that irreversible side effects — payments, emails, inventory decrements — stay exposed to double execution unless you design idempotency. The docs cover the per-API idempotency-key header, but rarely how to build idempotency for the agent as a whole.

What follows is the idempotency layer I rebuilt for a payment agent I ran as an indie developer, written up so you can use it directly. Three patterns: a deterministic idempotency key, an outbox, and a lightweight wrapper — with runnable code and what to measure in production.

Why idempotency is one notch harder in agents

Plain API clients need idempotency too, but agents add a wrinkle: the line between failure and success is blurry. Having caused the incident myself, what I came to understand is that three things bite at once.

First, model nondeterminism — the same intent can produce subtly different tool arguments, so you can't let the model generate the key that decides "is this the same operation." Second, partial success — if the loop crashes right after a tool succeeds, the next start has the model interpret it as "not called yet" and re-run it. Third, resume — session resume and checkpoints rewind state, so if the side-effect layer can't detect duplicates, they slip right through.

A mature SDK like Stripe accepts an Idempotency-Key, but your own DB INSERTs, email sends, and internal APIs need idempotency you build yourself. I make it a rule to get every tool idempotent before shipping; reverse that order and something breaks eventually.

Pattern 1: Derive a deterministic key from the inputs

An idempotency key needs exactly one property: the same intent must produce the same value no matter how many times you generate it. Minting a UUID on the spot is useless, so derive it deterministically by hashing the operation's inputs.

# idempotency_key.py — derive the same key deterministically from the same intent
from __future__ import annotations
import hashlib
import json
from typing import Any
 
 
def stable_idempotency_key(
    session_id: str,
    tool_name: str,
    logical_args: dict[str, Any],
    *,
    version: str = "v1",
) -> str:
    """Generate a deterministic idempotency key.
 
    Pass only the minimal args that represent intent into logical_args.
    Mixing in timestamp or retry_count makes the key change on every retry.
    """
    # Canonicalize with fixed key order (same key regardless of arg order)
    canonical = json.dumps(logical_args, sort_keys=True, separators=(",", ":"), default=str)
    raw = f"{version}|{session_id}|{tool_name}|{canonical}"
    digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
    return f"idem_{version}_{digest[:32]}"
 
 
if __name__ == "__main__":
    k1 = stable_idempotency_key(
        "sess_abc", "charge_payment",
        {"customer_id": "cus_001", "amount_jpy": 2480, "invoice_id": "inv_555"},
    )
    k2 = stable_idempotency_key(
        "sess_abc", "charge_payment",
        {"invoice_id": "inv_555", "amount_jpy": 2480, "customer_id": "cus_001"},
    )
    assert k1 == k2  # same key even with different arg order

Mixing time.time() or randomness into key generation breaks idempotency, because the key changes on every retry. Internal metadata like a retry count is just as guilty. Extract intent only — that's the rule.

The version field is a safety valve for the day you want to change key generation without colliding with old keys. I once skipped it and burned a night on DB cleanup when the key format changed. Adding it up front is cheap insurance.

Issue the session ID at the caller that launches the agent and pass it in. ClaudeSDKClient manages sessions internally, but to share with an external persistence layer you need to hold an explicit ID.

import uuid
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
 
session_id = f"sess_{uuid.uuid4().hex}"
options = ClaudeAgentOptions(
    system_prompt="You are a payment-processing agent.",
    extra_context={"session_id": session_id},  # make it reachable from the tool
)

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Deriving a deterministic idempotency key from inputs so retries and session resumes never change it — with full code
Using the outbox pattern to align the agent loop and external API transaction boundaries and structurally erase double charges
Three production metrics — duplicate rate, outbox backlog, key collision rate — and real threshold settings
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API & SDK2026-05-08
Implementing the Saga Pattern in Claude Agent SDK — Compensating Transactions and Idempotency
A practical guide to building safe multi-step Claude Agent SDK workflows. We cover compensating transactions, idempotency keys, and partial-failure state recovery, all from patterns that have run in production.
API & SDK2026-05-07
Implementing the Transactional Outbox Pattern with Claude Agent SDK — Eliminating Lost Side Effects in Production
Stop the 'the row was inserted but the email never went out' class of bugs in Claude Agent SDK apps. A production-grade walkthrough of the Transactional Outbox pattern using Postgres and Cloudflare Queues.
API & SDK2026-06-28
Did That Post Actually Go Through? Safely Retrying an Interrupted MCP Write Without Double-Executing
When an MCP write tool call is interrupted by a dropped connection, you can't tell whether the server ran it. Here's why naive retries cause double-execution, and a working wrapper that uses idempotency keys and a reconcile read to retry safely — with examples from an unattended pipeline.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →