Back to blog

Your Agent Said 'Ready to Commit.' Here's Why That Might Be a Lie

Sep 16, 2026
Series · Day 12
Distributed Systems in 30 Days
View all lessons →
Your Agent Said 'Ready to Commit.' Here's Why That Might Be a Lie

Day 12 — The 'Yes' That Has to Stick

Two of your agents just booked the same slot for the same customer. Nobody shipped buggy code — the orchestrator did exactly what you asked it to. Any system that fans work out to sub-agents and then tries to commit their results together is quietly running Two-Phase Commit, whether you've ever said that name out loud or not. And most implementations skip the one clause that makes the whole protocol hold together. That's exactly where the bug lives.

The vote that vanished

Here's the sequence, and you've probably seen a version of it. A participant says yes, I'm ready. Then, before anyone tells it to actually go, the thing it reserved disappears. The row it locked gets reclaimed by a timeout job. The session it held gets evicted to free up memory. The task it was about to run gets picked up and re-executed by a different worker because the coordinator was slow to reply. When the coordinator finally circles back and says 'commit,' there's nothing left to commit. The yes was a lie — not because anyone lied, but because nothing was enforcing it in the first place.

Naming the guarantee

Two-Phase Commit's core contract, stated precisely: in the prepare phase, a participant votes yes only if it can guarantee the operation will succeed no matter what happens next — and that guarantee has to survive until phase two, however long that takes. 'Prepared' doesn't mean 'probably fine.' It means: I have durably reserved this, I cannot unilaterally let it go, and I will honor whatever the coordinator decides — commit or abort — even after a crash, a restart, a long pause. Day 11 covered what the coordinator does once it has those votes in hand; today is about what has to be true on the participant's side for the vote to mean anything at all. A coordinator making flawless decisions on top of unenforceable promises is still a broken system.

Where the promise breaks: three shapes of the same bug

  • Database row: a participant prepares a transaction, using a held row lock as its proof of reservation. A lock-wait timeout, a failover, or an overzealous connection pool reaper releases that lock before phase two arrives. The 'yes' is now unbacked — any other transaction is free to grab the row and mutate it.
  • API session: a service prepares a multi-step operation and treats an in-memory session or a sticky-routed connection as its reservation. An unrelated autoscaler event kills the pod, or the load balancer routes the next request to an instance that's never heard of this session. The reservation only ever lived in one process's memory — it didn't survive the process.
  • LLM sub-agent: an orchestrator asks a sub-agent to 'reserve' a task — write a file, hold a slot, claim a customer record — and gets back 'done, ready to commit.' But nothing actually locked the resource. A second agent, running concurrently or spun up after a retry, claims the same task and overwrites the first one's state. The prepare message was just words; the reservation never existed.

Why LLM agents make this worse, not just repeat it

A database's 'yes' is backed by a write-ahead log entry and a held lock — physical state that survives a crash and gets replayed on recovery. An LLM sub-agent's 'yes, I've prepared this' is a sentence in a context window. No WAL behind it, no lock table, no fencing token, unless you go build one yourself. The moment that agent's context gets truncated, the process restarts, or a scheduler kicks off a duplicate instance of the same agent — which orchestration frameworks do all the time, on purpose, for retries — the promise just evaporates, and nothing notices, because nothing was ever checking. This is the failure you've probably already debugged under a different name: two agents both 'successfully' book the same slot, a tool call fires twice because the first agent's completion signal got lost and the orchestrator retried, a task marked 'claimed' in one agent's output gets claimed again by a fresh agent spun up after a timeout. It's the same problem distributed systems have had since the late 1970s. The only new part is that these systems sound confident enough to make teams forget to ask whether the confidence is backed by anything.

What actually has to exist for a 'yes' to be trustworthy

You don't need to implement the whole 2PC protocol to fix this. You need three primitives that make a 'yes' durable, and they're the same three whether your participant is Postgres or a sub-agent you spun up ten seconds ago.

  • Fencing tokens: every reservation gets a monotonically increasing token. When the commit finally arrives, the participant checks that its token is still current before acting. If a second reservation attempt issued a higher token in the meantime, the first one is stale and refuses to commit — instead of silently clobbering state.
  • Leases with explicit expiry: a reservation is a lease with a stated TTL, not an indefinite hold. The participant (or something supervising it) knows exactly when the lease dies, and the coordinator has to commit, abort, or explicitly renew before that clock runs out. No 'it'll probably still be there.'
  • Idempotent commit and abort: the coordinator's message can arrive late, arrive twice, or arrive after the lease already expired. So commit and abort both have to be safe to apply more than once, and safe to apply to an already-expired reservation — a no-op abort, not a crash. This is the difference between 'the network was flaky' and 'the data got corrupted.'
python
# minimal shape of a trustworthy prepare, for a sub-agent claiming a task
def prepare(task_id, agent_id):
    token = store.incr(f"fence:{task_id}")          # fencing token
    store.set(f"lease:{task_id}", agent_id, ex=LEASE_TTL_SECONDS, nx=True)
    return {"vote": "yes", "task_id": task_id, "fence_token": token}

def commit(task_id, fence_token):
    current = store.get(f"fence:{task_id}")
    if int(current) != fence_token:
        return "abort"                                # stale reservation, safe no-op
    apply_effect(task_id)                              # idempotent by design
    store.delete(f"lease:{task_id}")
    return "committed"

Before you call it 'prepared'

Run this against your orchestrator before you trust any agent's 'ready to commit' message:

  • If this participant crashed or got rescheduled right now, is the reservation still enforced — or did it only ever exist in someone's memory?
  • Does the reservation have an explicit expiry both sides agree on, or is it 'until someone says otherwise'?
  • If the commit message arrives twice, does anything break — a double charge, a double write, a double claim?
  • If a second attempt at the same task races the first, is there a token or version check that lets the loser lose safely?
  • Is the 'yes' backed by something the participant can't unilaterally forget — or is it just a string sitting in a prompt?

Get those five right and you've built the durable-prepare half of 2PC, whether or not you ever call it that. Day 13 goes after the other half of this same problem: what happens when it's the coordinator — not the participant — that can't be trusted to still be there when phase two comes around.

Flashcards
Check yourself

Extend your knowledge

  • Read Martin Kleppmann's 'How to do distributed locking' — the clearest explanation anywhere of why a lock without a token isn't actually a lock.
  • Go look at how your orchestration framework (LangGraph, Temporal, or a hand-rolled agent runner) represents 'in-flight' task state. Is it a durable store with a TTL, or an in-memory dict that dies with the process?
  • Take one real agent handoff in your pipeline and rewrite it using the prepare/commit shape above. Then simulate a duplicate commit call and see what breaks.
  • Go back and read Day 11's coordinator notes next to today's participant-side view — you now have both halves of the same protocol.
Test yourself on this lesson

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “Your Agent Said 'Ready to Commit.' Here's Why That Might Be a Lie” — trade-offs, decisions, or the story behind it.