<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss version="2.0"><channel><title>Hacker News</title><link>https://news.ycombinator.com/</link><description>Links for the intellectually curious, ranked by readers.</description><item><title>Idempotency Is Easy Until the Second Request Is Different</title><link>https://blog.dochia.dev/blog/idempotency/</link><pubDate>Thu, 07 May 2026 11:02:28 +0000</pubDate><comments>https://news.ycombinator.com/item?id=48047930</comments><description>&lt;a href="https://news.ycombinator.com/item?id=48047930"&gt;Comments&lt;/a&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="prose prose-lg max-w-none" style="max-width: 100%;" data-astro-cid-bvzihdzo="" morss_own_score="3.0" morss_score="664.0"&gt; &lt;hr&gt;
&lt;p&gt;People talk about idempotency like it is a solved problem:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Put an &lt;code&gt;Idempotency-Key&lt;/code&gt; on the request. Store the response. Replay it on retry.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And yes, that is doable. For the happy path, it is even fairly small.&lt;/p&gt;
&lt;p&gt;The client sends:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; /payments&lt;/span&gt;
&lt;span&gt;Idempotency-Key&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; abc-123&lt;/span&gt;
&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; application/json&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "merchantReference"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"invoice-7781"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server checks whether it has seen &lt;code&gt;abc-123&lt;/code&gt;. If not, it creates the payment. If yes, it returns the previous
response.&lt;/p&gt;
&lt;p&gt;That version survives the demo.&lt;/p&gt;
&lt;p&gt;The part I contest is that this is the hard part. It is not. The hard part starts with the second request, because the
second request is not always a clean replay of the first one.&lt;/p&gt;
&lt;p&gt;Maybe it is a completed replay. Fine. Return the stored result.&lt;/p&gt;
&lt;p&gt;Maybe it arrives while the first request is still running. Now your idempotency layer is part of your concurrency
control.&lt;/p&gt;
&lt;p&gt;Maybe the first request created a local payment but crashed before publishing an event. Now the local row and the
external side effects are out of step.&lt;/p&gt;
&lt;p&gt;Maybe the first request called a payment provider, the provider accepted it, and your process died before recording the
result. Now your database cannot infer whether money moved.&lt;/p&gt;
&lt;p&gt;Or maybe the second request has the same key and different content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"100.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "merchantReference"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"invoice-7781"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same key. Different amount.&lt;/p&gt;
&lt;p&gt;This is the case that makes idempotency interesting. Is it a retry? Is it a client bug? Is it a new operation? Should
the server replay the old response, reject the request, or treat &lt;code&gt;(key + content)&lt;/code&gt; as a new identity?&lt;/p&gt;
&lt;p&gt;You can pick any of those policies if you document it clearly. But the server should have an opinion. Not necessarily my
opinion, but a clear one.&lt;/p&gt;
&lt;p&gt;My bias for side-effecting APIs is: same scoped key plus different canonical command should be a hard error. It catches
client bugs early. A client that believes it is safely retrying a 10 EUR payment should not have the server silently
interpret the second request as something else.&lt;/p&gt;
&lt;p&gt;The cases that matter are the ones a replay cache does not explain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;completed replay&lt;/li&gt;
&lt;li&gt;concurrent retry&lt;/li&gt;
&lt;li&gt;partial local success&lt;/li&gt;
&lt;li&gt;downstream unknown state&lt;/li&gt;
&lt;li&gt;same key with a different canonical command&lt;/li&gt;
&lt;li&gt;duplicate operation without a key&lt;/li&gt;
&lt;li&gt;retry after expiry&lt;/li&gt;
&lt;li&gt;retry after deploy, schema change, service hop, or region failover&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your design only handles completed same-command retries, it is a replay cache. That might be enough for some
endpoints. But it is not the whole problem.&lt;/p&gt;
&lt;h2&gt;Idempotency is about the effect&lt;/h2&gt;
&lt;p&gt;An operation is idempotent if applying it once or many times has the same intended effect.&lt;/p&gt;
&lt;p&gt;That definition is simple. The word doing all the work is “effect”.&lt;/p&gt;
&lt;p&gt;HTTP gives you method-level semantics. A &lt;code&gt;PUT /users/123/email&lt;/code&gt; can be idempotent if sending the same representation
repeatedly leaves the resource in the same state. A &lt;code&gt;DELETE /sessions/456&lt;/code&gt; can be idempotent if deleting an
already-deleted session still means “session does not exist”. Repeating the &lt;code&gt;DELETE&lt;/code&gt; might return &lt;code&gt;404&lt;/code&gt;; the effect can
still be idempotent.&lt;/p&gt;
&lt;p&gt;But your handler can still produce repeated side effects the business cares about: duplicate audit records, duplicate
domain events, duplicate emails, duplicate provider calls, or duplicate metrics that affect billing or fraud logic.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;POST&lt;/code&gt; is usually not idempotent by default, but it can be made idempotent if the server stores and enforces the right
behavior. The key identifies a claimed operation. It does not define request equivalence, replay policy, or downstream
deduplication.&lt;/p&gt;
&lt;p&gt;A uniqueness constraint can prevent one class of duplicate. It does not, by itself, give the client a correct retry
result.&lt;/p&gt;
&lt;p&gt;For example, &lt;code&gt;unique(account_id, merchant_reference)&lt;/code&gt; might prevent two payment rows, but if the retry gets a generic
&lt;code&gt;500&lt;/code&gt;, the client still does not know whether the payment succeeded. If the row exists but the response is different, or
the event is published twice, or the ledger entry is duplicated, the operation is not idempotent in the way the caller
cares about.&lt;/p&gt;
&lt;h2&gt;What you need to remember&lt;/h2&gt;
&lt;p&gt;For &lt;code&gt;POST /payments&lt;/code&gt;, the durable idempotency record needs to answer three questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Who owns this key?&lt;/li&gt;
&lt;li&gt;What did the first command mean?&lt;/li&gt;
&lt;li&gt;What outcome can be replayed?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In PostgreSQL-ish SQL, a minimal table might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt; table&lt;/span&gt;&lt;span&gt; idempotency_requests&lt;/span&gt;
&lt;span&gt;(&lt;/span&gt;
&lt;span&gt;    tenant_id       &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;        not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    operation_name  &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;        not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    idempotency_key &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;        not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    request_hash    &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;        not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    status&lt;/span&gt;&lt;span&gt;          text&lt;/span&gt;&lt;span&gt;        not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    response_status &lt;/span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    response_body   jsonb,&lt;/span&gt;
&lt;span&gt;    resource_type   &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    resource_id     &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    error_code      &lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    created_at      &lt;/span&gt;&lt;span&gt;timestamptz&lt;/span&gt;&lt;span&gt; not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    updated_at      &lt;/span&gt;&lt;span&gt;timestamptz&lt;/span&gt;&lt;span&gt; not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    expires_at      &lt;/span&gt;&lt;span&gt;timestamptz&lt;/span&gt;&lt;span&gt; not null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    locked_until    &lt;/span&gt;&lt;span&gt;timestamptz&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;    primary key&lt;/span&gt;&lt;span&gt; (tenant_id, operation_name, idempotency_key)&lt;/span&gt;
&lt;span&gt;);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key is not globally unique unless you deliberately make it global. Usually it should not be. A broken client
generating &lt;code&gt;abc-123&lt;/code&gt; should only collide with itself, not with another tenant.&lt;/p&gt;
&lt;p&gt;Scope might be tenant, user, account, merchant, API client, or some combination. Pick it deliberately.&lt;/p&gt;
&lt;p&gt;The operation name prevents accidental reuse across different operations. A key used for &lt;code&gt;create_payment&lt;/code&gt; should not
automatically mean the same thing for &lt;code&gt;create_refund&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;request_hash&lt;/code&gt; is the server’s memory of the first command. Without it, same key plus different body becomes
ambiguous. You either replay the first response for a different command, or you execute a new operation under an old
key. Both are bad if the client thinks it is retrying.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IN_PROGRESS&lt;/code&gt; is not an internal detail. A retry can arrive while the first request still owns execution.&lt;/p&gt;
&lt;p&gt;The behavior needs to be explicit:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Existing record&lt;/th&gt;&lt;th&gt;Same canonical command?&lt;/th&gt;&lt;th&gt;Suggested behavior&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;none&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;insert &lt;code&gt;IN_PROGRESS&lt;/code&gt; and execute&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;COMPLETED&lt;/code&gt;&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;replay stored response or documented equivalent&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;any existing record&lt;/td&gt;&lt;td&gt;no&lt;/td&gt;&lt;td&gt;reject with idempotency conflict&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;IN_PROGRESS&lt;/code&gt;, fresh&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;wait, return &lt;code&gt;202&lt;/code&gt;, or return &lt;code&gt;409&lt;/code&gt; + &lt;code&gt;Retry-After&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;IN_PROGRESS&lt;/code&gt;, stale&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;recover ownership; do not blindly execute again&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;FAILED_REPLAYABLE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;replay stored failure&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;FAILED_RETRYABLE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;allow retry according to policy&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;UNKNOWN_REQUIRES_RECOVERY&lt;/code&gt;&lt;/td&gt;&lt;td&gt;yes&lt;/td&gt;&lt;td&gt;trigger reconciliation or return pending/recovery status&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;expired/deleted&lt;/td&gt;&lt;td&gt;unknown&lt;/td&gt;&lt;td&gt;follow documented expiry behavior&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The response fields exist because idempotency is not just about preventing duplicate writes. The client needs an answer.&lt;/p&gt;
&lt;p&gt;You can store the full response body, or store a reference to the created resource and reconstruct the response. Both
choices are annoying in different ways.&lt;/p&gt;
&lt;p&gt;Storing full responses gives faithful replay. It can also retain PII, signed URLs, one-time tokens, cardholder-related
data, or fields you never intended to keep in a retry table.&lt;/p&gt;
&lt;p&gt;Reconstructing from a resource reference saves space, but it can return a different representation if the resource
changed after creation.&lt;/p&gt;
&lt;p&gt;This is a contract decision. “Replay the creation response” and “return the current payment” are both valid API designs.
They are not the same design.&lt;/p&gt;
&lt;h2&gt;Same key, different command&lt;/h2&gt;
&lt;p&gt;This is the bug the idempotency layer should catch loudly.&lt;/p&gt;
&lt;p&gt;First request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "merchantReference"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"invoice-7781"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"100.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "merchantReference"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"invoice-7781"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same &lt;code&gt;Idempotency-Key: abc-123&lt;/code&gt;. Different amount.&lt;/p&gt;
&lt;p&gt;Returning the original response anyway is simple. It also hides a serious client bug. The client asked for a 100 EUR
payment and got back a 10 EUR payment. If the caller does not compare the response carefully, it may believe the 100 EUR
payment succeeded.&lt;/p&gt;
&lt;p&gt;That is not idempotency. That is reinterpretation.&lt;/p&gt;
&lt;p&gt;For side-effecting APIs, a scoped key reused with a different canonical command should be a hard error, regardless of
whether the first operation completed, failed, or is still running.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;HTTP&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;1.1&lt;/span&gt;&lt;span&gt; 409&lt;/span&gt;&lt;span&gt; Conflict&lt;/span&gt;
&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; application/json&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "errorCode"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "message"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"This idempotency key was already used with a different request."&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;409 Conflict&lt;/code&gt; is a defensible default because the request conflicts with the server’s remembered meaning for that
scoped key. Some APIs use &lt;code&gt;400&lt;/code&gt; or &lt;code&gt;422&lt;/code&gt;; the important part is a stable machine-readable error and no silent replay for
a different command.&lt;/p&gt;
&lt;p&gt;A common client bug looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;bad:&lt;/span&gt;
&lt;span&gt;  idempotencyKey = cartId&lt;/span&gt;

&lt;span&gt;POST /payments amount=10.00 key=cart_123&lt;/span&gt;
&lt;span&gt;POST /payments amount=15.00 key=cart_123&lt;/span&gt;

&lt;span&gt;better:&lt;/span&gt;
&lt;span&gt;  idempotencyKey = paymentAttemptId&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server should not guess which payment the cart key was supposed to represent.&lt;/p&gt;
&lt;p&gt;You can design an API where &lt;code&gt;(key + content hash)&lt;/code&gt; defines the operation identity. That is a valid policy. But then the
key is no longer an idempotency key in the usual retry sense. It is part of a composite operation identifier. That needs
to be obvious to the client.&lt;/p&gt;
&lt;p&gt;The dangerous version is the middle ground, where the client thinks it is safely retrying one operation and the server
silently interprets the second request as another.&lt;/p&gt;
&lt;h2&gt;Hash the command, not the bytes&lt;/h2&gt;
&lt;p&gt;Raw byte comparison is usually too strict for JSON APIs. These two bodies should normally be equivalent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Field order and whitespace should not matter.&lt;/p&gt;
&lt;p&gt;Defaults are less obvious:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;versus:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "channel"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"web"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;channel: "web"&lt;/code&gt; is the server default, are these the same logical command? Maybe. Decide before hashing.&lt;/p&gt;
&lt;p&gt;Unknown fields are another trap. Suppose your API ignores unknown JSON fields. If the first request includes
&lt;code&gt;"foo": "bar"&lt;/code&gt; and the second does not, do you consider them the same? If unknown fields are truly ignored, perhaps yes.
If they might become meaningful after a deploy, perhaps no.&lt;/p&gt;
&lt;p&gt;The practical rule is: hash the validated command, not the raw HTTP body.&lt;/p&gt;
&lt;p&gt;A reasonable flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse the request into a versioned request DTO or command.&lt;/li&gt;
&lt;li&gt;Normalize values your API treats as equivalent: amounts, enum casing, default fields, timestamp precision.&lt;/li&gt;
&lt;li&gt;Exclude transport-only metadata.&lt;/li&gt;
&lt;li&gt;Include path parameters and operation name.&lt;/li&gt;
&lt;li&gt;Include semantic headers if they affect the operation, such as API version.&lt;/li&gt;
&lt;li&gt;If a header only affects response shape, such as &lt;code&gt;Prefer: return=minimal&lt;/code&gt;, decide whether it belongs in the command
hash, the replay contract, or neither.&lt;/li&gt;
&lt;li&gt;Exclude &lt;code&gt;Authorization&lt;/code&gt; and the idempotency key itself.&lt;/li&gt;
&lt;li&gt;Serialize canonically.&lt;/li&gt;
&lt;li&gt;Hash with a stable algorithm.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the payment example, the fingerprint might include:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;operation: create_payment&lt;/span&gt;
&lt;span&gt;accountId: acc_1&lt;/span&gt;
&lt;span&gt;amount: 10.00&lt;/span&gt;
&lt;span&gt;currency: EUR&lt;/span&gt;
&lt;span&gt;merchantReference: invoice-7781&lt;/span&gt;
&lt;span&gt;channel: web&lt;/span&gt;
&lt;span&gt;apiVersion: 2026-05-01&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be careful with amounts, timestamps, generated defaults, locale-sensitive formatting, and fields added during deploys.
The request hash is a contract. If you change how it is computed, old retries can start looking different.&lt;/p&gt;
&lt;h2&gt;The first insert decides who owns execution&lt;/h2&gt;
&lt;p&gt;Two identical requests hit two API instances at nearly the same time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; /payments&lt;/span&gt;
&lt;span&gt;Idempotency-Key&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; abc-123&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same canonical command. Same tenant. Same endpoint.&lt;/p&gt;
&lt;p&gt;This implementation is broken even if every single-threaded test passes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;existing = find_by_key(key)&lt;/span&gt;
&lt;span&gt;if existing does not exist:&lt;/span&gt;
&lt;span&gt;    create_payment()&lt;/span&gt;
&lt;span&gt;    insert_idempotency_record()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both requests can observe no existing row. Both can execute the side effect.&lt;/p&gt;
&lt;p&gt;If there is no atomic insert or unique constraint on the scoped key, two instances can both decide they own execution.&lt;/p&gt;
&lt;p&gt;The insert-first shape is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;insert into&lt;/span&gt;&lt;span&gt; idempotency_requests (tenant_id,&lt;/span&gt;
&lt;span&gt;                                  operation_name,&lt;/span&gt;
&lt;span&gt;                                  idempotency_key,&lt;/span&gt;
&lt;span&gt;                                  request_hash,&lt;/span&gt;
&lt;span&gt;                                  status&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;                                  created_at,&lt;/span&gt;
&lt;span&gt;                                  updated_at,&lt;/span&gt;
&lt;span&gt;                                  expires_at,&lt;/span&gt;
&lt;span&gt;                                  locked_until)&lt;/span&gt;
&lt;span&gt;values&lt;/span&gt;&lt;span&gt; (:tenant_id,&lt;/span&gt;
&lt;span&gt;        'create_payment'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;        :idempotency_key,&lt;/span&gt;
&lt;span&gt;        :request_hash,&lt;/span&gt;
&lt;span&gt;        'IN_PROGRESS'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;        now&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
&lt;span&gt;        now&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;
&lt;span&gt;        now&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; interval &lt;/span&gt;&lt;span&gt;'24 hours'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;        now&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; interval &lt;/span&gt;&lt;span&gt;'30 seconds'&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt; conflict do nothing;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact syntax is database-specific. The important property is atomic ownership acquisition for
&lt;code&gt;(tenant_id, operation_name, idempotency_key)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;if rows_inserted == 1:&lt;/span&gt;
&lt;span&gt;    this request owns execution&lt;/span&gt;
&lt;span&gt;else:&lt;/span&gt;
&lt;span&gt;    existing = load idempotency row&lt;/span&gt;

&lt;span&gt;    if existing.request_hash != request_hash:&lt;/span&gt;
&lt;span&gt;        return 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST&lt;/span&gt;

&lt;span&gt;    if existing.status == COMPLETED:&lt;/span&gt;
&lt;span&gt;        return replay(existing.response_status, existing.response_body)&lt;/span&gt;

&lt;span&gt;    if existing.status == IN_PROGRESS and existing.locked_until &amp;gt; now():&lt;/span&gt;
&lt;span&gt;        return 202 or 409 + Retry-After&lt;/span&gt;

&lt;span&gt;    if existing.status == IN_PROGRESS and existing.locked_until &amp;lt;= now():&lt;/span&gt;
&lt;span&gt;        attempt recovery ownership&lt;/span&gt;
&lt;span&gt;        # this must be atomic too&lt;/span&gt;

&lt;span&gt;    if existing.status == UNKNOWN_REQUIRES_RECOVERY:&lt;/span&gt;
&lt;span&gt;        trigger reconciliation or return pending/recovery response&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Recovery ownership has to be acquired atomically too. Otherwise two retries can both decide the old owner is dead and
both start recovery.&lt;/p&gt;
&lt;p&gt;In the simple local case, the owner can create the payment and complete the idempotency record in one transaction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;begin transaction&lt;/span&gt;

&lt;span&gt;insert idempotency row as IN_PROGRESS&lt;/span&gt;
&lt;span&gt;insert payment row pay_789&lt;/span&gt;
&lt;span&gt;insert outbox event PaymentCreated(pay_789)&lt;/span&gt;
&lt;span&gt;update idempotency row:&lt;/span&gt;
&lt;span&gt;  status = COMPLETED&lt;/span&gt;
&lt;span&gt;  resource_type = payment&lt;/span&gt;
&lt;span&gt;  resource_id = pay_789&lt;/span&gt;
&lt;span&gt;  response_status = 201&lt;/span&gt;
&lt;span&gt;  response_body = {...}&lt;/span&gt;

&lt;span&gt;commit&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the nice version: one database transaction covers the idempotency row, the business row, and the outbox event.&lt;/p&gt;
&lt;p&gt;External side effects change the shape. Holding a database transaction open while calling a provider is usually a bad
idea. Committing before the provider call means your local state may say &lt;code&gt;IN_PROGRESS&lt;/code&gt; while execution continues outside
the transaction. If the process crashes there, a retry has to recover. This is where you need an operation state machine
and a recovery worker, not just a request table.&lt;/p&gt;
&lt;p&gt;Redis &lt;code&gt;SET NX EX&lt;/code&gt; is often proposed as the whole solution. At best, it is an execution guard:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;SET idempotency:tenant_1:create_payment:abc-123 value NX EX 30&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can reduce duplicate concurrent execution. It is not durable memory of the operation outcome. If the Redis lock
expires while the provider call is still running, another request can enter. If the process dies after the provider
succeeds but before storing the response, the lock does not help the retry know what happened. Redis locks also need
fencing or durable ownership if they protect downstream resources.&lt;/p&gt;
&lt;p&gt;Redis can be useful. It is not a substitute for remembering the operation outcome.&lt;/p&gt;
&lt;h2&gt;The provider timeout is where your guarantee ends&lt;/h2&gt;
&lt;p&gt;The failure path that matters is not exotic:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;API receives &lt;code&gt;POST /payments&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It inserts an idempotency row as &lt;code&gt;IN_PROGRESS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It creates local payment &lt;code&gt;pay_789&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It calls a downstream payment provider.&lt;/li&gt;
&lt;li&gt;The provider receives the request and succeeds.&lt;/li&gt;
&lt;li&gt;The API times out, crashes, or loses the provider response.&lt;/li&gt;
&lt;li&gt;The client retries with the same key.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the provider received your request and your process died before recording the result, your database cannot infer
whether money moved.&lt;/p&gt;
&lt;p&gt;A local state machine might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;RECEIVED&lt;/span&gt;
&lt;span&gt;LOCAL_PAYMENT_CREATED&lt;/span&gt;
&lt;span&gt;PROVIDER_REQUEST_SENT&lt;/span&gt;
&lt;span&gt;PROVIDER_CONFIRMED&lt;/span&gt;
&lt;span&gt;COMPLETED&lt;/span&gt;
&lt;span&gt;UNKNOWN_REQUIRES_RECOVERY&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The retry behavior depends on the state.&lt;/p&gt;
&lt;p&gt;If the retry finds &lt;code&gt;COMPLETED&lt;/code&gt;, replay.&lt;/p&gt;
&lt;p&gt;If it finds a fresh &lt;code&gt;PROVIDER_REQUEST_SENT&lt;/code&gt;, return &lt;code&gt;202 Accepted&lt;/code&gt;, &lt;code&gt;409 Conflict&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt;, or block briefly
and wait for completion. Pick one behavior and document it. Clients need to know whether to retry, poll, or wait.&lt;/p&gt;
&lt;p&gt;If it finds stale &lt;code&gt;PROVIDER_REQUEST_SENT&lt;/code&gt;, do not create &lt;code&gt;pay_790&lt;/code&gt;. Do not call the provider with a new identity.
Recover using the stable downstream operation ID:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;payment id: pay_789&lt;/span&gt;
&lt;span&gt;provider idempotency key: provider_payment_pay_789&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A recovery worker or retrying request can then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;acquire recovery ownership for &lt;code&gt;pay_789&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;query the provider by &lt;code&gt;provider_payment_pay_789&lt;/code&gt;, if the provider supports it&lt;/li&gt;
&lt;li&gt;if confirmed, mark the provider operation confirmed&lt;/li&gt;
&lt;li&gt;mark the idempotency record &lt;code&gt;COMPLETED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;store or reconstruct the response&lt;/li&gt;
&lt;li&gt;replay the response or return a documented final status&lt;/li&gt;
&lt;li&gt;if the provider cannot answer, mark &lt;code&gt;UNKNOWN_REQUIRES_RECOVERY&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the provider has no idempotency key and no query API, your system has an operational gap. You may still choose to
accept it, but the local idempotency table is not protecting the external effect. It only prevents duplicate local
request handling.&lt;/p&gt;
&lt;p&gt;For payment-like operations, the client’s idempotency key is often not the exact key sent downstream. The downstream
call needs a stable identity that survives retries, crashes, and reconciliation. Otherwise the second local attempt is
just a second provider attempt.&lt;/p&gt;
&lt;p&gt;I would avoid &lt;code&gt;425 Too Early&lt;/code&gt; unless your API already has a specific reason to use it. Most clients will not handle it
specially. &lt;code&gt;202 Accepted&lt;/code&gt;, &lt;code&gt;409 Conflict&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt;, or an operation-status endpoint are easier to explain.&lt;/p&gt;
&lt;h2&gt;Replay is a contract, not a convenience&lt;/h2&gt;
&lt;p&gt;For a completed idempotent request, replaying the same status and body is the least surprising behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;HTTP&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;1.1&lt;/span&gt;&lt;span&gt; 201&lt;/span&gt;&lt;span&gt; Created&lt;/span&gt;
&lt;span&gt;Idempotent-Replayed&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;
&lt;span&gt;Content-Type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; application/json&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "paymentId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"pay_789"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "status"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"PENDING"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "merchantReference"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"invoice-7781"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A custom response header such as &lt;code&gt;Idempotent-Replayed: true&lt;/code&gt; can help debugging. I would not make clients depend on it.&lt;/p&gt;
&lt;p&gt;Reconstructing responses from current resource state is tempting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;load payment pay_789&lt;/span&gt;
&lt;span&gt;return current representation&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But suppose the first response was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "paymentId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"pay_789"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "status"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"PENDING"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and the retry happens ten minutes later, after settlement:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "paymentId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"pay_789"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "status"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"SETTLED"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That may be useful, but it is not a replay. It is a fresh read of the resource. If your API contract says idempotent
retries return the original creation result, you need to store enough to do that.&lt;/p&gt;
&lt;p&gt;Schema changes make this worse.&lt;/p&gt;
&lt;p&gt;Version 2 response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "paymentId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"pay_789"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "status"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"PENDING"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Version 3 response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "id"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"pay_789"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "state"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"PENDING"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "createdAt"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"2026-05-07T10:00:00Z"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If a generated client retries after a deploy, should it receive the stored v2 response or a reconstructed v3 response?
Both can be defensible. They are different contracts.&lt;/p&gt;
&lt;p&gt;A common compromise is to store:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;resource_type = payment&lt;/span&gt;
&lt;span&gt;resource_id = pay_789&lt;/span&gt;
&lt;span&gt;response_status = 201&lt;/span&gt;
&lt;span&gt;response_schema_version = v2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and store full response bodies only for endpoints where exact replay matters. If you store bodies, treat the idempotency
table like sensitive data storage, not like a harmless cache.&lt;/p&gt;
&lt;h2&gt;Your queue consumer has the same bug&lt;/h2&gt;
&lt;p&gt;HTTP gets most of the attention because the header is visible. A lot of duplicate side effects happen later, in
consumers, outbox publishers, inbox processors, and notification workers.&lt;/p&gt;
&lt;p&gt;Suppose the payment service publishes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "eventId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"evt_100"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "type"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"PaymentCreated"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "paymentId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"pay_789"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "accountId"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"acc_1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A consumer receives it twice. That should not send two emails, create two ledger entries, or notify a provider twice.&lt;/p&gt;
&lt;p&gt;The dedupe key might be the event ID, message ID, operation ID, aggregate ID plus version, or a business key such as
&lt;code&gt;ledger_payment_pay_789&lt;/code&gt;. The right answer depends on the side effect.&lt;/p&gt;
&lt;p&gt;A consumer inbox table might be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;consumer_inbox&lt;/span&gt;

&lt;span&gt;- consumer_name&lt;/span&gt;
&lt;span&gt;- message_id&lt;/span&gt;
&lt;span&gt;- status&lt;/span&gt;
&lt;span&gt;- processed_at&lt;/span&gt;
&lt;span&gt;- error_code&lt;/span&gt;

&lt;span&gt;unique(consumer_name, message_id)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But marking the message processed is not trivial.&lt;/p&gt;
&lt;p&gt;If you mark it processed before sending the email and then crash, the retry skips the email forever. If you send the
email before marking it processed and then crash, the retry may send it again. The usual answer is to make the side
effect durable before sending it: insert an email notification row with a unique key, then have a sender process that
row.&lt;/p&gt;
&lt;p&gt;Ledger entries often have a natural idempotency key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;unique(ledger_entry_type, source_payment_id)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Processing &lt;code&gt;PaymentCreated(pay_789)&lt;/code&gt; twice attempts to create the same ledger entry twice, and the second attempt
resolves to the existing entry.&lt;/p&gt;
&lt;p&gt;Many production queue integrations are effectively at-least-once from the consumer’s point of view. Even when the broker
advertises stronger delivery semantics, your business side effects still need deduplication. Exactly-once delivery is
not exactly-once business effect. The latter usually comes from durable operation IDs, unique constraints, idempotent
writes, and recovery paths.&lt;/p&gt;
&lt;p&gt;Outbox/inbox is the usual shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;same database transaction:&lt;/span&gt;
&lt;span&gt;  insert payment row pay_789&lt;/span&gt;
&lt;span&gt;  insert outbox event PaymentCreated(pay_789)&lt;/span&gt;

&lt;span&gt;publisher:&lt;/span&gt;
&lt;span&gt;  reads unpublished outbox event&lt;/span&gt;
&lt;span&gt;  publishes event with eventId&lt;/span&gt;
&lt;span&gt;  marks outbox event published&lt;/span&gt;

&lt;span&gt;consumer:&lt;/span&gt;
&lt;span&gt;  deduplicates by eventId or business operation key&lt;/span&gt;
&lt;span&gt;  writes side effect behind a unique constraint&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Idempotency prevents some duplicates. It does not remove poison messages, broken providers, dead-letter handling, or
recovery work.&lt;/p&gt;
&lt;h2&gt;Expiry is part of the API contract&lt;/h2&gt;
&lt;p&gt;Idempotency records cannot usually live forever.&lt;/p&gt;
&lt;p&gt;If the server promises a 24-hour idempotency window, then a retry after 25 hours may create a new operation. That may be
acceptable. It may also surprise clients that queue retries for days. The replay window is a product/API decision, not
just a cleanup setting.&lt;/p&gt;
&lt;p&gt;A completed record might be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;created_at: 2026-05-07T10:00:00Z&lt;/span&gt;
&lt;span&gt;expires_at: 2026-05-08T10:00:00Z&lt;/span&gt;
&lt;span&gt;status: COMPLETED&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After expiry, you might delete the response body but retain metadata longer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;idempotency_key&lt;/span&gt;
&lt;span&gt;scope&lt;/span&gt;
&lt;span&gt;operation_name&lt;/span&gt;
&lt;span&gt;request_hash&lt;/span&gt;
&lt;span&gt;resource_id&lt;/span&gt;
&lt;span&gt;created_at&lt;/span&gt;
&lt;span&gt;expires_at&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That supports diagnostics without retaining sensitive response payloads.&lt;/p&gt;
&lt;p&gt;Stale &lt;code&gt;IN_PROGRESS&lt;/code&gt; needs separate handling:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;status: IN_PROGRESS&lt;/span&gt;
&lt;span&gt;resource_id: pay_789&lt;/span&gt;
&lt;span&gt;updated_at: 2026-05-07T10:00:00Z&lt;/span&gt;
&lt;span&gt;locked_until: 2026-05-07T10:00:30Z&lt;/span&gt;
&lt;span&gt;now: 2026-05-07T10:45:00Z&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A retry that sees this should not blindly execute again. It should acquire recovery ownership, inspect &lt;code&gt;pay_789&lt;/code&gt;, query
downstream if needed, and move the operation to &lt;code&gt;COMPLETED&lt;/code&gt;, &lt;code&gt;FAILED_RETRYABLE&lt;/code&gt;, or &lt;code&gt;UNKNOWN_REQUIRES_RECOVERY&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cleanup jobs should not remove in-progress records just because they are old. An old in-progress row may mean a stuck
worker, a process crash, or an operation waiting for reconciliation. Deleting it can allow a duplicate side effect.&lt;/p&gt;
&lt;p&gt;Bad cleanup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;delete&lt;/span&gt;
&lt;span&gt;from&lt;/span&gt;&lt;span&gt; idempotency_requests&lt;/span&gt;
&lt;span&gt;where&lt;/span&gt;&lt;span&gt; expires_at &lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt; now&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Better options include deleting in small batches, partitioning by &lt;code&gt;expires_at&lt;/code&gt;, dropping old time partitions after the
replay window, and keeping separate retention policies for response bodies and metadata.&lt;/p&gt;
&lt;p&gt;Replay count is mostly capacity planning. Different-body reuse, stale &lt;code&gt;IN_PROGRESS&lt;/code&gt; rows, expired retries, and unknown
states are the metrics that find bugs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;idempotency.replay.count&lt;/span&gt;
&lt;span&gt;idempotency.conflict.different_request.count&lt;/span&gt;
&lt;span&gt;idempotency.in_progress.age.max&lt;/span&gt;
&lt;span&gt;idempotency.expired_retry.count&lt;/span&gt;
&lt;span&gt;idempotency.unknown_state.count&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Failure replay is a policy decision&lt;/h2&gt;
&lt;p&gt;The dangerous mistake is treating every failure as either “safe to retry” or “completed”.&lt;/p&gt;
&lt;p&gt;Pure syntactic validation failures usually do not need idempotency storage. If the JSON is malformed or a required field
is missing, repeating the request will fail again.&lt;/p&gt;
&lt;p&gt;Business rejections are different. If the decision depends on mutable state, such as balance, inventory, account status,
or fraud rules, decide whether the first decision is binding for that idempotency key or whether the client must retry
with a new key.&lt;/p&gt;
&lt;p&gt;A deterministic rejection might be replayable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "errorCode"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"INSUFFICIENT_FUNDS"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "message"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"The account has insufficient funds for this payment."&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But if the account balance changes five seconds later, replaying that rejection may or may not be what your API intends.&lt;/p&gt;
&lt;p&gt;Authentication failures should not create idempotency records. For authorization failures, be careful: a retry must
still resolve to the same scope/principal that created the original record. Do not let one caller use another caller’s
idempotency key to discover whether an operation happened. Whether later permission changes block replay of an already
completed authorized operation is a product and security decision.&lt;/p&gt;
&lt;p&gt;Rate limits usually should not be recorded as completed idempotent outcomes. A retry later might be allowed.&lt;/p&gt;
&lt;p&gt;Server error before side effects can often allow retry. Server error after side effects is dangerous. If you created the
payment but failed to serialize the response, the retry should not create another payment. If you called a provider and
lost the response, the retry needs recovery state, not optimism.&lt;/p&gt;
&lt;p&gt;A practical internal status set might be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;IN_PROGRESS&lt;/span&gt;
&lt;span&gt;COMPLETED&lt;/span&gt;
&lt;span&gt;FAILED_REPLAYABLE&lt;/span&gt;
&lt;span&gt;FAILED_RETRYABLE&lt;/span&gt;
&lt;span&gt;UNKNOWN_REQUIRES_RECOVERY&lt;/span&gt;
&lt;span&gt;EXPIRED&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not expose every internal state directly. But internally, pretending every failure is either “done” or “not done”
makes recovery harder.&lt;/p&gt;
&lt;h2&gt;When one transaction cannot cover the operation&lt;/h2&gt;
&lt;p&gt;The useful distinction is not monolith versus microservices. It is whether one durable transaction can cover the
operation.&lt;/p&gt;
&lt;p&gt;If one database transaction can cover the idempotency row, payment row, and outbox record, the local part is
straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;insert idempotency row&lt;/span&gt;
&lt;span&gt;insert payment row&lt;/span&gt;
&lt;span&gt;insert outbox event&lt;/span&gt;
&lt;span&gt;mark idempotency completed&lt;/span&gt;
&lt;span&gt;commit&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The publisher can retry outbox delivery. Consumers deduplicate by event ID or business operation key. The local write
path is much easier to reason about.&lt;/p&gt;
&lt;p&gt;When side effects cross boundaries, every boundary that can repeat work needs its own duplicate-suppression rule.&lt;/p&gt;
&lt;p&gt;An upstream API accepting &lt;code&gt;Idempotency-Key: abc-123&lt;/code&gt; can prevent duplicate HTTP payment creation requests at the edge.
It does not automatically prevent duplicate ledger entries, duplicate notifications, duplicate provider calls, or
duplicate read-model updates.&lt;/p&gt;
&lt;p&gt;A better model is to maintain stable operation identities:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;client idempotency key: abc-123&lt;/span&gt;
&lt;span&gt;payment operation id: payop_456&lt;/span&gt;
&lt;span&gt;payment id: pay_789&lt;/span&gt;
&lt;span&gt;ledger entry id: ledger_payment_pay_789&lt;/span&gt;
&lt;span&gt;email dedupe key: receipt_payment_pay_789&lt;/span&gt;
&lt;span&gt;provider idempotency key: provider_payment_pay_789&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The names do not matter. The point is that each side effect has a durable identity appropriate to that side effect.&lt;/p&gt;
&lt;p&gt;In active-active multi-region deployments, a region-local idempotency table only protects retries that land in the same
region. You either need to route all requests for the same scoped key to a home region, use a strongly consistent shared
store for idempotency records, or rely on downstream business constraints that survive cross-region races. Async
replication alone can allow two regions to accept the same key before either sees the other write.&lt;/p&gt;
&lt;p&gt;For high-throughput APIs, the idempotency table can become a hot path. Response bodies can become expensive. Cleanup can
compete with traffic. Partition by tenant, hash, or time if needed. Know your replay window. Do not make a global table
the bottleneck unless the duplicate harm justifies it.&lt;/p&gt;
&lt;h2&gt;When not to build a general idempotency layer&lt;/h2&gt;
&lt;p&gt;The cost is not the header. The cost is the durable memory and recovery behavior behind it.&lt;/p&gt;
&lt;p&gt;Do not build a payment-grade idempotency layer for an admin action where a duplicate is harmless and visible.&lt;/p&gt;
&lt;p&gt;For read-only operations, idempotency keys usually add noise.&lt;/p&gt;
&lt;p&gt;If a duplicate analytics event costs almost nothing and can be corrected downstream, a heavy idempotency table may be
the wrong trade.&lt;/p&gt;
&lt;p&gt;For some operations, a business key is better than a random key:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;unique(account_id, merchant_reference)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the business rule is “there can be only one payment per merchant reference per account,” that constraint catches
duplicates even when the client retries with a new random key by mistake. Random idempotency keys only help when the
client reuses the same key for retries.&lt;/p&gt;
&lt;p&gt;For other operations, change the resource model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;PUT&lt;/span&gt;&lt;span&gt; /accounts/acc_1/settings/default-currency&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repeating that request leaves the setting as EUR. You still need to think about side effects, but the operation shape is
helping you.&lt;/p&gt;
&lt;p&gt;Client-generated keys are useful when the client can identify a retry of the same operation. Properly generated random
keys are usually enough; timestamp-only keys, counters, and keys derived from sensitive data are not. Scope the key to
the caller and operation, for example &lt;code&gt;(tenant_id, operation_name, idempotency_key)&lt;/code&gt;, so a bad client only collides with
itself. If clients generate a new key on every attempt, you need a business key or a server-created operation resource.&lt;/p&gt;
&lt;p&gt;Use the amount of harm caused by duplicate side effects, the likelihood of retries, and the difficulty of detecting
duplicates after the fact to decide how much machinery you need.&lt;/p&gt;
&lt;p&gt;If duplicates move money, notify humans, call providers, consume scarce inventory, or corrupt accounting, spend the
design effort. If duplicates are harmless, rare, and easy to clean up, use a smaller mechanism.&lt;/p&gt;
&lt;h2&gt;Failure modes worth testing&lt;/h2&gt;
&lt;p&gt;Here are tests I would rather see than a dozen happy-path unit tests.&lt;/p&gt;
&lt;h3&gt;Same key, same canonical command, completed&lt;/h3&gt;
&lt;p&gt;First request creates the payment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;POST&lt;/span&gt;&lt;span&gt; /payments&lt;/span&gt;
&lt;span&gt;Idempotency-Key&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; abc-123&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;returns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;201 Created&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with &lt;code&gt;paymentId = pay_789&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Second request with the same canonical command and key returns the same stored result or documented equivalent. It does
not create &lt;code&gt;pay_790&lt;/code&gt;. It does not publish a second &lt;code&gt;PaymentCreated&lt;/code&gt; event.&lt;/p&gt;
&lt;h3&gt;Same key, different canonical command&lt;/h3&gt;
&lt;p&gt;First request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"10.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  "amount"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"100.00"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;  "currency"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"EUR"&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same key.&lt;/p&gt;
&lt;p&gt;Expected behavior: reject with a stable machine-readable idempotency conflict. Log and count it.&lt;/p&gt;
&lt;h3&gt;Two concurrent identical requests&lt;/h3&gt;
&lt;p&gt;Start two requests at the same time with the same key and same command.&lt;/p&gt;
&lt;p&gt;Expected behavior: one wins execution. The other sees &lt;code&gt;IN_PROGRESS&lt;/code&gt;, waits and replays, or returns a retry-later
response. The side effect executes once.&lt;/p&gt;
&lt;p&gt;If this test passes without a unique constraint or atomic insert, be suspicious of the test.&lt;/p&gt;
&lt;h3&gt;Timeout after downstream success&lt;/h3&gt;
&lt;p&gt;Simulate provider success and then crash before the client receives the response.&lt;/p&gt;
&lt;p&gt;Expected behavior: the retry should not call the provider with a new operation identity. It should find local completed
state, query provider idempotent state, or move into recovery.&lt;/p&gt;
&lt;h3&gt;Duplicate message from a queue&lt;/h3&gt;
&lt;p&gt;Deliver &lt;code&gt;PaymentCreated(pay_789)&lt;/code&gt; twice.&lt;/p&gt;
&lt;p&gt;Expected behavior: one ledger entry, one email notification, one provider notification. If the first attempt fails
halfway through, the retry should complete missing durable work without duplicating completed work.&lt;/p&gt;
&lt;h3&gt;Expired or stale state&lt;/h3&gt;
&lt;p&gt;Retry after the idempotency record expired. Retry while the record is stale &lt;code&gt;IN_PROGRESS&lt;/code&gt;. Retry after response schema
changed. Retry from another region if your deployment allows it.&lt;/p&gt;
&lt;p&gt;These are not exotic cases. They are the normal edges of retrying over networks.&lt;/p&gt;
&lt;h2&gt;Checklist before shipping&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Reject same scoped key plus different canonical command.&lt;/li&gt;
&lt;li&gt;Use a unique constraint or atomic insert on the scoped key.&lt;/li&gt;
&lt;li&gt;Hash the validated command, not raw JSON bytes.&lt;/li&gt;
&lt;li&gt;Treat &lt;code&gt;IN_PROGRESS&lt;/code&gt; as API-visible behavior.&lt;/li&gt;
&lt;li&gt;Define fresh, stale, completed, retryable failure, replayable failure, and unknown states.&lt;/li&gt;
&lt;li&gt;Store enough response data to satisfy your replay contract.&lt;/li&gt;
&lt;li&gt;Make downstream calls idempotent too, or have reconciliation.&lt;/li&gt;
&lt;li&gt;Use outbox/inbox patterns where events and queues are involved.&lt;/li&gt;
&lt;li&gt;Do not mark messages processed before their durable side effects exist.&lt;/li&gt;
&lt;li&gt;Define the idempotency window as part of the API contract.&lt;/li&gt;
&lt;li&gt;Retain metadata separately from sensitive response bodies if needed.&lt;/li&gt;
&lt;li&gt;Test concurrent duplicates, timeout after downstream success, partial failure, expiry, and schema-change replay.&lt;/li&gt;
&lt;li&gt;Monitor different-body reuse, stale &lt;code&gt;IN_PROGRESS&lt;/code&gt;, expired retries, unknown states, and replay rates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The second request is not a repeat until proven&lt;/h2&gt;
&lt;p&gt;The easy version of idempotency remembers that a key was seen.&lt;/p&gt;
&lt;p&gt;The useful version remembers what the key meant.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;POST /payments&lt;/code&gt;, that means remembering the scoped operation, the canonical command, the execution state, the
resulting resource or response, the expiry window, and enough failure state to avoid turning uncertainty into duplicate
side effects.&lt;/p&gt;
&lt;p&gt;The second request may be a retry. It may be a different operation wearing the same key. It may be racing the first
request. It may arrive after the provider succeeded but your process failed. It may arrive after your cleanup job
deleted the only memory of what happened.&lt;/p&gt;
&lt;p&gt;The server has to prove which case it is.&lt;/p&gt;
&lt;p&gt;The key is not the guarantee. The guarantee is that the server remembers the first operation precisely enough to replay
it, reject a mismatch, or recover instead of guessing.&lt;/p&gt; &lt;/div&gt; </ns0:encoded></item><item><title>Bun's experimental Rust rewrite hits 99.8% test compatibility on Linux x64 glibc</title><link>https://twitter.com/jarredsumner/status/2053047748191232310</link><pubDate>Sat, 09 May 2026 10:12:55 +0000</pubDate><comments>https://news.ycombinator.com/item?id=48073680</comments><description>&lt;a href="https://news.ycombinator.com/item?id=48073680"&gt;Comments&lt;/a&gt;</description></item><item><title>The One Dollar Counterfeiter</title><link>https://www.amusingplanet.com/2026/05/emerich-juettner-one-dollar.html</link><pubDate>Thu, 07 May 2026 12:40:35 +0000</pubDate><comments>https://news.ycombinator.com/item?id=48048684</comments><description>&lt;a href="https://news.ycombinator.com/item?id=48048684"&gt;Comments&lt;/a&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;article class="post postContainer" data-postid="6312347561510666639" morss_own_score="9.848139711465452" morss_score="15.793791885378495"&gt;


&lt;h1&gt;
Emerich Juettner: The One Dollar Counterfeiter
&lt;/h1&gt;

&lt;a href="https://www.blogger.com/profile/15000427721236718033" title="author profile"&gt;
Kaushik Patowary
&lt;/a&gt;
&lt;span title="2026-05-04T20:28:00+05:30"&gt;
May 4, 2026
&lt;/span&gt;





&lt;div id="adsense-target" morss_own_score="2.977920883164673" morss_score="83.51792088316468"&gt;&lt;p&gt;In fiction as well as in real life, counterfeiters have always been portrayed as master forgers and artists who reproduced banknotes with astonishing precision. They were often shown as vast criminal enterprises and gangsters who destabilized economies with fake currency. Then there was Emerich Juettner, a frail old immigrant living alone in a shabby New York apartment, quietly printing one-dollar bills on a cheap hand press. He was, by almost every conventional standard, terrible at counterfeiting.&lt;/p&gt;
&lt;p&gt;And yet he succeeded for nearly a decade. By the time the United States Secret Service finally caught Juettner in 1948, he had become kind of folk hero. The press adored him and the public sympathized with him. A Hollywood film would soon immortalize him under the nickname “Mister 880,” a reference to his Secret Service case number.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiwXpxWbUXh8JQ77qOz95jEMSz1Om4dBMMX9KkNU6q9VOgoSd0c9x09Iz2QFuGHTrW30gZQw-LIkr0LenTF5plIieA_W3peYZAD5JgtXMCAprBF11XeeREsHq8OU1tj5-6Ruz8CYFPEZXmbe-RzX6CvogO8X9ylGDDFCr-iATAGOOUlkSgsuCDx-WCo2ig/s1600/edward-juettner-2%20.jpg"&gt;&lt;/p&gt;&lt;p&gt;Emerich Juettner (also known as Edward Mueller) hardly looked like the sort of person who would trouble federal agents. Born in Austria-Hungary in 1876, he emigrated to the United States and spent most of his life drifting through modest occupations. Initially he worked as a picture frame gilder before marrying Florence LeMein in 1902 at the age of 26. After the birth of his son and daughter, Juettner began working as a maintenance man and building superintendent in New York's Upper East Side. His job allowed him and his family to live rent free in the basement of the building where he worked.&lt;/p&gt;
&lt;p&gt;In 1937, Juettner’s wife died and the sixty-year-old suddenly found himself living alone in New York City. He then became a junk collector.&lt;/p&gt;
&lt;p&gt;Juettner bought a used, two-wheel pushcart and spent long days ambling about the streets of New York picking up the discarded goods of city dwellers and selling off the occasional find to a wholesale dealer. But Juettner’s earnings were sporadic and he was barely putting food in his mouth. This forced him to look into another way of making a living.&lt;/p&gt;
&lt;p&gt;In his youth, Juettner had learned metal engraving. He had also dabbled in photography. Combining these two skills, in November 1938, he began to make counterfeit one-dollar bills. He snapped pictures of a $1 bill, transferred the images to a pair of zinc plates, and then meticulously filled in small details of the bill by hand. &lt;/p&gt;
&lt;p&gt;Juettner’s fake notes were laughably crude. He produced them using inexpensive materials and primitive techniques in his apartment kitchen. The paper was wrong. The ink was poor. The engraving lacked detail. The bills often appeared slightly blurred or uneven. Some notes even had spelling errors.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgZqL7bBTv2XNe1qnHmCuYJ1uH_2UK91pjMAMT3Eca7zf4devM7AJOw-Sr-QRz4Yb6n01nj1K37dwL4zJJllXJTJI86qakq2-CpKwF_BCY5Og7_0MQFJexX9IUV1sA8hEjdmHXsDjAxbLvs4rCeshdc30MQyL8cPmWskwp8mm29mjvtP6kaQeDzrM4eqd0/s1600/edward-juettner-1%20.jpg"&gt;&lt;br&gt;&lt;em&gt;Scenes from the movie Mister 880 dramatizing Juettner’s counterfeit operation.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;They were not the kind of counterfeit currency that could fool bankers or cashiers under careful inspection. But Juettner understood that almost nobody examines a one-dollar bill closely.&lt;/p&gt;
&lt;p&gt;Large-denomination counterfeits attract scrutiny because people expect fraud where substantial sums are involved. A suspicious twenty-dollar bill may be held to the light, checked for texture, or compared against a real note. But a worn one-dollar bill passing quickly between customers in a busy shop? Few people cared enough to look carefully.&lt;/p&gt;
&lt;p&gt;Juettner exploited that indifference brilliantly. He also operated on an extremely small scale. Instead of flooding the market, he released only a handful of fake bills at a time. The notes circulated quietly through diners, bars, street vendors, and small stores, disappearing into the immense ocean of American currency.&lt;/p&gt;
&lt;p&gt;The Secret Service became aware that poor-quality counterfeit dollar bills were appearing in New York, but the case puzzled investigators. Professional counterfeiters usually aimed for large profits. These bills, by contrast, were amateurish and limited in number. Whoever was making them seemed content merely to survive.&lt;/p&gt;
&lt;p&gt;The United States Secret Service, which investigates counterfeiting, opened a file on the mysterious forger. The investigation was assigned case number 880. Over the years, agents tried in vain to locate the culprit. They handed out some 200,000 warning placards at 10,000 stores. They tracked down dozens of folks who’d spent the bills and interviewed them.&lt;/p&gt;
&lt;p&gt;But Juettner remained elusive. He was careful not to draw attention. He never attempted large transactions. He worked alone, and he printed only tiny amounts.&lt;/p&gt;
&lt;p&gt;10 years went by and the search for Mister 880 turned into the largest and most expensive counterfeit investigation in Secret Service history.&lt;/p&gt;
&lt;p&gt;Then in January of 1948, some schoolboys playing around in a vacant lot in the Upper West Side uncovered a couple of zinc engraving plates buried in the snow. They also found “30 funny-looking dollar bills.”&lt;/p&gt;
&lt;p&gt;A week later, one boy’s father caught him playing poker with a strange bill and turned it into the police, who handed it over to the Secret Service. They soon tracked down the lot where the boys found the plates and learned that a few weeks earlier, there had been a fire in a bordering apartment. The firefighters had entered to find the place full to the brim with junk, and had thrown them out the window into the alley to make room. The Secret Service had found their mystery man, Mister 880.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiWHgUVP1LlN78c3IR8Oo-eK99g2d70kXAuzSC5zXC_0GiET_4_98x0Sdub7iWd_nVjfk6JA2HJrJOwHOucUEHJxiUE6-gga5sbxgKnkXzGQeUgDUXtqQ6lrJL2afeNGgzsjIT3KCLMTx_qvY8LlwgmWagUMUxcKhTMoZ5iW7jhxV_dvxt9GVmkPD2JJDk/s1600/edward-juettner-3%20.jpg"&gt;&lt;br&gt;&lt;em&gt;Emerich Juettner is interrogated by authorities after his arrest.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Emerich Juettner was arrested. When questioned about his crimes, he nonchalantly admitted to them. Juettner said that he had been making them for 9 to 10 years, and that he never gave more than one bill to any one person, “so nobody ever lost more than $1.”&lt;/p&gt;
&lt;p&gt;Under ordinary circumstances, a federal counterfeiting arrest would have generated little sympathy. But the story of Emerich Juettner struck the public imagination immediately. Here was an old man surviving in poverty by printing crude one-dollar bills one at a time. He was not violent, greedy, or glamorous.&lt;/p&gt;
&lt;p&gt;At trial, Juettner admitted his activities openly. The judge sentenced him to only a year and a day in prison, and he was paroled after 4 months. He was also made to pay a fine of $1. It has been agreed that Juettner’s complete lack of greed was the rationale behind the light sentence.&lt;/p&gt;
&lt;p&gt;After his release, Juettner briefly achieved celebrity status. His notoriety became so widespread that Hollywood adapted the story into the 1950 film &lt;em&gt;Mister 880&lt;/em&gt;, directed by Edmund Goulding. Eventually, Juettner made more money from the release of &lt;em&gt;Mister 880&lt;/em&gt; than he had made by counterfeiting.&lt;/p&gt;
&lt;p&gt;Juettner returned to a life of normalcy, and lived out the rest of his days in the suburbs of Long Island, where he died in 1955, at the age of 79.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; # The 70-year-old retiree who became America’s worst counterfeiter. &lt;a href="https://thehustle.co/worst-counterfeiter-in-history-mr-880"&gt;Hustle&lt;/a&gt;&lt;/p&gt;&lt;p&gt; # Finding ‘Mr. 880’: The case of the $1 counterfeit. &lt;a href="https://www.nydailynews.com/2011/04/03/finding-mr-880-the-case-of-the-1-counterfeit/"&gt;NY Daily News&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;












&lt;/article&gt;
</ns0:encoded></item><item><title>Show HN: Building a web server in assembly to give my life (a lack of) meaning</title><link>https://github.com/imtomt/ymawky</link><pubDate>Sun, 10 May 2026 03:01:44 +0000</pubDate><comments>https://news.ycombinator.com/item?id=48080587</comments><description>&lt;a href="https://news.ycombinator.com/item?id=48080587"&gt;Comments&lt;/a&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;article class="markdown-body entry-content container-lg" itemprop="text" morss_own_score="9.934640522875817" morss_score="84.50964052287583"&gt;&lt;p&gt;&lt;a href="https://github.com/imtomt/ymawky/blob/main/docs/ymawky.png"&gt;&lt;img src="https://github.com/imtomt/ymawky/raw/main/docs/ymawky.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;&lt;em&gt;ymawky&lt;/em&gt; -- web server in ARM assembly&lt;/h1&gt;
&lt;p&gt;This is &lt;em&gt;ymawky&lt;/em&gt; (yuh maw kee), a web server written entirely in ARM64 assembly. ymawky is a syscall-only, no libc, fork-per-connection web server written by hand. While it is developed for MacOS, I've tried to make it as portable as possible -- &lt;em&gt;however&lt;/em&gt;, it's likely you will still need to make some &lt;del&gt;(hopefully minor)&lt;/del&gt; Significant tweaks to get this to run on Linux/other Unix systems. See &lt;a href="https://github.com/imtomt/ymawky#implementation-notes"&gt;Implementation Notes&lt;/a&gt; for more details.&lt;/p&gt;
&lt;h2&gt;Building&lt;/h2&gt;
&lt;p&gt;Requires Xcode Command Line Tools. Install with &lt;code&gt;xcode-select --install&lt;/code&gt;.
ymawky only runs on apple silicon (arm64).&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;make&lt;/code&gt; to build.&lt;/p&gt;
&lt;p&gt;Ensure there is a &lt;code&gt;www/&lt;/code&gt; directory next to the &lt;code&gt;ymawky&lt;/code&gt; executable. That's the document root where &lt;em&gt;ymawky&lt;/em&gt; searches for files.
&lt;code&gt;GET&lt;/code&gt; with an empty filename (&lt;code&gt;GET /&lt;/code&gt;) will search for &lt;code&gt;www/index.html&lt;/code&gt;, so you might want to make sure there's an &lt;code&gt;index.html&lt;/code&gt; as well.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;ymawky&lt;/em&gt; will try to serve static error pages when a client's request results in error, eg 404. The pages it searches for in &lt;code&gt;err/(code).html&lt;/code&gt;, so ensure &lt;code&gt;err/&lt;/code&gt; exists alongisde &lt;code&gt;ymawky&lt;/code&gt; and &lt;code&gt;www/&lt;/code&gt;.
See &lt;a href="https://github.com/imtomt/ymawky#configuration"&gt;Configuration&lt;/a&gt; to modify the default file and docroot.&lt;/p&gt;
&lt;h2&gt;Running&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;./ymawky&lt;/code&gt; to start running the web server on &lt;code&gt;127.0.0.1:8080&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./ymawky [port]&lt;/code&gt; to start running the web server on &lt;code&gt;127.0.0.1:[port]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./ymawky [literally-any-character-other-than-0-9]&lt;/code&gt; to start running the web server on 127.0.0.1:8080 in debug mode. Debug mode disables forking, and makes ymawky only handle one request. (&lt;em&gt;I needed to do this because &lt;code&gt;lldb&lt;/code&gt; wasn't letting me debug the children, ugh.&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately, while custom ports are supported, custom addresses are not. as of right now, ymawky can only run on &lt;code&gt;127.0.0.1&lt;/code&gt;. This is solely because I haven't implemented it -- but if you'd like to consider this a safety feature, then I guess it could be intentional.&lt;/p&gt;
&lt;p&gt;To see ymawky in action, start running ymawky with &lt;code&gt;./ymawky [port]&lt;/code&gt;. Then open your web browser of choice (or use curl), and visit &lt;code&gt;127.0.0.1:8080/&lt;/code&gt; or &lt;code&gt;127.0.0.1:8080/pretty/index.html&lt;/code&gt;. Bask in the warmth of assembly.&lt;/p&gt;
&lt;h2&gt;What can it do?&lt;/h2&gt;
&lt;p&gt;ymawky is a static-file web server. It doesn't support server-side code to generate content on-the-fly, or more advanced URL parsing, such as &lt;code&gt;/search?query=term&lt;/code&gt;. That's not to say it's non-functional, though.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Supported HTTP methods:
&lt;ul&gt;
&lt;li&gt;GET&lt;/li&gt;
&lt;li&gt;PUT&lt;/li&gt;
&lt;li&gt;DELETE&lt;/li&gt;
&lt;li&gt;OPTIONS&lt;/li&gt;
&lt;li&gt;HEAD&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Basic protection from slowloris-like Denial of Service attacks&lt;/li&gt;
&lt;li&gt;Decodes % hex encoding, eg, &lt;code&gt;%20&lt;/code&gt; decodes to a space in filenames, and &lt;code&gt;%61&lt;/code&gt; decodes to &lt;code&gt;a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Smart path traversal detection and prevention. Blocks &lt;code&gt;..&lt;/code&gt; from traversing paths, while not disallowing multiple periods when they're part of a file:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /../../../etc/passwd&lt;/code&gt; -&amp;gt; &lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /ohwell...txt&lt;/code&gt; -&amp;gt; &lt;code&gt;200 OK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /../src/ymawky.S&lt;/code&gt; -&amp;gt; &lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /hehe..txt&lt;/code&gt; -&amp;gt; &lt;code&gt;200 OK&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Automatically prepends &lt;code&gt;www/&lt;/code&gt; to requested files. &lt;code&gt;GET /index.html&lt;/code&gt; will retrieve &lt;code&gt;www/index.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Empty &lt;code&gt;GET /&lt;/code&gt; requests default to &lt;code&gt;GET www/index.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt; requests support uploads of up to 1GiB, though this can be configured for larger files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT&lt;/code&gt; is atomic due to writing to a temporary file then renaming, allowing concurrent &lt;code&gt;PUT&lt;/code&gt; requests without leaving partially-written files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Length:&lt;/code&gt; parsing and verification in &lt;code&gt;PUT&lt;/code&gt; requests&lt;/li&gt;
&lt;li&gt;MIME type detection, giving &lt;code&gt;Content-Type&lt;/code&gt; in the response header with the corresponding MIME type&lt;/li&gt;
&lt;li&gt;Accepts &lt;code&gt;Range: bytes=&lt;/code&gt; ranges in GET requests, supporting full ranges &lt;code&gt;bytes=X-N&lt;/code&gt;, suffix ranges &lt;code&gt;bytes=-N&lt;/code&gt;, and open-ended ranges &lt;code&gt;bytes=X-&lt;/code&gt;. Video scrubbing is well supported&lt;/li&gt;
&lt;li&gt;Basic HTTP version parsing. Requests need to specify &lt;code&gt;HTTP/1.1&lt;/code&gt; or &lt;code&gt;HTTP/1.0&lt;/code&gt;, and if requesting &lt;code&gt;HTTP/1.1&lt;/code&gt;, a &lt;code&gt;Host:&lt;/code&gt; field needs to be present in the header. Currently, ymawky doesn't do anything with Host, but per RFC 9112 Section 3.2, the Header must be sent&lt;/li&gt;
&lt;li&gt;Serves custom HTML pages for error codes, such as 404, or 500. Look in the &lt;code&gt;err/&lt;/code&gt; directory for an example&lt;/li&gt;
&lt;li&gt;If the requested resource is a directory, list all files and subdirs in the directory. Note that this excludes www/ (or whatever your docroot is): GET / will always search for index.html if no file is given.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;"Safety"&lt;/h2&gt;
&lt;p&gt;This is a web server written entirely by-hand in ARM64 assembly as a fun project. It's probably got a lot of vulnerabilities I'm unaware of. However, I did do my best to make it safer. Here are some safety precautions ymawky takes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rejects paths &amp;gt;= PATH_MAX (4096 bytes)&lt;/li&gt;
&lt;li&gt;Reject any paths that include path traversal -- &lt;code&gt;/../..&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Reject any requests that do not contain a path within 16 bytes&lt;/li&gt;
&lt;li&gt;Confined to &lt;code&gt;www/&lt;/code&gt;. Any path requested gets &lt;code&gt;www/&lt;/code&gt; prepended to it&lt;/li&gt;
&lt;li&gt;Rejects any path containing symlinks, with O_NOFOLLOW_ANY&lt;/li&gt;
&lt;li&gt;PUT writes to a temporary file, &lt;code&gt;www/.ymawky_tmp_&amp;lt;pid&amp;gt;&lt;/code&gt;. Upon successfully receiving the whole file, this temporary file is then renamed to the requested filename. This prevents partial or corrupted PUT requests from overwriting existing files.&lt;/li&gt;
&lt;li&gt;Reject any requests whose path starts with &lt;code&gt;www/.ymawky_tmp_&lt;/code&gt;. This prevents someone from &lt;code&gt;GET&lt;/code&gt;ing a temporary file, and prevents someone from sending &lt;code&gt;PUT /.ymawky_tmp_4533&lt;/code&gt; or something.&lt;/li&gt;
&lt;li&gt;Must receive data within 10 seconds. If it's slower, the connection will close. If the entire header is not received within 10 seconds total, the connection will be closed. This is to prevent slowloris-like attacks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;HTTP Status Codes&lt;/h2&gt;
&lt;p&gt;ymawky currently supports and can reply with the following status codes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;200 OK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;201 Created&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;204 No Content&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;206 Partial Content&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;400 Bad Request&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;404 Not Found&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;408 Request Timeout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;409 Conflict&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;411 Length Required&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;413 Content Too Large&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;414 URI Too Long&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;416 Range Not Satisfiable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;418 I'm a teapot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;431 Request Header Fields Too Large&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;500 Internal Server Error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;501 Not Implemented&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;505 HTTP Version Not Supported&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;507 Insufficient Storage&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Custom HTML pages will be served alongside the error codes (400+). These HTML files are located in &lt;code&gt;err/(code).html&lt;/code&gt;. You can use &lt;code&gt;build_err_pages.sh&lt;/code&gt; to create a page for each code, with different text at your leisure. Edit the source code of &lt;code&gt;build_err_pages.sh&lt;/code&gt; to modify the text per-page, and modify &lt;code&gt;err/template.html&lt;/code&gt; to modify the base template. In &lt;code&gt;err/template.html&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{{CODE}}&lt;/code&gt;  - HTTP Code: eg, 404&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{{TITLE}}&lt;/code&gt; - Title text: eg, "Not Found"&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{{MSG}}&lt;/code&gt;   - Custom message: eg, "the rats ate this page"&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MIME Types&lt;/h2&gt;
&lt;p&gt;MIME types are detected by analyzing the file extension. The following MIME types are recognized.&lt;/p&gt;
&lt;p&gt;Web-related files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.html&lt;/code&gt;  -&amp;gt; &lt;code&gt;text/html; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.htm&lt;/code&gt;   -&amp;gt; &lt;code&gt;text/html; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.css&lt;/code&gt;   -&amp;gt; &lt;code&gt;text/css; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.csv&lt;/code&gt;   -&amp;gt; &lt;code&gt;text/csv; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.xml&lt;/code&gt;   -&amp;gt; &lt;code&gt;text/xml; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.js&lt;/code&gt;    -&amp;gt; &lt;code&gt;text/javascript; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.json&lt;/code&gt;  -&amp;gt; &lt;code&gt;application/json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.wasm&lt;/code&gt;  -&amp;gt; &lt;code&gt;application/wasm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.mjs&lt;/code&gt;   -&amp;gt; &lt;code&gt;text/javascript; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.map&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Image files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.png&lt;/code&gt;   -&amp;gt; &lt;code&gt;image/png&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.jpg&lt;/code&gt;   -&amp;gt; &lt;code&gt;image/jpeg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.jpeg&lt;/code&gt;  -&amp;gt; &lt;code&gt;image/jpeg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gif&lt;/code&gt;   -&amp;gt; &lt;code&gt;image/gif&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.svg&lt;/code&gt;   -&amp;gt; &lt;code&gt;image/svg+xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.ico&lt;/code&gt;   -&amp;gt; &lt;code&gt;image/x-icon&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.webp&lt;/code&gt;  -&amp;gt; &lt;code&gt;image/webp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.avif&lt;/code&gt;  -&amp;gt; &lt;code&gt;image/avif&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bmp&lt;/code&gt;   -&amp;gt; &lt;code&gt;image/bmp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.tiff&lt;/code&gt;  -&amp;gt; &lt;code&gt;image/tiff&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.apng&lt;/code&gt;  -&amp;gt; &lt;code&gt;image/apng&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Font files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.woff&lt;/code&gt;  -&amp;gt; &lt;code&gt;font/woff&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.woff2&lt;/code&gt; -&amp;gt; &lt;code&gt;font/woff2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.ttf&lt;/code&gt;   -&amp;gt; &lt;code&gt;font/ttf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.otf&lt;/code&gt;   -&amp;gt; &lt;code&gt;font/otf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Document files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.txt&lt;/code&gt;   -&amp;gt; &lt;code&gt;text/plain; charset=utf-8&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.pdf&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/pdf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.doc&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/msword&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.docx&lt;/code&gt;  -&amp;gt; &lt;code&gt;application/vnd.openxmlformats-officedocument.wordprocessingml.document&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.epub&lt;/code&gt;  -&amp;gt; &lt;code&gt;application/epub+zip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rtf&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/rtf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Video files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.mp4&lt;/code&gt;   -&amp;gt; &lt;code&gt;video/mp4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.webm&lt;/code&gt;  -&amp;gt; &lt;code&gt;video/webm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.mkv&lt;/code&gt;   -&amp;gt; &lt;code&gt;video/x-matroska&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.avi&lt;/code&gt;   -&amp;gt; &lt;code&gt;video/x-msvideo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.mov&lt;/code&gt;   -&amp;gt; &lt;code&gt;video/quicktime&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Audio files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.mp3&lt;/code&gt;   -&amp;gt; &lt;code&gt;audio/mpeg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.ogg&lt;/code&gt;   -&amp;gt; &lt;code&gt;audio/ogg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.wav&lt;/code&gt;   -&amp;gt; &lt;code&gt;audio/wav&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.flac&lt;/code&gt;  -&amp;gt; &lt;code&gt;audio/flac&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.aac&lt;/code&gt;   -&amp;gt; &lt;code&gt;audio/aac&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.m4a&lt;/code&gt;   -&amp;gt; &lt;code&gt;audio/mp4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.opus&lt;/code&gt;  -&amp;gt; &lt;code&gt;audio/opus&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Archive files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.zip&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/zip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gz&lt;/code&gt;    -&amp;gt; &lt;code&gt;application/gzip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.tar&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/x-tar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.7z&lt;/code&gt;    -&amp;gt; &lt;code&gt;application/x-7z-compressed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bz2&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/x-bzip2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rar&lt;/code&gt;   -&amp;gt; &lt;code&gt;application/vnd.rar&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Configuration&lt;/h2&gt;
&lt;p&gt;You can configure ymawky with the &lt;code&gt;config.S&lt;/code&gt; file. The options are documented here.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#define DEFAULT_DIR "www/"&lt;/code&gt; -- This is the docroot. Change it to wherever your HTML files are, relative to ymawky, or use an absolute path:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#define DEFAULT_DIR "www/"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#define DEFAULT_DIR "/Library/WebServer/Documents&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#define DEFAULT_DIR "./"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#default ERR_DIR "err/"&lt;/code&gt; -- This is the directory in which ymawky will search for custom error HTML pages, eg, &lt;code&gt;err/404.html&lt;/code&gt; or &lt;code&gt;err/500.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#define DEFAULT_FILE "index.html"&lt;/code&gt; -- This is the default file ymawky will serve when it receives an empty &lt;code&gt;GET / HTTP/1.1&lt;/code&gt; request&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ RECV_TIMEOUT, 10&lt;/code&gt; -- Number of seconds ymawky will wait to receive datta before closing the connection. If it's more than &lt;code&gt;RECV_TIMEOUT&lt;/code&gt; seconds between &lt;code&gt;read()&lt;/code&gt;s, ymawky will close the connection with &lt;code&gt;408 Request Timed Out&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ HEADER_REQ_TIMEOUT_SECS, 10&lt;/code&gt; -- Maximum number of seconds ymawky will wait to receive the full header before timing out. If it takes, longer than this to receive the header, ymawky will close the connection with &lt;code&gt;408 Request Timed Out&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ PUT_GRACE_SECS, 5&lt;/code&gt; -- ymawky dynamically calculates a max-time-per-PUT based on &lt;code&gt;Content-Length&lt;/code&gt;. The max time is defined as &lt;code&gt;PUT_GRACE_SECS + Content-Length / PUT_MIN_BPS&lt;/code&gt;. This is the minimum grace period allowed if it calculates a file should take &amp;lt;1 second to upload&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ PUT_MIN_BPS, 1024 * 16&lt;/code&gt; -- Minimum bytes-per-second. Higher if you want to be stricter, smaller if you want to be more lenient. Since this uses the &lt;code&gt;.equ&lt;/code&gt; directive, arithmetic is supported, and &lt;code&gt;1024 * 16&lt;/code&gt; gets calculated at assembly time becoming &lt;code&gt;16384&lt;/code&gt; or 16KB&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ MAX_BODY_SIZE, 1024 * 1024 * 1024&lt;/code&gt; -- Maximum bytes PUT allows for Content-Length. By default, 1GB (1024&lt;em&gt;1024&lt;/em&gt;1024 = 1073741824 bytes). Files with a larger Content-Length larger than this will be rejected with &lt;code&gt;413 Content Too Large&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ MAX_PROCS, 256&lt;/code&gt; -- Maximum number of concurrent proccesses ymawky is allowed to run. Since ymawky is a fork-per-connection server, you want to ensure ymawky doesn't exhaust your PID space. ymawky will reply with &lt;code&gt;503 Service Unavailable&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Implementation Notes&lt;/h2&gt;
&lt;p&gt;ymawky is written for MacOS (sorry...). There are a few (well, more than a &lt;em&gt;few&lt;/em&gt;) things that are MacOS-specific in this code that won't be portable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Syscalls on MacOS use &lt;code&gt;x16&lt;/code&gt; for the number and &lt;code&gt;svc #0x80&lt;/code&gt; to call it. Linux uses &lt;code&gt;x8&lt;/code&gt; and &lt;code&gt;svc #0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Error reporting is different. MacOS sets the carry flag on error, and puts &lt;code&gt;errno&lt;/code&gt; in &lt;code&gt;x0&lt;/code&gt;. Linux returns a negative value in &lt;code&gt;x0&lt;/code&gt;, like &lt;code&gt;-ENOENT&lt;/code&gt;. Ever &lt;code&gt;b.cs&lt;/code&gt; would need to be replaced with &lt;code&gt;cmp x0, #0&lt;/code&gt; / &lt;code&gt;b.lt ...&lt;/code&gt;, and you'd negate &lt;code&gt;x0&lt;/code&gt; to get errno.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fork()&lt;/code&gt; works differently, MacOS puts 1 in &lt;code&gt;x1&lt;/code&gt; in the child process, whereas Linux puts &lt;code&gt;0&lt;/code&gt; in &lt;code&gt;x0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SO_NOSIGPIPE&lt;/code&gt; doesn't exist on Linux.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O_NOFOLLOW_ANY&lt;/code&gt; is also MacOS-specific.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;renameatx_np()&lt;/code&gt; is also MacOS-specific. Linux has &lt;code&gt;renameat2()&lt;/code&gt;, with different flag values.&lt;/li&gt;
&lt;li&gt;Struct layouts and offsets will differ. The &lt;code&gt;stat64&lt;/code&gt; struct, &lt;code&gt;itimerval&lt;/code&gt; struct, and &lt;code&gt;sockaddr_in&lt;/code&gt; struct, will all need to be reconsidered.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;adr xN, foo@PAGE&lt;/code&gt; / &lt;code&gt;add xN, xN, foo@PAGEOFF&lt;/code&gt; are Mach-O relocation operators. Linux ELF uses different syntax, like &lt;code&gt;:pg_hi21:&lt;/code&gt; and &lt;code&gt;:lo12:&lt;/code&gt;. The &lt;code&gt;adr_l&lt;/code&gt;, &lt;code&gt;ldr_l&lt;/code&gt; and &lt;code&gt;str_l&lt;/code&gt; macros would need to be rewritten or replaced.&lt;/li&gt;
&lt;li&gt;My personal favorite :3 Signal handling works differently on Linux and MacOS. MacOS's &lt;code&gt;sigaction&lt;/code&gt; struct contains a &lt;code&gt;sa_tramp&lt;/code&gt; field that the kernel jumps to before your handler. ymawky utilizes &lt;code&gt;sa_tramp&lt;/code&gt; directly &lt;em&gt;as the handler itself&lt;/em&gt;, skipping the libc trampoline and &lt;code&gt;sigreturn&lt;/code&gt; entirely. Since the handler only sends a 408 and exits, without needing to return, that's fine and works wonderfully without libc. The &lt;code&gt;sigaction&lt;/code&gt; call would need to be rewritten for POSIX systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Special Thanks:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Bob Johnson&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Bob Johnson's Therapist&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;</ns0:encoded></item><item><title>We see something that works, and then we understand it</title><link>https://lemire.me/blog/2025/12/04/we-see-something-that-works-and-then-we-understand-it/</link><pubDate>Wed, 06 May 2026 15:15:23 +0000</pubDate><comments>https://news.ycombinator.com/item?id=48037223</comments><description>&lt;a href="https://news.ycombinator.com/item?id=48037223"&gt;Comments&lt;/a&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;article id="post-22357" class="post-22357 post type-post status-publish format-standard has-post-thumbnail hentry category-84" morss_own_score="9.59059893858984" morss_score="15.01091143858984"&gt;

&lt;img src="https://lemire.me/blog/wp-content/uploads/2025/12/Capture-decran-le-2025-12-04-a-10.40.38-825x510.png"&gt; 

&lt;h1&gt;We see something that works, and then we understand it&lt;/h1&gt; 
&lt;div class="entry-content" morss_own_score="5.840624999999999" morss_score="52.26484491084695"&gt;
&lt;p&gt;“We see something that works, and then we understand it.” (Thomas Dullien)&lt;/p&gt;
&lt;p&gt;It is a deeper insight than it seems.&lt;/p&gt;
&lt;p&gt;Young people spend years in school learning the reverse: understanding happens before progress. That is the linear theory of innovation.&lt;/p&gt;
&lt;p&gt;So Isaac Newton comes up with his three laws of mechanics, and we get a clockmaking boom. Of course, that’s not what happened: we get the pendulum clock in 1656, then Hooke (1660) and Newton (1665–1666) get to think about forces, speed, motion, and latent energy.&lt;/p&gt;
&lt;p&gt;The linear model of innovation makes as much sense as the waterfall model in software engineering. In the waterfall model, you are taught that you first need to design every detail of your software application (e.g., using a language like UML) before you implement it. To this day, half of the information technology staff members at my school are made up of “analysts” whose main job is supposedly to create such designs based on requirements and supervise execution.&lt;/p&gt;
&lt;p&gt;Both the linear theory and the waterfall model are forms of thinkism, a term I learned from Kevin Kelly. Thinkism sets aside practice and experience. It is the belief that given a problem, you should just think long and hard about it, and if you spend enough time thinking, you will solve it.&lt;/p&gt;
&lt;p&gt;Thinkism works well in school. The teacher gives you all the concepts, then gives you a problem that, by a wonderful coincidence, can be solved just by thinking with the tools the same teacher just gave you.&lt;/p&gt;
&lt;p&gt;As a teacher, I can tell you that students get really angry if you put a question on an exam that requires a concept not explicitly covered in class. Of course, if you work as an engineer and you’re stuck on a problem and you tell your boss it cannot be solved with the ideas you learned in college… you’re going to look like a fool.&lt;/p&gt;
&lt;p&gt;If you’re still in school, here’s a fact: you will learn as much or more every year of your professional life than you learned during an entire university degree—assuming you have a real engineering job.&lt;/p&gt;
&lt;p&gt;Thinkism also works well in other limited domains beyond school. It works well in bureaucratic settings where all the rules are known and you’re expected to apply them without question. There are many jobs where you first learn and then apply. And if you ever encounter new conditions where your training doesn’t directly apply, you’re supposed to report back to your superiors, who will then tell you what to do.&lt;/p&gt;
&lt;p&gt;But if you work in research and development, you always begin with incomplete understanding. And most of the time, even if you could read everything ever written about your problem, you still wouldn’t understand enough to solve it. The way you make discoveries is often to either try something that seems sensible, or to observe something that happens to work—maybe your colleague has a practical technique that just works—and then you start thinking about it, formalizing it, putting it into words… and it becomes a discovery.&lt;/p&gt;
&lt;p&gt;And the reason it often works this way is that “nobody knows anything.” The world is so complex that even the smartest individual knows only a fraction of what there is to know, and much of what they think they know is slightly wrong—and they don’t know which part is wrong.&lt;/p&gt;
&lt;p&gt;So why should you care about how progress happens? You should care because…&lt;/p&gt;
&lt;p&gt;
1. It gives you a recipe for breakthroughs: spend more time observing and trying new things… and less time thinking abstractly.&lt;/p&gt;&lt;p&gt;
2. Stop expecting an AI to cure all diseases or solve all problems just because it can read all the scholarship and “think” for a very long time. No matter how much an AI “knows,” it is always too little.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Further reading&lt;/strong&gt;: Godin, Benoît (2017). Models of innovation: The history of an idea. MIT press.&lt;/p&gt;
&lt;p&gt;Daniel Lemire, "We see something that works, and then we understand it," in &lt;em&gt;Daniel Lemire's blog&lt;/em&gt;, December 4, 2025, &lt;a href="https://lemire.me/blog/2025/12/04/we-see-something-that-works-and-then-we-understand-it/"&gt;https://lemire.me/blog/2025/12/04/we-see-something-that-works-and-then-we-understand-it/&lt;/a&gt;.&#13;
&lt;br&gt;
&lt;a href="https://lemire.me/blog/2025/12/04/we-see-something-that-works-and-then-we-understand-it/"&gt; [BibTeX]&#13;
&lt;/a&gt;
&lt;/p&gt;
 &lt;/div&gt;


&lt;img src="https://secure.gravatar.com/avatar/a0c6c34cf4a45ff5083d5ea541e7f27c5e4457ac393889e077af10688f6b3831?s=56&amp;amp;d=mm&amp;amp;r=g"&gt; 

&lt;h3&gt;Daniel Lemire&lt;/h3&gt;
&lt;p&gt;
			A computer science professor at the University of Quebec (TELUQ).			&lt;a href="https://lemire.me/blog/author/lemire/"&gt;
				View all posts by Daniel Lemire			&lt;/a&gt;
&lt;/p&gt;


&lt;/article&gt;
</ns0:encoded></item></channel></rss>