<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Distributed Notes]]></title><description><![CDATA[Distributed Notes]]></description><link>https://distributed-notes.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 11:43:24 GMT</lastBuildDate><atom:link href="https://distributed-notes.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Idempotency Keys: The Small Design Choice That Saves Incident Reviews]]></title><description><![CDATA[A payment endpoint retried once at the wrong moment can cost more than the outage that caused the retry. We learned that the hard way last year, and the…]]></description><link>https://distributed-notes.hashnode.dev/idempotency-keys-the-small-design-choice-that-saves-incident-reviews</link><guid isPermaLink="true">https://distributed-notes.hashnode.dev/idempotency-keys-the-small-design-choice-that-saves-incident-reviews</guid><category><![CDATA[backend]]></category><category><![CDATA[distributed systems]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Tobias Lang]]></dc:creator><pubDate>Wed, 28 Oct 2026 05:14:48 GMT</pubDate><content:encoded><![CDATA[<p>A payment endpoint retried once at the wrong moment can cost more than the outage that caused the retry.</p>
<p>We learned that the hard way last year, and the fix was not a new service. It was one header and one table.</p>
<h2>The failure</h2>
<p>Our client retried a POST after a 504. The server had already committed the charge. Two charges, one order.</p>
<p>The retry logic was correct for reads. For writes it was a coin flip, because the endpoint had no way to tell "this is the same request" from "this is a new one".</p>
<h2>The fix</h2>
<p>Every write now carries a client-generated key:</p>
<pre><code class="language-http">POST /charges
Idempotency-Key: 6f1c2a9e-8b3d-4f71-9c2a-...
</code></pre>
<p>The server stores the key together with the response body, in the same transaction that does the work:</p>
<pre><code class="language-sql">INSERT INTO idempotency_keys (key, request_hash, response_body, created_at)
VALUES ($1, $2, $3, now())
ON CONFLICT (key) DO NOTHING;
</code></pre>
<p>If the insert conflicts, we return the stored response instead of charging again. Same key, same answer, no second write.</p>
<h2>What surprised me</h2>
<p>The hard part was not the storage. It was expiry. Keys need to live longer than any client retry window, and longer than most people expect. We picked 24 hours after watching a mobile client retry a request eleven times across a subway ride.</p>
<p>We also had to hash the request body. The same key with a different body is a client bug, and silently returning a cached success hides exactly the bug you want to see.</p>
<h2>The takeaway</h2>
<p>Idempotency is a contract, not a feature. Once clients know it exists, they retry more freely, which is precisely what you want during an incident: the retry stops being a gamble.</p>
]]></content:encoded></item></channel></rss>