Duplicate Webhook Alerts: Why Your Bot Double-Orders (2026)

You set one alert. Your broker log shows two orders. Same symbol, same second, two contracts where you wanted one.

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:

  1. 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.
  2. Pine Script frequency settings. Using alert.freq_every_bar instead of once_per_bar_close is one of the most frequent reasons a single condition fires repeatedly.
  3. Duplicate alerts on the chart. Two alerts set on the same condition, easy to do across multiple charts, each send their own webhook.
  4. 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.

Close-up of stock market numbers and data streaming on a monitor, representing repeated webhook alerts hitting a trading bot

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 person studying a rising performance graph on a laptop screen, representing the drawdown a duplicate futures order eats into

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.

What a double-fire does to your risk Account risk per position (%), 0.75% intended size 2.0% 1.0% 0% 1% max-risk guideline 0.75% One order 1.50% Duplicate fire
Illustrative sizing based on a 0.5% to 1% per-trade guideline.

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.

The idempotency gate Alert arrives Extract key ticker+side+barTime Seen in last 96h? yes Skip, return 200 OK no Place order Store key + time
The gate every inbound alert passes through before it becomes an order.

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.

Make your dedup memory outlast retries Retry / dedup window (hours) Shopify retries 48h Payment webhooks ~72h Your dedup TTL 96h
Set your dedup TTL above the longest retry window you face.

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_close frequency 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

A laptop displaying financial charts next to a planning tablet, representing PickMyTrade's cloud order routing and built-in deduplication

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_ignore stops 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:

FeatureSelf-hosted receiverManaged router (PickMyTrade)
Dedup logicYou build and maintain itBuilt in via same_direction_ignore
Setup timeHours to daysMinutes, no code
Order latencyDepends on your VPSSub-200ms
Prop firm connectivityWire each broker yourselfApex, Topstep, Tradeify ready
Ongoing costServer plus upkeepFlat 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

Why does my TradingView alert fire twice?

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.

What is an idempotency key in a trading webhook?

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.

Can duplicate orders fail my prop firm challenge?

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.

How long should my deduplication window be?

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.

Do I need to code this myself?

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

Leave a Comment

Your email address will not be published. Required fields are marked *

error

Follow us for more insights and updates

Scroll to Top
Verified by MonsterInsights