CLAUDE LABJP
BILLING — 1 day to the Jun 15 change: Agent SDK, headless runs, GitHub Actions, and third-party agents move to separate monthly credits ($20/$100/$200) metered at full API rates, no rolloverFABLE5 — Claude Fable 5, a Mythos-class model billed as Anthropic's most capable generally available release, is usable in Claude Code v2.1.170+ (launched Jun 9)SUBAGENTS — Claude Code sub-agents can now spawn their own sub-agents, with smarter model and region handlingENTERPRISE — Custom roles gain admin permissions, letting members reach billing and privacy settings without Owner accessPLUGINS — New plugin search plus better Chrome, VSCode, and terminal workflows; session, memory, and permission bugs fixedUI — New setting disables mouse-wheel scroll acceleration in fullscreen; the /model picker now shows model families correctlyBILLING — 1 day to the Jun 15 change: Agent SDK, headless runs, GitHub Actions, and third-party agents move to separate monthly credits ($20/$100/$200) metered at full API rates, no rolloverFABLE5 — Claude Fable 5, a Mythos-class model billed as Anthropic's most capable generally available release, is usable in Claude Code v2.1.170+ (launched Jun 9)SUBAGENTS — Claude Code sub-agents can now spawn their own sub-agents, with smarter model and region handlingENTERPRISE — Custom roles gain admin permissions, letting members reach billing and privacy settings without Owner accessPLUGINS — New plugin search plus better Chrome, VSCode, and terminal workflows; session, memory, and permission bugs fixedUI — New setting disables mouse-wheel scroll acceleration in fullscreen; the /model picker now shows model families correctly
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-sdk5idempotency3outboxreliability5production90

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-04-24
Running the Claude API in Python Production — Rate Limits, Retries, and Timeouts
If you put Claude API into a real backend service, how you handle 429, 503, and read timeouts decides your reliability ceiling. This is the design I settled on after operating it in production.
📚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 →