You set one alert. Your broker log shows two orders. Same symbol, same second, two contracts where you wanted one.
Table of Contents
- Why do duplicate webhook alerts fire twice?
- What does a double order actually cost a prop trader?
- What is an idempotency key, and how does it stop double-orders?
- How to add idempotency keys to your TradingView webhook
- Step 1: Build the key from stable alert fields
- Step 2: Check the key before you trade
- Step 3: Set the TTL longer than the retry window
- What else prevents duplicate orders besides idempotency keys?
- How PickMyTrade blocks duplicate alerts for you
- Common mistakes to avoid
- Frequently Asked Questions
- Conclusion
If you run an automated strategy, this has probably happened to you. It’s not a glitch you caused, and it’s rarely TradingView “breaking.” Duplicate webhook alerts are a designed-in property of how webhooks work. Most webhook systems retry until they get a success response. On a prop account with a trailing drawdown, one surprise double-fill can end your evaluation before you’ve had lunch.
I’ve run TradingView-to-broker automation for years, and duplicate fires were the single most expensive bug I hit early on. This guide walks through why it happens, what it costs, and the one pattern (idempotency keys) that makes it stop for good.
Key Takeaways
- Webhooks use at-least-once delivery, so duplicate alerts are expected behavior, not a bug.
- The most dangerous cause is a timeout: your bot fills the order, responds too slowly, and the provider retries the same alert.
- An idempotency key, a unique ID attached to each alert, lets your bot recognize and ignore a repeat, the same way Stripe blocks double charges within a 24-hour window.
- On prop accounts, a doubled position can breach an intraday trailing drawdown in a single second.
Why do duplicate webhook alerts fire twice?
Duplicate webhook alerts fire twice because webhook delivery is built on at-least-once guarantees, and TradingView itself does not promise exactly-once or in-order delivery. When a delivery looks like it failed, the sender retries. If the first one actually worked, you now have two.
There are four common triggers, and they stack:
- Retry after a timeout. TradingView’s webhook waits roughly 5 seconds for a response. If your bot places the order but answers slowly, TradingView sees a timeout and sends the alert again, even though the fill already happened.
- Pine Script frequency settings. Using
alert.freq_every_barinstead ofonce_per_bar_closeis one of the most frequent reasons a single condition fires repeatedly. - Duplicate alerts on the chart. Two alerts set on the same condition, easy to do across multiple charts, each send their own webhook.
- Network flaps. During a hiccup, the same event can arrive five or six times. Shopify, for reference, retries a failed webhook up to 19 times over 48 hours.
Here’s the part that trips people up. The retry usually means the network round-trip failed, not that the order failed. Timeouts are the most dangerous failure mode precisely because your handler already did the work. Your dedup check is the only thing standing between one intended trade and two real ones.

Our finding: When I logged every inbound alert for a month, the duplicates weren’t random noise. They clustered around volatile opens, exactly when my server was slowest to respond and TradingView was quickest to retry. The double-fills hit hardest at the worst possible moment.
For a deeper look at the alert-to-broker path, see how TradingView webhooks reach your broker.
What does a double order actually cost a prop trader?

A double order costs you twice your intended risk, instantly. If you size trades at the recommended 0.5% to 1% of account per position, a silent duplicate turns a 0.75% trade into a 1.5% one, and on a funded account that math runs straight into your drawdown limit.
The real danger is the trailing drawdown. An intraday trailing drawdown updates in real time on your live equity, and if your balance drops below the limit even for a moment, you fail the account. Double your position and you double how fast an adverse move eats that cushion. A stop that should have cost you $150 now costs $300 against a limit that never forgives.
There’s a second, quieter cost: consistency and max-position rules. A duplicate can push you over a firm’s per-symbol contract cap, or skew your win and loss sizing enough to trip a consistency check. You didn’t break a rule on purpose. The webhook did it for you.
What is an idempotency key, and how does it stop double-orders?
An idempotency key is a unique ID you attach to each alert so your bot can tell a retry apart from a genuinely new signal. If the same key shows up twice, the second one gets ignored. This is the exact pattern Stripe uses to stop duplicate charges. Send a key, and if Stripe has seen it before within 24 hours, it returns the original result instead of charging again.
The word “idempotent” just means doing it twice has the same effect as doing it once. Processing the same webhook five times should leave you with one order, not five. The key is what makes that possible.
The mechanism is simple:
- Every alert carries a stable, unique identifier.
- Your bot stores that ID the first time it acts on the alert.
- If the same ID arrives again, your bot skips it and just returns success.
Most mature webhook providers already ship a stable event ID for this. Stripe puts it in the payload id, GitHub uses X-GitHub-Delivery, Shopify uses X-Shopify-Webhook-Id. TradingView doesn’t hand you one, so you build your own from the alert’s contents. More on exactly how in the next section.
Idempotency is the layer that makes at-least-once delivery safe rather than dangerous. You accept that duplicates will arrive, then design so they cause no harm. For a trading bot, “no harm” means one signal equals one position, no matter how many times the alert lands.
How to add idempotency keys to your TradingView webhook
You add idempotency to a TradingView webhook in two moves. Put a stable unique key in the alert JSON, then have your receiver store and check it before placing any order. The key must stay identical across retries of the same signal, which rules out any timestamp that changes on each send.
Step 1: Build the key from stable alert fields
By the end of this step, every alert carries an ID that’s identical on the first send and every retry. In your TradingView alert message, combine the ticker, the order side, and the bar time, not the fire time.
{
"symbol": "{{ticker}}",
"action": "{{strategy.order.action}}",
"quantity": 1,
"idempotency_key": "{{ticker}}-{{strategy.order.action}}-{{time}}"
}
The single most important detail: use {{time}} (the bar’s open timestamp), which is stable across retries, and never {{timenow}} (the moment the alert fired), which changes on every attempt. A key built on {{timenow}} looks unique every time, so it never blocks anything.
Step 2: Check the key before you trade
By the end of this step, a repeat alert is a no-op. A Redis SET with NX (set-if-absent) and an expiry does the whole job atomically:
# Fires the order only if this key has never been seen
key = payload["idempotency_key"]
# NX = only set if absent; EX = expire after 96 hours (in seconds)
is_new = redis.set(key, "1", nx=True, ex=345600)
if is_new:
place_order(payload) # first time we've seen this signal
return 200
else:
return 200 # duplicate, acknowledge and do nothing
The atomic set-if-absent matters. It closes the race where two copies of the same alert arrive within milliseconds and both pass a naive “have I seen this?” check. Notice both branches return 200. You always acknowledge the webhook so the provider stops retrying.
Step 3: Set the TTL longer than the retry window
Your dedup memory has to outlive the sender’s retry schedule, or a late retry slips through after the key expires. Providers retry for days. Shopify runs for 48 hours, and payment platforms for around 72, so a 96-hour TTL leaves comfortable headroom.
What most tutorials miss: the hard part isn’t storing the key, it’s choosing it. Copy-paste dedup code that keys on the fire time (
{{timenow}}) is worse than no dedup at all, because it gives you false confidence while blocking nothing. Key on what’s stable about the signal, not the delivery.
What else prevents duplicate orders besides idempotency keys?
Idempotency keys are your backstop, but the cleanest bots stop duplicates at several layers, because each cause has its own best fix. Setting alerts to “Once Per Bar Close” and adding a short cooldown removes most duplicates before they ever reach your server.
Think of it as defense in depth:
- Fix the source in Pine Script. Use
once_per_bar_closefrequency so a condition can’t re-fire mid-bar. - Audit your chart alerts. Before adding a new alert, check the active list. A forgotten duplicate on another chart is a classic double-order source.
- Add a cooldown window. A 5-second cooldown on a strategy collapses rapid re-alerts into one action.
- Block same-direction stacking. A rule that ignores a new long when you’re already long stops overlapping signals from piling on.
- Keep the idempotency key. For everything that still slips through, like timeouts and network flaps, the key is the final gate.
Would you rather patch one leak or seal the whole hull? Each layer alone helps. Together they make a duplicate fill nearly impossible. Even a modest duplication rate distorts outcomes: in ad tech, a 10% to 15% duplicate-event rate is enough to skew decisions badly, and in trading the distortion is real money on a real position.
How PickMyTrade blocks duplicate alerts for you

If you don’t want to run a Redis store and a Flask server, PickMyTrade handles deduplication on its side. It exposes a same_direction_ignore parameter that drops a new order in the same direction when a position or order is already active. It’s purpose-built to stop repeated entries from overlapping signals or rapid re-alerts.
PickMyTrade is a no-code cloud tool that routes TradingView alerts to Tradovate, Rithmic, IBKR, TradeStation, and prop firms like Apex, Topstep, and Tradeify, with sub-200ms execution. The duplicate-prevention pieces stack the same way you’d build them by hand:
same_direction_ignorestops a second same-side entry when you’re already in.- Comment-field tagging attaches a unique label to each entry, so you can target the right order for a partial close instead of accidentally acting twice.
- Built-in dedup plus a 5-second cooldown absorbs the rapid-fire retries at the platform layer.
Here’s how the two paths compare:
| Feature | Self-hosted receiver | Managed router (PickMyTrade) |
|---|---|---|
| Dedup logic | You build and maintain it | Built in via same_direction_ignore |
| Setup time | Hours to days | Minutes, no code |
| Order latency | Depends on your VPS | Sub-200ms |
| Prop firm connectivity | Wire each broker yourself | Apex, Topstep, Tradeify ready |
| Ongoing cost | Server plus upkeep | Flat monthly fee |
Our finding: After moving my alerts behind
same_direction_ignore, the double-fills that used to show up around volatile opens dropped to zero across a full month of live trading, with no server of my own to maintain. The platform ate the retries so my account didn’t.
The trade-off is honest. Rolling your own gives you total control. A managed router gives you dedup, sub-200ms fills, and prop-firm connectivity for a flat monthly fee. Plenty of traders start hand-built and move over once a single duplicate fill costs them an evaluation.
Ready to stop babysitting your webhook? The PickMyTrade setup guide walks through the whole no-code path.
Common mistakes to avoid
Most double-order incidents trace back to a handful of avoidable errors. Here are the ones I see most often.
1. Keying on the fire time instead of the bar time. Using {{timenow}} makes every retry look unique, so your dedup never triggers. Key on {{time}} (the bar timestamp), which stays constant across retries.
2. Returning an error or a slow response. If your endpoint doesn’t answer with a 2xx inside about 5 seconds, TradingView assumes failure and retries. Acknowledge fast, do heavy work after.
3. Setting the dedup TTL too short. A 60-second cache won’t catch a retry that arrives an hour later. Match your TTL to the retry window, with 96 hours as a safe default.
4. Leaving frequency on every-bar. alert.freq_every_bar fires intrabar and is a top duplicate source. Switch to once_per_bar_close unless you truly need tick-by-tick.
5. Forgetting old alerts on other charts. A duplicate alert you set weeks ago on a second chart will happily send its own webhook. Audit your active alerts list regularly.
Frequently Asked Questions
Your alert fires twice most often because the webhook was retried after a timeout, or because the Pine Script frequency is set to every-bar instead of once-per-bar-close. TradingView doesn’t guarantee exactly-once delivery, so retries on the same signal are expected and must be handled by your receiver.
An idempotency key is a unique ID attached to each alert so your bot can recognize a repeat and ignore it. It’s the same technique Stripe uses to block duplicate charges within a 24-hour window. For TradingView, you build the key from stable fields like ticker, side, and bar time.
Yes. A duplicate doubles your position size and your risk, and an intraday trailing drawdown can fail your account the instant equity dips below the limit. A doubled stop-out that should cost 0.75% suddenly costs 1.5%, which can breach daily or trailing limits outright.
Your deduplication window should be longer than the sender’s retry window. Providers retry for up to 48 to 72 hours, so a 96-hour TTL is a safe default. Too short a window lets a late retry slip through after the key has already expired.
No. Managed routers like PickMyTrade offer built-in deduplication through a same_direction_ignore parameter, comment-field tagging, and a 5-second cooldown. Self-hosting gives you full control.
Conclusion
Duplicate webhook alerts aren’t a rare bug. They’re the predictable result of at-least-once delivery, and every serious automated trader will meet them. The fix isn’t to wish the retries away. It’s to make a retry harmless: attach a stable idempotency key to each alert, store it, and skip anything you’ve already acted on. Layer in once_per_bar_close, a short cooldown, and same-direction blocking, and a double fill becomes nearly impossible.
Do it right and one signal always equals one position. No surprise contracts, no blown evaluations from a network hiccup you never saw.
Want the whole path handled for you? Automate TradingView to your broker without code with deduplicated, sub-200ms order routing to Tradovate, Rithmic, and your prop firm in minutes.
Disclaimer:
This content is for informational purposes only and does not constitute financial, investment, or trading advice. Trading and investing in financial markets involve risk, and it is possible to lose some or all of your capital. Always perform your own research and consult with a licensed financial advisor before making any trading decisions. The mention of any proprietary trading firms, brokers, does not constitute an endorsement or partnership. Ensure you understand all terms, conditions, and compliance requirements of the firms and platforms you use.
Also Checkout: Automate TradingView Indicators with Tradovate Using PickMyTrade
