5/10/2026 at 2:03:24 PM
This is all way too much. If you see a duplicate idempotency key, skip the replay and always return 409. This becomes a client problem. Clients already need to help enforce idempotent contracts; "check for conflict response" is not an onerous imposition.I've built multiple ecommerce APIs with this approach and they work great. No heroic measures required. You can often satisfy this contract with a unique constraint; if not, a simple presence check in redis. No hashing or worrying about PII.
My rant about this: https://github.com/stickfigure/blog/wiki/How-to-%28and-how-n...
by stickfigure
5/10/2026 at 2:43:31 PM
But that's not idempotent? If I'm a client and I don't know if the original request went through, getting a 409 on any subsequent requests tells me nothing about whether the original request was successful or not.by halestock
5/10/2026 at 3:12:16 PM
Retries will only receive 409 if the original request was successful. If the original request failed, the server performs the operation as normal on the second request. It doesn't replay failures.The whole point of the idempotence mechanism is so you can make a reliable distributed system. If the first try fails, the client doesn't know if it succeeded or not, so the client should try again later ("at-least-once"). The idempotence mechanism just ensures that we don't get duplicates in the case that the first try actually succeeded.
If you replayed failures there wouldn't be any point to the idempotency key.
by stickfigure
5/10/2026 at 3:25:32 PM
What if the original request is still being processed when the retry comes in? That doesn't fall into either of your categories: the request isn't successful, but it hasn't failed either.by pdonis
5/10/2026 at 8:42:58 PM
That’s because everyone here thinking about payments completely incorrectly. They’re not atomic and your server shouldn’t pretend they are.You need to store the payment state at each relevant step and process it asynchronously. If requests time out, you check the status of it using the key you store (with the processor) to see if it was even received.
It’s not perfect, some processors will 500 while processing the payment (Braintree), so you still need reconciliation on the backend.
by scott_w
5/10/2026 at 11:32:31 PM
You still return 409, with error detail in the response body, because it is still true that the client sent multiple requests with the same idempotency key, regardless of the progress of the original request.Regardless, I think your assumption about how the request/response cycle should be working is wrong. For this kind of API and transaction, the server should be returning a response immediately: 202 Accepted. The only thing the API server should be doing before returning is creating a row in a DB (with a "state" field with an initial value of "pending"), and pushing some work on a queue.
The server should not be sitting there with the HTTP request open, trying to complete the transaction, and only returning a response to the client when the transaction is finished or has encountered an error.
The client will have to learn about the progress of the state of the transaction outside of this initial request. There are many options here: polling, webhooks, a message queue like kinesis or kafka, etc.
by kelnos
5/11/2026 at 12:50:23 PM
Ah you see, you just essentially moved unique constraint to a client. It is no different generating an ID on client side and making a request. It is not idempotency.Idempotency-Key should not replay the response (it depends, actually). But also it should not error 409. You need to be content aware before adding Idemmpotency Key header handling.
What will happen when the request is received and handled but during writing response body TCP connection dropped unexpectedly. And after second or two a connection reestablished. How two sides agree that previous request accepted and everything good to go? That's what Idempotency-Key header does.
by GRiMe2D
5/11/2026 at 3:07:26 PM
By "request" I didn't necessarily mean the HTTP request sent by the client, and I don't think the post I was responding to did either. But I agree my use of terminology was ambiguous. Let me restate more clearly, and in a way that shows the issue under your request/response process:An HTTP request comes in with a certain idempotency key. The server returns 202, as you say, and begins to process the database transaction.
While the server is still procesing the database transaction, a second HTTP request comes in with the same idempotency key. What response does this second HTTP request get? The original transaction that the first HTTP request triggered hasn't succeeded and hasn't failed, so it doesn't fall into either of the categories in the post I responded to.
Your answer is that the second HTTP request gets a 409, which makes sense to me, although others are objecting to it.
by pdonis
5/11/2026 at 4:42:41 PM
Best practice is to keep database transactions short. There is no value in returning a "hey this is in progress" error code to a client when transactions are short, and databases don't easily give you this primitive anyway.You seem very focused on long-running orchestration type systems. You build these on top of basic transactional primitives, but it's a mistake to try to make the whole process a single transaction. You can have a quick, transactional "start process" operation which must be idempotent. Other operations like "check status" need not be so complicated.
You don't necessarily share the idempotency key between the "start process" request and the "check status" request. You could for convenience, but it isn't necessary, and on balance most APIs don't. This is the "client picks ID" vs "server picks ID" design choice.
by stickfigure
5/11/2026 at 5:12:37 PM
> There is no value in returning a "hey this is in progress" error code to a client when transactions are shortFair enough. So basically your approach is to wait until the first request completes to decide how to respond to the second request that came in with the same idempotency key.
However, that would seem to me to imply that when the second request comes in, you check its idempotency key, realize you've already received a request with that key and you're processing it, and don't do anything else with the second request until the first one is completed. In particular, you don't have the second request trigger the start of another transaction.
But elsewhere in this thread, you've said you would start a second transaction based on the second request, and let your database's transaction mechanism tell you that it's a duplicate when you try to commit it. Why would you do that if you've checked the second request's idempotency key and you know it's a duplicate?
> You seem very focused on long-running orchestration type systems.
I'm not focused on anything except getting what I thought would be a simple answer to a simple question. The above seems to provide that (though it still leaves a question open, as above). That's all I wanted.
> You don't necessarily share the idempotency key between the "start process" request and the "check status" request.
I'm not talking about a "check status" request. The scenario I've been asking about all along is when a second "start process" request comes in with the same idempotency key as a previous "start process" request, while the process is still in progress.
by pdonis
5/11/2026 at 6:33:05 PM
While I'd love to take credit for it, this isn't literally "my" approach; this is just the standard way that transaction processing systems on the web work. And it's so standard that you don't even have to write code for it. Databases handle all the isolation and concurrency issues for you.Part of the problem here is that we're confusing how do you structure the API (replay? 409? something else?) with how we implement the API. The original article (and my original response) focused on API structure. We're wandering into the details of implementation, which is fine, but there are of course many ways to do the implementation. Some simpler than others.
Here's the simplest and most reliable way to implement idempotency for a trivial "create payment" operation, where the client submits an idempotency key. This pattern is incredibly common. Every request looks something like this:
* Start a transaction
* Lookup "does this idempotency key already exist"
* If it doesn't, insert the payment record with the idempotency key
* Commit the transaction
* Return the result. Successful insert is always 200OK. "key already exists" results in either replay of the original result (Stripe model) or an explicit error like 409 (my favored approach, still ubiquitous in ecommerce, and very common in financial APIs that predate Stripe).
Does that help? If you're using your database to handle concurrency, you need every request to start inside the transaction. You can't check the idempotency key outside of the transaction or you can't guarantee once-and-only-once behavior.
[Before someone mentions it, yes you can use a unique constraint instead of an explicit transaction, and this is conceptually identical - the check-for-dup transaction is inside a single INSERT]
by stickfigure
5/11/2026 at 8:00:22 PM
> Does that help?What you said up to that point didn't really. But then you said this:
> If you're using your database to handle concurrency, you need every request to start inside the transaction. You can't check the idempotency key outside of the transaction or you can't guarantee once-and-only-once behavior.
Which answers the question that what you said earlier in your post raised. If I'm understanding you right, "lookup the idempotency key" is also relying on the same database, so you need the whole operation to be inside a single transaction in that database.
by pdonis
5/11/2026 at 8:02:15 PM
> Part of the problem here is that we're confusing how do you structure the API (replay? 409? something else?) with how we implement the API.It would seem to me that you would want "what happens if a second request comes in with the same idempotency key while the first is still in progress" to be part of the API, so clients would know what your server is going to do in that scenario.
by pdonis
5/12/2026 at 5:08:02 AM
Nobody cares. These are short lived transactions (generally milliseconds); collisions are a rare edge case; it's fine to block. One request succeeds, the other gets a dup error (or a replay).You could invent your own more sophisticated idempotency API but good luck finding someone that wants to implement it or use it. What real-world problem are you trying to solve?
by stickfigure
5/12/2026 at 3:25:01 PM
> Nobody cares.Meaning, clients don't care about the thing I asked about?
> What real-world problem are you trying to solve?
I'm trying to understand your answers to my questions. When there seems to me to be something missing, I ask about it.
by pdonis
5/12/2026 at 4:48:46 PM
Correct, clients don't care to be notified if some other short lived request is in flight.by stickfigure
5/11/2026 at 3:40:50 PM
> You still return 409No no no no no.
You have multiple clients submitting the same business operation simultaneously. One must succeed, the others must fail. If you're using the 409 approach ("notify client that request is redundant") you must not send a 409 code until the work is complete.
The client must interpret 200 and 409 as success cases. 200 means "it was done" and 409 means "it was already done". Clients looping (say, processing durable queue messages) can stop when they receive these responses.
If the work is not complete, you can't return 409, or clients will think the work is done. You will lose messages.
by stickfigure
5/11/2026 at 7:33:26 PM
What's the best practice here? It's trying to represent as binary a multi-state operation, but the redundant clients should check the response's body to know why it 409'd. If the process is slow, it can't return a 200 immediately, and yet it should return 409 to all other attempts, even if the initial attempt ends up unsuccessful.by ASalazarMX
5/11/2026 at 9:40:51 PM
> and yet it should return 409 to all other attempts, even if the initial attempt ends up unsuccessful.No, it shouldn't. The comment you're responding to is taking 200 to mean "success" and 409 to mean "it was done" so if it was not in fact done then you _must not_ return that.
That said, I thought one of the benefits of idempotency was nonblocking APIs so I'm not sure I like that scheme. It seems like 200 should mean "submitted, accepted, incomplete" and 409 should mean "previously completed". The client never knows which request succeeded but they're idempotent so that doesn't matter. You just poll until the 200 becomes a 409.
Of course that would provide zero diagnostics in the case of failure so I think it's not sufficient as described.
by fc417fc802
5/11/2026 at 4:00:03 PM
> You have multiple clients submitting the same business operation simultaneously.It doesn't have to be multiple clients. It could be the same client, not having received a response to its first request and deciding to re-send the request again.
by pdonis
5/11/2026 at 4:30:46 PM
That's fine! It doesn't change anything.by stickfigure
5/10/2026 at 5:14:25 PM
Being charitable, I'd say the poster above is saying that in the web architecture you can (should?) shift more of the burden for idempotence to the client.But, rather than 409, I'd say that you should be using opportunistic concurrency control if you adopt this perspective. There should be a resource context for the request, so the client can obtain an ETag and send If-None-Match headers, and get a 412 response if things are out of sync. That allows them to retry a failed/lost request and safely prevent a double action.
Under a 412, they have to step back and retry a larger loop where they GET some new state and prepare a new action. Just like in DB transaction programming, where your failed commit means you roll back, clean the slate, and start a whole new interrogation of transaction-protected state leading up to your new mutation request.
by saltcured
5/10/2026 at 7:04:03 PM
The client is already participating in a transaction in a distributed system. There is no way to change the reality of that. Suggestions about masking this only make the composite system unsound and will not improve net service reliability improvement.That doesn't mean that idempotency keys have to be used. You can certainly hash message content if that is documented behavior. That probably only makes sense when there is already some logical session or transaction identifier that makes dedupe semantics clear.
The system you propose might be sound and might be necessary in some systems, but I can't think of what they might be that wouldn't be better served by the simpler solution that is already widely used for this purpose.
by jeremyjh
5/10/2026 at 7:33:48 PM
Your database should not allow both commits to happen - one should get rolled back.If it processed 99% of the request and the final bookkeeping failed because of a duplicate, that's still a failed request.
Arguably this should be the primary way you check for idempotent requests - you shouldn't have a separate check for existence, you should have the insert/update fail atomically.
This is the same thing you see on filesystems for TOCTOU security holes - the right way is to atomically access and modify once, and you only know the request was already processed because that fails.
by jmalicki
5/10/2026 at 8:45:15 PM
Not in the payments world. If you’re 99% done but only the bookkeeping failed, then it’s likely that money is changing hands and you need to deal with that fact. Payments are not an atomic infrastructure and you cannot magic that into reality.by scott_w
5/10/2026 at 9:27:01 PM
Payments are multistep but each of the steps needs to be atomic. The "create payment" operation must be transactional and the communication channel between you and the processor must be idempotent so you don't inadvertently create multiple payments.The fact that payments have a settlement process is not relevant to this discussion.
by stickfigure
5/11/2026 at 12:46:36 PM
> The "create payment" operation must be transactional and the communication channel between you and the processor must be idempotent so you don't inadvertently create multiple payments.Yes, I agree. You want to generate a token, persist it locally and use that to communicate with the payment gateway, so re-submissions use the same key and either error or return the transaction state.
> The fact that payments have a settlement process is not relevant to this discussion.
I wasn't talking about settlement, I was talking about the processing aspect. What I meant was: once you kickstart the process with the gateway, money is highly likely to change hands as a result. This means a process of:
1. POST /checkout
2. Create token
3. POST to payment gateway with token
4. Wait for gateway to return
5. Persist transaction/error
6. Return success/error
What is needed is to persist and return the token to the caller before contacting the payment gateway, to make a check + retry mechanism possible.
And yes, I've seen code that follows steps 1-6 exactly as I've described and, yes, all the problems you imagine would occur from those steps have occurred at one time or another.
by scott_w
5/11/2026 at 2:37:20 PM
This is one possible API but it's not the dominant way payments are exposed by payments providers. Stripe, Amazon, Paypal, et al do this differently. They're fine.by stickfigure
5/11/2026 at 4:52:07 PM
I pointed to Braintree in another comment where this is very much not fine. Also these providers operate at a lower level of the stack than we do, so they have finer control over the process than you or I.Even then, it’s not fine because those requests might time out, or your request times out waiting for theirs. Just because your provider abstracts behind one API doesn’t mean you necessarily can!
by scott_w
5/11/2026 at 6:48:48 PM
This problem is the entire point of the idempotency key system. You (the client) must retry the payment until success (where APIs that provide "this idempotency key already seen" is considered success). The idempotency key prevents you from creating duplicate payments.500 errors, network timeouts, etc all happen. We can't run 2PC transactions with Stripe, so you need durable retries. People run billions of dollars through these APIs every day. It's fine.
by stickfigure
5/11/2026 at 10:21:56 PM
I’m not disagreeing with that point: I’m just pointing out that the idempotency key must be persisted in your own system before the payment is submitted. Some gateways (Braintree) will let you submit no key and just give you one on request completion.My point is that you can’t rely on this specific mechanism because request failure does not mean the payment is not going through!
I’m not sure where we disagree so I must ask: do you disagree with what I’ve written and, if so, what and why?
by scott_w
5/12/2026 at 11:40:41 AM
If the idempotency key must be persisted, that is the bookkeeping that has to occur to consider the request a success.by jmalicki
5/12/2026 at 2:10:50 PM
Ok, so I’m stating that the persisting of that key must happen before the call to the payment gateway happens and regardless of whether that call succeeds or even completes. Is this something we agree on?by scott_w
5/10/2026 at 10:23:00 PM
And if there is part of the process that isn't idempotent, you want to make that surface area as small as possible, so that only failures at that one discrete step can cause issues.by jmalicki
5/10/2026 at 3:35:38 PM
That's usually solved with traditional database transactions.Even if you have a complex long-running multistep orchestration problem, you can break it down into simpler transactions. Eg you could start with a "lock the resources" txn.
But 99% of these conversations around idempotence are simple POST operations like "create order" that regular old database concurrency management handles just fine.
by stickfigure
5/10/2026 at 3:36:36 PM
> That's usually solved with traditional database transactions.That doesn't answer my question. What response do you return to the client in the case I described?
by pdonis
5/10/2026 at 3:42:13 PM
This is just normal concurrent programming? If two requests come in for the same idempotency key/customer reference id, only one will succeed. Use standard database transaction isolation.So one will complete with 200, one will complete with 409. It doesn't matter which.
That said, there's something odd about the way you phrased this question. If the original request hasn't gotten a response yet, why is it sending a retry? What you're asking is more general: What happens when two conflicting requests come in? This is something we've been solving with RDBMSes since the 1970s.
by stickfigure
5/10/2026 at 6:31:56 PM
> That said, there's something odd about the way you phrased this question. If the original request hasn't gotten a response yet, why is it sending a retry?Because it hasn't gotten a response yet. That's got to be far and away the most common reason any request gets retried in any context.
by thaumasiotes
5/10/2026 at 4:32:16 PM
So you have to serialize the requests, and have one of them wait for the other to finish to return the 409?> why is it sending a retry?
may be two clients tries to do it? Or there's a bug with the client in how they do it?
Isn't the point of idempotency meant to enable clients to retry again, without fear that a 2nd request somehow breaking things?
by chii
5/10/2026 at 5:04:14 PM
By definition we have to serialize something somewhere so we can decide which request is the success and which request is the duplicate. There's nothing special about the case of retries, this is standard concurrent programming 101. Two conflicting requests come in, which one wins?You absolutely must wait for one request to finish before any other request can return a 409. 409 is a signal to the client that they can stop retrying, the job is done. If some request returns 409 early and the "original" request fails, you will not get further retries and the message will be lost.
Stripe's approach requires serialization as well. Only one request can succeed. If you send multiple conflicting requests in simultaneously, some of those have to block.
The good news is that we have been solving this problem for decades and we have incredibly well refined tools - database transactions and isolation levels - for solving this problem.
by stickfigure
5/10/2026 at 5:21:47 PM
In my opinion, the idea of idempotency is to accept both requests, but only one is actioned (and the requester is non-the-wiser about which). Otherwise, you're just recreating database transactions - something that doesn't need to be named idempotency.And you haven't considered multiple servers in your scenario - what if two requests meant to be idempotent with each other arrived at different servers?
by chii
5/10/2026 at 5:48:01 PM
How would the requester get notified if it doesn’t know which request succeeded? Is it listening for events?And at the sake of repeating the above commenter, you solve the multiple server by serializing somewhere, because you ultimately need a lock on something. You can also perform the operation in both places and then reconcile the state later but that’s a lot more complex.
by yladiz
5/10/2026 at 8:48:04 PM
The requester doesn't want to know which request succeeded, because they are duplicates and one is a retry!When you are using TCP, and you send the same data twice because of a delayed ack, you likewise don't care if the ACK is for the first time or the second time you sent the data. You just know the other side got the data, and that's all you care about.
by jmalicki
5/10/2026 at 6:37:57 PM
> How would the requester get notified if it doesn’t know which request succeeded?By sending a third request and getting a response that reveals the state of the system.
by thaumasiotes
5/10/2026 at 7:05:30 PM
Seems like more overhead than just getting a response from the initial request.by yladiz
5/10/2026 at 11:11:29 PM
If you get a response from the initial request, then you do know whether it succeeded.by thaumasiotes
5/10/2026 at 6:51:31 PM
> So you have to serialize the requests,Not necessarily - there are different transaction isolation and conflict resolution methods provided by every database built for this purpose. You just have to ensure that only one request actually commits to the database, and that one sends a success response while the other sends a 409. The database or another lock provider can either help enforce serialization up-front - or the app can use optimistic locks based on data in the request that will only block if there is actually a conflict, and this won't delay the first transaction at all.
Solving these kinds of issues are exactly the purposes of idempotency keys and database transactions and using them in the intended way is really the only sound way to build a distributed system. Making things more complicated to "improve DevX" is just going to make them unsound. That is what Stripe chose to do. Their 24-hour replay idea is fine but why not send 409s after that rather than accept those transactions? If "that will never happen" then the 409s will never happen. It would have cost approximately nothing (if designed that way upfront) and inconvenienced their clients not at all.
by jeremyjh
5/10/2026 at 4:02:52 PM
> If the original request hasn't gotten a response yet, why is it sending a retry?Um, because connections over the Internet aren't 100% always on? Because packets can get lost? Because computers sometimes have to reboot?
You're assuming that the client will always receive whatever response your server finally sends, and that the client will wait indefinitely to receive a response. Neither of those things are true. So the client can be in a state where it sends a retry because it got no response and doesn't know why. And that means a retry request could come in while the first one is still being resolved--because the client had a timeout or it rebooted or something else happened that made it lose the connection state it previously had. That's the case I'm asking about.
by pdonis
5/10/2026 at 5:33:19 PM
I'm not assuming anything. Let me try to reframe this for you.The case of "client sends a retry with the same idempotency key" generalizes to "multiple requests come in for the same idempotency key". These can come in spread out over time (like a traditional loop), or they could come in at once. The solution is the same either way.
The problem of "how do we deal with multiple conflicting requests coming in at once" is something we have been dealing with for decades. We have databases with transactions and isolation levels. If I said in an interview "make an endpoint that inserts a value in a database and returns an error if the value is a duplicate", any competent backend web developer should be able write it without Claude's help. Concurrency is part of our life.
Whether you want to return 409 or replay the success is irrelevant to this question. You must serialize the idempotent operation on the server, because you can have multiple requests coming in simultaneously. If you put the operation in a database transaction with an appropriate isolation level, you are most of the way there.
by stickfigure
5/10/2026 at 6:13:44 PM
> I'm not assuming anything.Sure you are. You said:
"Retries will only receive 409 if the original request was successful. If the original request failed, the server performs the operation as normal on the second request. It doesn't replay failures."
I understand all that just fine; you don't need to keep trying to "reframe" it. But what you said that I just quoted above assumes, implicitly, that if you get a second request with the same idempotency key, the original request has either failed or succeeded--because you don't even address the case where neither of those things are true. I'm asking you to address that case.
If your answer is "that will never happen", I disagree, and I explained why in response to your question about why the client would send a retry if it hasn't received a response to the original request. You could answer, I guess, that you still think that would never happen--and I would still disagree. But at least that would be an answer. So far all you've done is "reframe" something that I already understand and wasn't asking about.
by pdonis
5/10/2026 at 7:19:07 PM
The other cases are the original request is still in flight or never occurred. The former case was explained by the prior comment, one request is processed, the other is returned by 409. The system cares little for which is which and neither should the caller. The latter case is handled by clients retrying until a request is received, at which point one of the other three states takes over.Whether or not a prior request exists in the system in processed or unprocessed state should not matter in a properly implemented idempotent system, the whole point is that one and only one is processed, and all replicas indicate that they are such.
What you do inside of your boundary to implement that idempotent contract need not be part of the contract and the decision of what primitives to use (locking, content-based addressing etc) are mainly just a question of implementation constraints.
by graemefawcett
5/10/2026 at 9:20:49 PM
> The other cases are the original request is still in flight or never occurred.I'm not sure what you mean by "in flight". The case I'm asking about is where the original request was received by the server and is being processed--and then a second request comes in with the same idempotency key. The original request has not succeeded, and has not failed--it's still in process. What response does the second request get? I do not see an answer to that question anywhere in this thread.
by pdonis
5/10/2026 at 10:01:56 PM
The answer is "the same thing as every other concurrency conflict between two requests". In modern backend development this is most commonly handled by the database, and the practical result is that (from the client's perspective) the requests will block, and only one will "actually succeed".Here's a typical example, assuming serializable isolation in a database that uses optimistic concurrency.
* Two simultaneous requests come in to create a payment.
* The requests provide an idempotency key that is expected to be unique (possibly scoped to a tenant).
* The first request starts a transaction and starts processing, everything looks good - no dups.
* The second request starts a transaction and starts processing, everything looks good - no dups.
* The first one commits and returns success.
* The second tries to commit, but a conflict is detected (the first txn committed first). Typically this causes the second transaction to retry.
* On retry, the second transaction detects the duplicate.
The only question here is what happens when the second transaction fails? The Stripe model is "look up the original response and hand that back to the client". An equally valid and much easier to implement solution is "return a response that tells the client that there was a conflict".
Both solutions offer "create payment" as an idempotent operation.
by stickfigure
5/10/2026 at 10:59:58 PM
> The second request starts a transaction and starts processing, everything looks good - no dups.So when the second request comes in, even though it has the same idempotency key as the first request, the server doesn't check to see if there's already a request received with that idempotency key?
That would seem to defeat the whole purpose of idempotency keys.
> On retry, the second transaction detects the duplicate.
So at this point, the second request would return a 409 code (or something like that) to the client?
by pdonis
5/11/2026 at 5:52:30 AM
There are a lot of ways to implement this, so I posted an example with one of the most common ways - a database which uses optimistic concurrency in serializable isolation level. Postgres is often configured this way, though it's not the only way it can be configured.With optimistic concurrency models, collisions are only detected at commit time. Two transactions can simultaneously update the same data; each update will "succeed"; when they try to commit, only the first one will succeed. The second one will fail with a code that indicates a collision. Standard practice is to just retry the transaction.
In serializable isolation, every transaction sees the state of the database frozen in time at the start of the transaction. They don't see each other's writes (that would be "read committed"). So if you have two transactions simultaneously which do "check if value XYZ exists; if it doesn't exist, insert it" they will both run the insert. The collision will only be detected when the second transaction tries to commit.
There are many other ways to implement this, but this is a pretty common approach.
>> On retry, the second transaction detects the duplicate.
> So at this point, the second request would return a 409 code (or something like that) to the client?
Yes. Stripe's approach is not fundamentally different; they just lookup the original request and return that response body instead of returning an error. It's more work for the server side engineers (and has a bunch of complex but obscure failure modes) but all the underlying database behavior is the same.
by stickfigure
5/11/2026 at 3:14:57 PM
> There are a lot of ways to implement thisSure, I get that. What I don't get is why you would be using idempotency keys as part of the implementation if you're going to go ahead and start a second transaction when you get a duplicate request, and not even check the idempotency key, and let your database tell you you've got a duplicate when you try to commit the second transaction. This subthread is specifically about implementations that use idempotency keys, since that's what the article is about.
by pdonis
5/11/2026 at 8:07:46 PM
Update: stickfigure basically answered this in another subthread: as I understand the answer, it is that you do check the idempotency key, but inside the DB transaction, so you have to start a new transaction on every request. If, inside the transaction, your idempotency key check shows that another request with that key was already received, you don't do anything to change the DB inside the transaction and just commit it as a no-op.by pdonis
5/11/2026 at 8:26:51 AM
"a database which uses optimistic concurrency in serializable isolation level. Postgres is often configured this way, though it's not the only way it can be configured."It's not the default (read committed is) and I never saw serializable being set in actual production systems. You can do it, but then you have to be able to retry all of your transactions, including read.
What if the task you do take 5 minutes? 30 minutes? 10 hours? Do you create long transaction, blocking all reads?
by ahoka
5/11/2026 at 12:56:44 PM
> It's not the default (read committed is) and I never saw serializable being set in actual production systems.It's not the common mode of deployment, but it's definitely in prod use.
> You can do it, but then you have to be able to retry all of your transactions, including read.
Pure read transactions shouldn't need to be retried in postgres due to serialization errors. You need to have read-write dependencies for that.
That's not to say that effectively read only transactions aren't affected by serializable, you do need to record the necessary metadata for the serialization logic to work.
FWIW, if you know your transaction is read only and long running, you can start a transaction with START TRANSACTION READ ONLY DEFERRABLE, which makes the start transaction slower, but then does not need to do any work related to serializable while the transaction is running.
by anarazel
5/11/2026 at 2:54:05 PM
> I never saw serializable being set in actual production systemsEvery major prod system I've worked on in the last 15 years ran in serializable, including my current charge which processes tens of billions of dollars annually. YMMV but this is quite common in serious production systems. Google's Spanner only runs in serializable.
It doesn't matter though. I could write the sequence out with a SELECT FOR UPDATE and the second request will block instead of retry. The client experience is the same; the "second" request blocks. @pdonis wanted an example so I picked one.
by stickfigure
5/10/2026 at 6:09:49 PM
You're asking questions that broadly summarize as: "what if we had an idempotency key [which must work concurrently to be useful], but it didn't work concurrently?"Idempotency keys are themselves the solution you're looking for. If they don't work concurrently, they aren't idempotency keys. Your response in races or duplicates doesn't inherently matter in that sense, pick whatever semantics make sense for your system.
by Groxx
5/10/2026 at 9:24:04 PM
> You're asking questions that broadly summarize asNo, I'm asking one question, which doesn't seem to be summarized by your summary.
The situation is that your server has received two requests with the same idempotency key. For the first request, one of three things could be true: it could have succeeded, it could have failed, or it could still be in process.
The original post I responded to said what response the second request gets if the first request succeeded and if it failed. But it didn't say what response the second request gets if the first request is still in process on the server--so it hasn't succeeded and it hasn't failed. I do not see an answer to that anywhere in this thread.
by pdonis
5/10/2026 at 9:38:15 PM
If the first is still in progress and a second arrives, you can wait for it to finish (and then return whatever would be useful, e.g. you could replay the first request's response), or fail the second (409, 423, 429, there are a number of codes that could apply) to tell the caller to retry later.So yeah, you can do basically anything that isn't inconsistent. Success, fail, delay, don't respond until timeout, all are valid as long as you don't double-apply. Most concurrent systems are like this in some way, because all successes can become errors, and all responses might never arrive. It has nothing really to do with idempotency.
by Groxx
5/10/2026 at 3:40:28 PM
The lock would normally make the second request wait, aka not return a response, until the first one is done. Then it sees it's a duplicate and returns that. Or it times out and returns an error. Then the client hopefully have some exponential back off strategy, so the third attempt doesn't suffer the same fate.by matsemann
5/10/2026 at 4:05:23 PM
If a transaction is locked then subsequent requests would return a 409, ideally with an error message indicating that it's currently being processed.by MattDaEskimo
5/10/2026 at 5:22:18 PM
To be honest, I liked your original response about returning a 409 - it's not something I'd done before and I like how it keeps things simpler.But your follow up responses here are making me rethink. Now you have to have all these special cases where the original request is still in process. I think or assertion of "99% are simple POST operations" is bullshit. For the times where idempotency is hard and really matters, often times you're calling a third party API, like a payment processing API.
I would think a better approach would be to always return a 409 on a subsequent request, regardless of whether it passed or failed, and then have a separate standard API that lets you get the result of any request by its idempotency key.
by hn_throwaway_99
5/10/2026 at 7:15:43 PM
Idempotence already requires some client thinking if you want to conform to HTTP specs.I.e. idempotent DELETE with proper protocol behavior requires that one request see the 200 OK or 204 No Content and the other sees 404 Not Found, because the delete has already happened. It would be misleading to say 200 OK to both, because that answer means the resource was there when the request arrived.
Honestly, the whole HTTP resource model has a different conceptual backing for state management than the independently developed "idempotence" concepts in distributed systems. Those non-HTTP concepts came from more message-based rather than resource-based architectural assumptions.
The cleanest mapping in the spirit of HTTP would be that you do multiple round trips. A POST creates a new idempotence context, a bit like "start a transaction". The new URI is the key for coordinating state change and allowing restart/recovery.
As I remember it, the idea of idempotence keys in headers really came from the SOAP RPC mindset. It's kind of funny to see it persisting in some hybrid SOAP + REST mental model.
by saltcured
5/10/2026 at 7:37:35 PM
> The cleanest mapping in the spirit of HTTP would be that you do multiple round trips. A POST creates a new idempotence context, a bit like "start a transaction". The new URI is the key for coordinating state change and allowing restart/recovery.I think that gave me "Enterprise Java Beans PTSD". I.e. an over-engineered solution that adds complexity for both the client and server in the name of some sort of "protocol purity".
People bolted on idempotent semantics onto HTTP because it wasn't provided natively by the protocol, so I don't think it makes sense to go through some hoop-jumping gymnastics for the sake of conforming to a spec that doesn't describe the necessary semantics in the first place.
by hn_throwaway_99
5/11/2026 at 4:28:04 PM
FWIW, I'm not particularly fond of HTTP. But there is PTSD in both directions. Doing random things ignoring (or subverting) "protocol purity" often create disastrous effects when they haven't considered how the larger system will behave when you have various middleware bits that are essentially obeying different protocols while superficially claiming to use an interoperable standard.When I let myself ruminate, it irks me that we all let HTTP become the defacto "internet protocol" just because of firewalls. Because there was a cargo cult idea that HTTP is benign and so one of few ports allowed almost everywhere, we do stupid contortions to squeeze every protocol through an HTTP tunnel.
These short-sighted acts of laziness accumulate into HTTP everywhere. And of course, the firewall is nearly pointless when "everything" is going through that one hole anyway.
by saltcured
5/10/2026 at 5:35:21 PM
> special cases where the original request is still in processThis isn't a special case, and it's the same problem if you want to replay the original response on conflict. If the original request isn't complete, what are you going to replay?
by stickfigure
5/10/2026 at 6:19:07 PM
> If the original request isn't complete, what are you going to replay?Who says you have to replay? If you get a second request with the same idempotency key, and the original request is still in process, why not just send the client a response that says so?
by pdonis
5/11/2026 at 5:28:57 AM
It's not a terrible question. It would be complicated to implement and isn't more useful than using the database's consistency model, so nobody does it that way.Long running transactions create all sorts of problems, so transactions are generally expected to be short. The actual work behind "create payment" or "create order" is generally fairly trivial - more or less insert a row in a table. There's no good reason to make the API complicated... you either "win" at concurrency or you lose, and the difference is generally sub-millisecond. The only meaningful thing you need to communicate to the client is "you're done" (for both the win and lose cases) or "you need to try again" (for the "something unexpected went wrong" case).
Complicated workflows can certainly have multiple steps, with "fetch the current status" calls in between. But somewhere near the beginning of every complicated workflow there will be a call to "create workflow" and it will need to have sort of mechanism which allows clients to call it idempotently. Otherwise you end up with multiple starts.
I've literally received duplicate products in the mail because of this kind of problem. I've also sent multiple products in the mail because services I relied on didn't offer the necessary idempotency mechanisms.
by stickfigure
5/11/2026 at 11:29:42 AM
> The actual work behind "create payment" or "create order" is generally fairly trivial - more or less insert a row in a table.It's generally insert a row in someone else's table, over the wire, 50ms+ away. They might not even be using an RDBMS.
by mrkeen
5/10/2026 at 7:14:37 PM
That is my point. When you are doing "normal" idempotency where you do the appropriate locking and keep around a table with ongoing request status and the result that you can return on a subsequent duplicate request, you handle all these cases. But in your "409" version of it, you haven't really saved much complexity on the server because you still need to keep around all that info if you're not just returning a 409 if you get a second request while the first is in progress.by hn_throwaway_99
5/12/2026 at 5:19:33 AM
I don't understand what you're saying. I can take any cheap rdbms, put a unique constraint on a column, and make my API return 409 for conflict vs 200 on success. There's so little code involved that it's embarrassing to charge money for it.by stickfigure
5/10/2026 at 10:28:56 PM
You have a transaction row lock and you tell the subsequent requests to f off. I would go as far as returning a 429.Devs are too scared to be nice (ie not return errors) to clients when they misbehave.
by turtlebits
5/10/2026 at 3:47:53 PM
You are improvising, and in the process, changing the semantics of a well established design pattern. Do not recommend.by whoamii
5/10/2026 at 4:22:32 PM
Your comment contributes nothing to the discourse. It's FUD.The pattern I describe was the dominant design pattern for financial transaction processing systems before Stripe. Stripe's API makes life for the clients slightly easier at the expense of making life for servers more complicated, but the two approaches are equivalent in function.
by stickfigure
5/10/2026 at 5:12:55 PM
The topic is about idempotency though, and what you are describing is not idempotency. You are describing something different, and arguing “but it accomplishes a similar thing.” It appears you have come to the same conclusion as the article that building idempotency isn’t trivial.by whoamii
5/10/2026 at 5:45:39 PM
It absolutely is idempotent behavior of the system. The goal is "make an idempotent payment", and this approach ("return an error for duplicates") was standard for financial APIs before Stripe. It still dominates for ecommerce APIs.It isn't complicated, though I can see how if your entire experience with financial APIs is Stripe, you might not be aware of how simple it is. Because Stripe's approach, while mildly more convenient for clients, is a PITA to implement properly.
by stickfigure
5/10/2026 at 7:36:52 PM
You seem to believe idempotency is a fancy word for avoiding duplicates, which is why we seem to be having different conversations.by whoamii
5/11/2026 at 6:01:42 AM
Idempotence is a fancy word for being able to repeat an operation without changing the outcome.You seem to think that it's important to make the specific bytes of an http request-response idempotent.
I think that it's important to make a business operation idempotent.
You're missing the forest for the trees.
by stickfigure
5/10/2026 at 11:27:48 PM
You provide in the response body a JSON blob or something that indicates a detailed error code, like DUPLICATE_IDEMPOTENCY_KEY, with some more information that points to the record ID of the corresponding transaction that the client can fetch.Sure, a strict reading of "idempotence" might require that the response for subsequent requests be identical to the first, but for practical concerns, what matters is the API contract you define, document, and adhere to. The purpose of idempotence is to ensure that you don't end up with duplicate transactions. That's what actually matters. How that's represented in the protocol is an implementation detail.
by kelnos
5/10/2026 at 3:56:42 PM
If you're a client using the same idempotency key for a materially different request you have a bug.by hamdingers
5/10/2026 at 4:54:51 PM
Yes, and if you are building a payment API you need to be robust to client bugs.by theptip
5/10/2026 at 6:00:40 PM
Rejecting the conflicting request is being robust to improperly reused idempotency keys. There's no other reasonable answer.by anamexis
5/10/2026 at 7:10:45 PM
The client is part of a distributed transaction. It can't be oblivious to this. Clear semantics and accurate adherence to them is the only answer that doesn't make the overall system unsound. Client bugs are expected and so the simplest semantics that ensure data integrity and accurate responses are the best way to help them identify and fix their bugs.by jeremyjh
5/10/2026 at 2:50:41 PM
The 409 should come with an id or a url the client can use to find the original result.by onionisafruit
5/10/2026 at 3:00:03 PM
...and if you're using the approach of "let the client pick ids", you don't even need that. The client has everything it needs.by stickfigure
5/10/2026 at 7:54:36 PM
I really like that article, thank you for sharing it again. I wish I had read it a decade ago, or even the first time you submitted it - I had to learn some of these things the hard way.I agree with you - I don't think Stripe has made the right choices here and its unfortunate that it has inspired so many other people to make the same poor choices. I don't agree that their system is as sound as always returning 409s. I think having a short window where you return response bodies is fine, but after that they should still be sending 409s. If no one will ever actually resend a request after 24 hours how is it not fine to send 409s when they do? They've chosen to implement the more expensive choice and then not back it with the cheapest one.
by jeremyjh
5/10/2026 at 3:57:58 PM
I really like this approach, and am going to keep it in my back pocket.However, it's good for e-commerce where there are a subset of "important" operations, but I'd argue the idempotency key is better for a financial API like Stripe where the majority of your operations need these semantics.
You also run into a problem if PUT/PATCH needs to be exactly-once instead of at least once. Not as common, but again, might be something you run into with a financial API.
by pverheggen
5/10/2026 at 4:45:49 PM
There's nothing special about any particular HTTP method; it is the inherent nature of distributed systems that you can not have exactly-once semantics with a single HTTP request. To create exactly-once semantics, you need a client which retries until success and a server which prevents duplicates.The only difference between the approach I'm describing and Stripe's approach is the detail of how the client knows that it's done. "My" mechanism (notify the client that a request is a duplicate) was dominant in financial transaction processing systems before Stripe; I used probably a half dozen of them back in the day.
Stripe came along and from the beginning decided "we want to compete based on making a friendly API". They succeeded, and if you ever look at Paypal's original API, it's easy to see why. It's true that repeating successful API responses makes life slightly easier for clients; they never need to check for a 409 (or whatever) response. It comes at a cost of making life quite a bit more complex on servers. Personally I don't think the tradeoff is worth it, but YMMV. If your single competitive advantage is "easy API" maybe it makes sense. If you're normal B2B, almost certainly not.
by stickfigure
5/11/2026 at 4:39:47 PM
FWIW, I enjoyed your engagement throughout the thread above, fighting the good fight.I think there is also a risk of an "easy API" when it leads to magical thinking and sloppy development. If the naive client programmer starts to think the reliability is handled for them, they may also flub the handling of the idempotence key that remains the crux here. E.g. not persisting them well enough and returning to a situation where the user can accidentally make duplicate payments just like the naive system with no idempotence feature...
by saltcured
5/10/2026 at 10:22:07 PM
Exactly - junior engineers haven't yet developed a good taste of "how much the system should try and deal with errors" - so they always go way overboard to guess and try and fix errors way too much. I agree with you - this is a client error - 409 and make them fix the bug on their side. Clean, reliable, simple, separation of responsibility.by malux85
5/10/2026 at 4:09:14 PM
And a good way to convey this contract is force the clients to hash-chain the calls, so is now more clear that it should do (and even can detect the send will fail at client side)by mamcx
5/11/2026 at 7:03:24 AM
Thank you, like and agree with the rest of the rulesby fuzzythinker
5/11/2026 at 12:02:57 AM
> If you see a duplicate idempotency keyI'm no expert but an "idempotency key" already has some major smell to it.
by kgwxd
5/11/2026 at 8:06:03 PM
Then learn from the experts, maybe? https://docs.stripe.com/api/idempotent_requestsby yencabulator
5/11/2026 at 6:14:04 AM
Why? I've used plenty of systems that have an idempotency key. E.g. many payment processors will take an order id and won't let you charge an order id more than once. That's just an idempotency key by another name.by bcrosby95
5/10/2026 at 9:59:36 PM
This is because there are countless tutorials and slop on the internet that says, no, instructs, you to handle idempotency on the client instead of where it belongs. Server-side.by reactordev
5/10/2026 at 2:31:30 PM
[dead]by Pxtl