TradingView gives your server three seconds to answer and cancels the request if it takes longer. Its documentation also says a webhook may occasionally fail to reach the URL you set. What it does not publish is what happens next.
What we see on our own webhook endpoint is the same alert body arriving twice. The second delivery is byte-identical to the first. Same payload, same alert, nothing in it that says "this is the second attempt." A router that treats every POST as a new order sends two contracts, and you find out when your position is twice the size you planned.
The fix isn't a faster server. It's an idempotency key derived from the alert itself, plus a durable fence on the router, not on the broker.
What TradingView actually does when your server is slow
TradingView cancels a request your server does not answer quickly enough, and it says a webhook may occasionally fail to reach the URL you set. What we observe on our side is the other half of that: the same body arriving at that URL a second time. Your server being slow is invisible to TradingView either way, which is why sorting out a second arrival is entirely your problem.
It is worth separating what TradingView publishes from what you can only observe. TradingView's own webhook documentation states that "if a remote server takes longer than three seconds to process a request, the request will be cancelled", that only ports 80 and 443 are accepted, that IPv6 is not supported, and that webhooks may occasionally fail to reach the specified URL. It does not publish its retry mechanics. So build for the behaviour, not for a documented retry count: assume the same body can arrive more than once, and assume you will never be told which arrival is which.
Three seconds sounds generous until you look at what a naive router spends them on. TLS handshake. JSON parse. Looking up which account this webhook belongs to. Then the part that actually takes time, which is the round trip to your broker: contract lookup, order placement, waiting for an acknowledgement. Do the broker call before you answer TradingView and you have put a network call to a third party inside a three-second budget you do not control.
Answer first, trade second
The structural fix is to stop doing the work inside the request. Validate the payload, claim the order, write it down, and answer. Everything after that happens on a queue behind the response. That is how we run it: the webhook endpoint returns an acknowledgement carrying the order id and a queued status, and the broker submission happens on a worker that owns retries, backoff and the broker's own rate limits. That worker never resends an order placement after a timeout, and the fence is written before it ever calls the broker, so nothing on that path can turn one alert into two orders. The alert gets its fast answer regardless of what the broker is doing.
This also explains why a bigger server rarely fixes duplicate fills. The slow part is almost never your CPU. It is the broker, or a cold connection, or a moment of network weather between two clouds. You cannot make that reliably sub-three-seconds. You can make it irrelevant.
Not every double order is a retry
Before you build anything, work out which of these you actually have. They look identical in your broker's fill list and they need different fixes.
| What happened | What the payloads look like | What fixes it |
|---|---|---|
| Your router answered slowly and the same body arrived again | Byte-identical bodies, seconds apart | A content-derived key, or answering faster than the timeout |
| Your strategy genuinely fired twice on the same bar | Two different alerts, often different prices or order ids | A fix in the Pine script, not in the router |
| A repainting indicator re-evaluated the bar and fired again | Similar bodies, same bar, different fire times | A key anchored to the bar, plus non-repainting alert conditions |
| You have two alerts pointing at the same webhook | Byte-identical bodies, arbitrary spacing | Delete one. No router-side gate should hide this from you |
Only the first and the last are duplicates in the sense this article means. The middle two are your strategy telling you something true, and a router that silently swallows them has made your backtest and your live account disagree in a way you cannot see.
Why “the broker will dedupe” is a bet you lose
Brokers deduplicate on their own terms, usually against a client order id you have to supply correctly, and sometimes not at all. Two identical market orders a second apart are an ordinary thing for a broker to accept, because sometimes that is exactly what a trader meant.
Look closely at what a client order id is. It is an input you generate. If your router mints a fresh one per POST, which it must if the two orders are ever to be told apart in your own records, then the broker sees two distinct orders and accepts both. It did not fail. It did what you asked, twice.
Even where a venue does enforce uniqueness on that id, the guarantee is narrower than it sounds. The uniqueness window is the venue's to define, not yours, and it typically stops applying once the original order reaches a terminal state. An id that was rejected as a duplicate at 09:31 can be accepted at 09:33 because the first order has since filled. You cannot build a safety property on a window somebody else can change without telling you.
We do not rely on broker-side deduplication anywhere in our pipeline, and we would not advise anyone else to. The fence is ours, it is on our side of the wire, and it is written before the broker is called.
The retry that creates the duplicate is often your own
Here is the subtler version of the same mistake, and it is the one that bites teams who have already solved the inbound problem. Your router calls the broker to place an order. The call times out. Did the order reach the venue? You do not know. A timeout tells you nothing about what happened on the other side, only that you stopped waiting.
Retry that call and you may have just placed a second order. So we do not. A timeout on a mutating order endpoint, placing an order, placing a bracket, liquidating a position, is never retried automatically. It is recorded as an unknown outcome and resolved by reading the broker's own order list, which is the only source that can actually answer the question. Timeouts on reads, and rate-limit responses, are retried freely, because replaying a read cannot create a position.
The rule underneath all of this
A retry is safe only when the receiver can prove it already did the work. Not guess. Prove, from something it wrote down before it started. Everything in the next section is an application of that one sentence.
How to make a retry harmless
Derive a key from the alert's own content and the account it targets, write the fence before you call the broker, and make the fence durable enough to survive the process that wrote it. The second POST then resolves to the first order instead of creating a new one.
Concretely, in the order you should build them.
1. Give the alert an identity in the alert itself
The cleanest key is one TradingView stamps into the message body when the alert fires, because it is then identical across every delivery of that fire and different for the next one. TradingView's alert placeholder documentation gives you the pieces: {{ticker}}, {{interval}}, {{time}} for the bar's trigger time, {{timenow}} for the moment the alert fired, and the strategy fields including {{strategy.order.id}}.
{
"symbol": "ESZ5",
"action": "buy",
"quantity": 1,
"orderType": "market",
"takeProfitPrice": 6012.25,
"stopLossPrice": 5998.50,
"idempotencyKey": "{{ticker}}-{{interval}}-{{time}}-{{strategy.order.action}}"
}We read that field as idempotencyKey. The first POST claims it. A later POST carrying a key that has already been claimed is refused rather than placed, and the refusal says so in words your logs can act on.
HTTP/1.1 409 Conflict
{
"success": false,
"error": {
"code": "DUPLICATE_REQUEST",
"message": "Request with this idempotency key already processed"
},
"timestamp": "2026-09-14T14:32:07.114Z"
}Choose the key's ingredients deliberately, because they decide what collapses. Anchoring to {{time}} means two entries on the same bar in the same direction collapse into one, which is exactly right for a repaint and exactly wrong for a strategy that legitimately scales in twice on one bar. Anchoring to {{timenow}} collapses redeliveries of one fire and nothing else. Both are defensible. Picking one without noticing you picked it is not.
2. If you cannot add a key, hash the body
Some setups cannot edit the alert message, and third-party signal sources will not change theirs for you. Then derive the key yourself from the exact bytes you received plus the webhook destination they arrived on, and hold it briefly. A redelivery is byte-identical and lands inside the window. A legitimate repeat signal is minutes away and does not.
That is the gate we run first, before anything else touches the order: a hash of the destination and the raw body, held for a window measured in seconds rather than minutes, claimed atomically so two simultaneous deliveries cannot both think they were first. The second delivery gets the first delivery's order id back and a success response, because as far as the sender is concerned its alert was accepted, and it was.
3. Write the fence before you call the broker
This is the part most implementations get subtly wrong. It is not enough to record that an order exists. The marker that blocks a replay has to be written by the same process that makes the broker call, and committed before the call leaves. Otherwise a queue redelivery arrives, reads a state that was set before the work was handed over, and cannot tell its own first attempt from a second one. Both look identical from there.
Durable matters too. A fence that lives in one process's memory disappears with the process, and processes restart for reasons that have nothing to do with you: a deploy, a scale-in, a host going away. The fence has to be somewhere that outlives the thing that wrote it, and it has to be claimed with a compare-and-set so that two workers racing for the same order produce one winner and one no-op.
When a process does die between the fence and the acknowledgement, the recovery is not a guess either. The next sweep finds the claim with no outcome attached, asks the broker what it actually has for that account, and reconciles to that answer: one match means record the fill, no match means the order never landed and can be failed honestly, ambiguity means leave it alone and surface it rather than resolve it wrongly.
4. Decide what a duplicate should return
There are two defensible answers and we use both, depending on which gate caught it. A content-hash hit returns success with the original order id, because the sender asked for an order and there is one. An explicit key that was already claimed returns a conflict, because the sender asserted uniqueness and we are telling it the assertion failed. What matters is that neither answer is a new order, and that both are distinguishable in your logs from a rejection.
5. Fence the position, not just the payload
Content hashing has one blind spot, and it is a common one. Two alerts that mean the same thing but do not say it identically, one contract and then two contracts, the same entry with a different tick of price, produce different hashes and sail through. So the last gate is not about the message at all. It is about the position: a same-direction claim per account and symbol, held while the first order is in flight, so a second same-direction signal for the same instrument is refused while the first is still resolving rather than stacked on top of it.
That is also the gate that answers the question everyone eventually asks, which is what happens when a duplicate arrives after the first order has already filled. At that point it is not a duplicate order any more, it is a position-sizing decision, and it belongs to the layer that knows what you are already holding. If you want to see what that layer looks like from the trader's side, the manual controls are the same machinery with a human at the keyboard.
6. Prove it, on your own account
None of this is real until you have watched it work. Fire the same alert twice by hand, one second apart, into a simulated account. You should see one order. Then restart whatever is holding the fence and do it again. You should still see one order. A dedup design that has only ever been reasoned about is a dedup design that has not been tested.
- Point the alert at a simulated or evaluation account you do not mind marking up.
- Send the identical body twice, seconds apart, and confirm exactly one order exists at the broker.
- Send it twice again with the two deliveries several minutes apart, and confirm you get two orders. A gate that suppresses this is too wide.
- Restart the router between the two deliveries and confirm the first result still holds.
If you would rather not build and prove all of this yourself, every one of these gates is already running behind our TradingView automation.
Checklist
- Your router answers inside TradingView's three-second limit, so the request is never cancelled.
- Two identical alerts one second apart produce one order. Test it. Don't assume it.
- The dedup fence survives a restart of the process that wrote it.
- A rejected order tells you why, and can be retried without re-firing the alert.
- Your broker call is never retried automatically after a timeout on a mutating endpoint.
- Two alerts minutes apart still produce two orders, so the gate is not hiding real signals.