Most tracking setups treat bot traffic as a reporting problem. You notice sessions climbing without revenue following, you switch on a filter in GA4, the graph looks calmer, and you move on.
That fixes the report. It does nothing about the part that actually costs money.
Every bot that reaches a conversion tag gets forwarded to Google Ads and Meta exactly like a real customer. Those platforms then use it as training data. And unlike a missing conversion, which is simply less signal, a bot conversion is wrong signal: it teaches the algorithm that a traffic pattern which will never buy is a pattern worth finding more of.
This guide covers why that distinction matters, and the exact setup for filtering bots at every layer, from the edge down to your offline conversion uploads, using server-side Google Tag Manager (sGTM) and Stape's Bot Detection power-up.
There is a long-running argument about how much conversion data really changes an ad platform's behaviour. The honest answer for missing conversions is: less than most people claim. An algorithm learning from thousands of events does not fundamentally change because you recovered another 20% of the same signal. That is a volume problem, and volume problems get absorbed.
Bot conversions are a different category. They are a labelling problem.
Your reported CPA drops while your real CPA rises, which is the worst possible combination for decision-making.
Severity depends heavily on your model:
view_item and add_to_cart events, your audience lists, and any campaign optimizing toward upper-funnel actions still get polluted.The practical rule: the further up the funnel your conversion action sits, and the cheaper it is to complete, the more bot filtering is worth to you.
Bot filtering works best as layers, not as a single switch. Each layer catches what the one before it missed, and each sits at a different cost point:
Enable the Bot Detection power-up in your Stape tagging server. It is available on the Pro plan and higher.
The power-up adds two headers to every incoming request.
The score bands matter, because they are not all equally actionable:
Leave blocking switched off for now. In this mode Stape only labels traffic, it does not act on it.

Next, surface that label in your reporting. In sGTM, create a User-Defined Variable of type Request Header that reads X-Device-Bot-Score, and add it to your GA4 tag as an event parameter:

Then register bot_score as an event-scoped custom dimension in GA4, so the parameter becomes reportable.

Now let it run for one to two weeks.
You are looking for three things: what share of sessions score above 75, which channels and landing pages they concentrate in, and whether any of them are reaching your conversion events.
That last one tells you whether this is a reporting annoyance or a bidding problem.
The most controlled way to enforce is inside the container, tag by tag.
You already built the score variable in step 1. Create a trigger that fires on your conversion event and a high bot score, then attach it as a trigger exception on the tags you want protected:
Trigger name: Event - Purchase - X-Device-Bot-Score >75
Trigger type: Custom Event
Event name: purchase
Condition: Header - X-Device-Bot-Score greater than 75
Use the score rather than the true/false X-Device-Bot header. It costs you nothing extra and it leaves the threshold in your hands.
Which settles what to do with the 50 to 75 band, the one genuinely awkward decision in this setup. Do not block it. Let it reach GA4 so it stays visible, keep it out of nothing else, and treat it as a watchlist rather than a verdict. If a large share of that band also reports a 0x0 screen resolution, you have two independent signals agreeing and can safely drop your threshold to 50. If it does not, leave it at 75 and accept that some bots get counted. That is the cheaper mistake.
Apply the exception to every tag except GA4: Google Ads conversions, Meta CAPI purchase and lead events, and any other ad platform tag. Leaving GA4 out is deliberate. Bots keep landing in your reporting, so you can carry on measuring, while nothing reaches a bidding algorithm.
The advantage of this approach is granularity. You can let a labelled bot through to GA4 for continued measurement while blocking it from ever reaching Google Ads. That is often the right compromise during the first month.

Once you trust the data, you can let Stape enforce it upstream. Switch blocking on, and requests scoring above 75 are rejected before they reach sGTM, on the /collect (GA4) and /data (Data Tag) paths.
Two benefits over per-tag filtering:
The tradeoff is that it is all or nothing at the request level. You lose the ability to keep measuring bot traffic in GA4, because it never arrives. This is why the measurement phase comes first.

Important:
Bot Detection will not catch everything, and one of the strongest remaining signals costs you nothing.
A screen resolution of 0x0 is close to a guarantee that no human is present. Headless browsers and scripted clients frequently report no viewport at all, and older headless Chrome defaults to a small fixed size such as 800x600.
You can act on this in both places:
screen_resolution in event data. Add a trigger exception on your conversion tags where it equals 0x0.bot_score dimension.Check the overlap between the two before you rely on either. If everything scoring above 75 also reports 0x0, you have a simple confirmation. If they catch different traffic, you now have two complementary filters instead of one.
Be careful with the fixed resolutions. Real devices do use 800x600 occasionally, so treat it as a signal to combine with others, not a standalone block rule.
Expect the volume to be small. In the report below, 0x0 sits well under a percent of users. That is not a reason to ignore it: this filter is about precision rather than reach, and those sessions were still credited with key events. Sort the same report by average engagement time and you will usually surface two or three other resolutions showing the same two-second pattern, worth investigating before you block anything.

The cheapest bot to handle is the one that never reaches your site. If you run Cloudflare, a WAF custom rule removes a large share of automated traffic before it costs you a page load, a tag, or a server request.
Most low-effort bot traffic originates from a handful of hosting providers. Blocking by ASN is more durable than blocking individual IPs, which rotate constantly. The full rule:
Rule name: Challenge datacenter traffic
Action: Managed Challenge
Expression:
(ip.geoip.asnum in {16509 14618 8075 14061 24940 16276 20473 63949 45102 132203})
and not cf.client.bot
Those ASNs cover the providers most automated traffic runs on:
The not cf.client.bot clause is the part people forget, and it is the one that matters most. Cloudflare uses cf.client.bot to identify verified bots: Googlebot, Bingbot, and other crawlers you actively want on your site. Without that exclusion you will block search engine crawlers from a cloud IP range and quietly damage your organic visibility.
Start the rule on Managed Challenge rather than Block. Watch it for a few days in your firewall events, confirm you are not challenging real users, and only then escalate to Block.
One interaction to think about before you enable this, and it catches people out precisely because it is the setup this guide otherwise recommends. If you run a same-origin tagging server, your sGTM endpoint sits behind the same Cloudflare zone as your website. Server-to-server traffic hitting that endpoint, Shopify Flow webhooks, payment provider callbacks, CRM postbacks, originates from exactly the datacenter ASNs this rule challenges. A rule written for your website will happily block your own webhooks.
Two ways to avoid it, and you want one of them in place before the rule goes live:
http.request.uri.path contains "/webhooks"), so server-to-server calls bypass the challenge entirely.and not http.request.uri.path contains "/your-tagging-path" to the expression above.The same applies to uptime monitors and any scheduled job that calls your site from a cloud IP.
If you need to stop a small number of specific offenders instead, Stape's Block Request by IP power-up handles up to 30 addresses without touching your CDN configuration.
For lead generation this is the highest-value layer, because it stops a bot from ever becoming a conversion.
Honeypot field. Add an input that is hidden from humans via CSS, never via type="hidden", which bots detect easily. Real users cannot see or fill it. Any submission where it contains a value is automated.
Minimum time to submit. Stamp the form when it renders and compare on submit. A genuine enquiry takes longer than a few seconds.
Both together, in the markup:
<div>
<label>Company website
<input type="text" name="website" tabindex="-1" autocomplete="off">
</label>
</div>
<input type="hidden" name="rendered_at" value="1721556000">
And the check on your server, before anything is recorded as a lead:
const elapsed = Date.now() / 1000 - Number(body.rendered_at);
if (body.website) return reject('honeypot filled');
if (elapsed < 3) return reject('submitted too fast');
Validate server-side, then fire the event. This is the change that matters most, and the one most setups get wrong. Do not push generate_lead to the dataLayer on form submit. Push it after your backend has accepted the submission and passed its checks. The conversion event should represent a validated lead, not a button click.
If you want a third signal, Cloudflare Turnstile or reCAPTCHA v3 return a score you can evaluate server-side in the same validation step.
Filtering the browser path is wasted effort if you upload the same junk two weeks later.
If you run offline conversion tracking, your CRM is the last place bot leads hide. A bot-submitted lead gets a status of spam, junk or disqualified in your CRM, and then gets included in the upload anyway because the export query only filters on "has a GCLID".
Fix it in the export query, not after the fact:
WHERE gclid IS NOT NULL
AND lead_status NOT IN ('spam', 'disqualified', 'unqualified')
AND stage_reached >= 'contacted'
That second condition is the bot filter. The third is a free upgrade: uploading only leads that reached a real stage improves your signal well beyond bot removal. Apply the same rule to Enhanced Conversions for Leads, because uploading hashed user data for a bot lead is worse than uploading nothing.
GA4 belongs at the end, because it is reporting hygiene rather than signal quality. It is also the layer most people mistakenly start with.
It helps to be precise about what GA4's built-in filtering actually does:
In other words, it handles the polite bots. Everything this guide addresses is the traffic that list will never contain.
What is worth configuring in GA4:
bot_score custom dimension in place after you start blocking, so you retain visibility into what is still getting through./webhooks. Custom paths bypass automatic bot blocking, which applies to /collect and /data. Verify your webhook traffic still arrives immediately after enabling blocking, not a week later when you notice conversions missing.Before you consider this finished:
X-Device-Bot and X-Device-Bot-Score are visible in sGTM preview mode on a live request.bot_score is arriving in GA4 and populating the custom dimension.generate_lead fires.bot_score report you built in step 1 and compare it against your baseline. You are looking for the share of high-scoring traffic reaching key events to be near zero. If it is not, something is still routing around your exceptions, and the report will tell you which events.Bot filtering is usually framed as a data cleanliness exercise. It is really a bidding exercise.
Your reports being wrong is annoying. Your bidding algorithm being trained on traffic that will never buy is expensive, and it compounds, because the platform keeps spending to find more of what you told it was good.
The setup that works is layered and, importantly, measured first:
Start with measurement, enforce one layer at a time, and keep the bot score visible after you start blocking. The goal is not zero bots. The goal is that nothing which will never buy from you ends up teaching your bidding algorithm what a customer looks like.
Comments