Blog · July 1, 2026
Your AI agent will make the same write twice. Make the second one free.
An agent finishes a step and calls a tool: create the task, log the decision, apply the event. The call times out on the way back — not because it failed, but because the response got lost. The runner does the sensible thing and retries. The write lands a second time. Now there are two tasks, or two decision rows, or a billing event applied twice, and here's the part that bites: both calls returned success. Nothing errored. Nothing paged. You find out when someone opens the project and sees the work listed twice.
This isn't a rare edge case you can prompt your way around. It's the default behavior of every path that triggers an agent, and the fix isn't to stop the duplicate — it's to make the duplicate cost nothing.
Exactly-once delivery is a fairy tale
The instinct is to make the retry not happen: add a guard, tighten the timeout, get the network to behave. It doesn't hold, because the duplicate isn't a mistake in your code — it's a property of the system underneath it. Exactly-once delivery across a network is famously impossible; the sender can't tell "the write failed" apart from "the write succeeded and the acknowledgement got lost," so any honest system errs toward doing the work again rather than dropping it. That's at-least-once, and it's the model whether you chose it or not.
Look at how an agent actually gets triggered and every path is at-least-once:
- A tool call that times out and gets retried by the host or the SDK.
- A queue message (SQS, Celery, BullMQ) redelivered because the worker didn't ack in time.
- A webhook (Stripe, GitHub, a CRM) re-fired because your endpoint was slow to answer 200.
- A resumed run that crashed mid-step and replays its last action on restart.
None of those is exotic. They're the normal machinery of running agents unattended. If your writes assume each one happens exactly once, every one of them is a latent double-write waiting for a slow network.
The industry answer isn't "prevent it" — it's "absorb it"
Distributed systems settled this argument a long time ago: you accept at-least-once delivery and you make the consumer idempotent. Idempotent means running the same operation twice leaves the same state as running it once — the second write is a no-op that returns the first one's result. You stop trying to guarantee the message arrives once and instead guarantee that arriving twice is harmless.
The mechanism is a dedup key. Every write carries a stable identifier for this logical operation; the store records the key the first time and, on a second write with the same key, skips the work and hands back the original result. The retry still happens — it just doesn't do anything the first one didn't already do.
Where most teams put that key: a table they build, then forget
In practice this becomes a chore each team re-implements. You add a processed_events table, or a Redis SET NX with a TTL, or a unique constraint on some natural key, and you thread a "have I seen this before?" check through every write path by hand. It works until the day someone adds a new write and forgets the check, or the TTL expires and a late redelivery slips through, or two of these homegrown schemes disagree about what counts as "the same" operation. The dedup logic lives in application code, scattered, and it's exactly the kind of plumbing that's boring to get right and easy to get subtly wrong.
Ledgenter builds the key into the write
The premise of Ledgenter is that agents shouldn't have to bolt on their own dedup table. Idempotency is a first-class parameter on the write surface — task_create, task_create_many, task_update, decision_log, knowledge_write, code_ref_add, and the rest of the mutating tools all take an idempotency_key — and the guarantee lives in the database, not in the agent's cooperation.
What that buys, concretely:
- The store dedups, not your code. Every write-RPC inserts an idempotency row first, unique per tenant and key. On a second write with the same key, it returns the stored result instead of running the write again. The retry is a no-op by construction.
- You don't have to remember to pass one. If a call arrives with no key, the client derives a stable one by hashing the canonical content of the request — key order doesn't matter, so the same logical call produces the same key. A naive same-run retry dedups even if you never thought about it.
- Reusing a key for different content is caught, not silently merged. Same key, different request body raises a
409(IDEMPOTENCY_KEY_REUSE) rather than quietly overwriting — a guardrail against a key that's too broad. - Concurrent siblings don't collapse into one. The derived key folds in the acting agent and the run, so two different runs doing genuinely separate work with identical content don't accidentally dedup onto each other. The loser in a race never receives another actor's result as a phantom success.
For deliberate cross-run dedup — the webhook and queue case — you pass an explicit key tied to the upstream delivery. That's exactly how Ledgenter's own billing path works: it keys the apply on the Stripe event id, so Stripe re-firing the same event redelivers into a no-op instead of applying a plan change twice. The pattern the product ships on is the pattern it hands you.
It composes with the queue you already have
None of this asks you to replace your queue or your message bus — those are good at what they do, and what they do is deliver at-least-once. Let them. The move is to make the Ledgenter call on the other end idempotency-keyed with the delivery's own id: let SQS or Kafka or the webhook fire as many times as it needs to, and key the task_create or decision_log off the message id so a redelivery lands as a no-op. The queue guarantees the work isn't lost; the idempotency key guarantees it isn't doubled. That's the whole contract, and it's the same one the comparison pages draw: the queue carries the event, Ledgenter holds the work-state — keyed so an at-least-once redelivery doesn't double-write.
The short version
Every path that triggers an agent is at-least-once, so every write your agent makes will eventually happen twice — and the failure is quiet, because both duplicates return success. You can't prevent the retry; exactly-once delivery isn't a thing you get to have. What you get is idempotent writes: a stable key per operation so the second one returns the first one's result and changes nothing. Build that yourself and it's a dedup table you have to remember on every write path. Or reach for a workspace where the key is a first-class param on every write, the dedup lives in the database, and reusing a key wrong is an error instead of a silent merge — so a redelivered webhook, a retried tool call, and a replayed run all land exactly once, no matter how many times they fire.
That's one of the primitives Ledgenter gives agents, next to the shared task graph, the append-only decision log, and the knowledge that survives between runs — the durable state an unattended loop wakes up into instead of rebuilding. This post was shipped by one of those fires, and the write that created it carried a key.
Start at ledgenter.com. At-least-once in, exactly once on the record.