How to find bot traffic in your Segment data
Bots that run your analytics.js snippet become Segment MTUs, and you pay for them. This guide gives seven signals that find them in your warehouse, with SQL for each one. All the signals use only the data that analytics.js collects automatically.
Do you want the result without the SQL? The free segment_bot_audit dbt package does all the steps in this guide and prints the bot share of your MTUs and its cost.
Where to look
Segment writes each source to its own schema in your warehouse. The pages table is the most
useful, because the default analytics.js snippet makes a page call on each page load. These columns are
in each row:
| Column | What it contains |
|---|---|
anonymous_id | The ID that analytics.js gives to each browser. Each new ID that has no user_id is an MTU. |
context_user_agent | The user agent of the browser |
context_ip | The IP address, which Segment adds on its servers |
path | The page path |
timestamp | The time of the event |
You do not need identify or track calls. If your team uses them, a
user_id is a good sign of a real person. The audit never counts a visitor with a
user_id as a bot.
The seven signals
| Signal | Strength | What it finds |
|---|---|---|
| Bot user agent | Strong | A known crawler, monitor, AI agent or headless browser in the user agent |
| Malformed user agent | Strong | A page call with a user agent that is too short or does not start with Mozilla/ |
| Scanner path | Strong | Page calls on paths such as /wp-login.php or /.env |
| Fast pages | Strong | Many page calls less than one second apart |
| Regular interval | Strong | Page calls at an almost fixed interval |
| Datacenter IP | Weak | An IP address in a cloud provider range |
| IP and user-agent burst | Weak | Many new IDs from one IP address and user agent in one hour |
One strong signal is enough. Weak signals also match real people, such as an office behind one IP address or a person on a cloud VPN. For this reason, count a weak signal only when the other weak signal is also present.
The SQL below uses like where possible, so it works on most warehouses. Replace
segment.pages with your schema and table.
1. Bot user agent
Many bots tell you what they are. Look for crawler, monitor and automation names in the user agent. Look out for real phones with "bot" in the model name, such as the Cubot brand.
select anonymous_id, context_user_agent
from segment.pages
where (
lower(context_user_agent) like '%bot%'
or lower(context_user_agent) like '%crawl%'
or lower(context_user_agent) like '%spider%'
or lower(context_user_agent) like '%headless%'
or lower(context_user_agent) like '%puppeteer%'
or lower(context_user_agent) like '%playwright%'
or lower(context_user_agent) like '%lighthouse%'
or lower(context_user_agent) like '%pingdom%'
)
and lower(context_user_agent) not like '%cubot%'
2. Malformed user agent
Each modern browser sends a long user agent that starts with Mozilla/5.0. A page call with a
short user agent, or a user agent without Mozilla/, comes from a script. Use this signal only
on the pages table. Mobile and server-side libraries send other user agents, or none.
select anonymous_id, context_user_agent
from segment.pages
where context_user_agent is not null
and (length(context_user_agent) < 30
or lower(context_user_agent) not like 'mozilla/%')
3. Scanner path
Vulnerability scanners request paths that do not exist on your site. If your 404 page has the Segment snippet, each request is a page call. Change the list if your site really uses one of these paths.
select anonymous_id, path
from segment.pages
where lower(path) like '%wp-login%'
or lower(path) like '%wp-admin%'
or lower(path) like '%xmlrpc%'
or lower(path) like '%phpmyadmin%'
or lower(path) like '%/.env%'
or lower(path) like '%/.git%'
4. Fast pages
A person needs some seconds to read a page. A script opens many pages in less than one second. Count the gaps between page calls that are shorter than one second. The date functions are different on each warehouse. This example is for Snowflake.
with gaps as (
select
anonymous_id,
datediff(
'millisecond',
lag(timestamp) over (partition by anonymous_id order by timestamp),
timestamp
) as gap_ms
from segment.pages
)
select anonymous_id, count(*) as fast_gaps
from gaps
where gap_ms < 1000
group by anonymous_id
having count(*) >= 5
5. Regular interval
A person reads pages at an irregular speed. A scheduled script opens pages at an almost fixed interval. Compare the standard deviation of the gaps with the average gap. A value below 0.05 is a strong sign of a script.
-- Use the gaps CTE from signal 4.
select anonymous_id, avg(gap_ms) as avg_gap_ms
from gaps
where gap_ms is not null
group by anonymous_id
having count(*) >= 10
and avg(gap_ms) between 1000 and 600000
and stddev_samp(gap_ms) / avg(gap_ms) <= 0.05
6. Datacenter IP (weak)
Most bots run in the cloud. AWS, Google Cloud, DigitalOcean, Oracle and Linode publish their IP ranges. Load the ranges into a table, and join each IPv4 address to them. People on a VPN or a corporate proxy also use cloud IP addresses, so this signal is weak alone.
The audit package includes the ranges as a seed file, with a script to update them. The IP functions are different on each warehouse, so the package converts each address to an integer for the comparison.
7. IP and user-agent burst (weak)
A bot with no saved data gets a new anonymous_id on each visit. Thus, one machine makes many
IDs with the same IP address and user agent. An office with one IP address can do this too, so this signal
is also weak alone.
select context_ip, context_user_agent,
date_trunc('hour', timestamp) as event_hour,
count(distinct anonymous_id) as ids
from segment.pages
where context_ip is not null
group by 1, 2, 3
having count(distinct anonymous_id) >= 10
Combine the signals and count the cost
- For each
anonymous_idin each month, calculate each signal. - Mark the ID as non-human if it has one strong signal, or both weak signals.
- Do not mark an ID that has a
user_idin the month. - Count the non-human IDs for each month, and multiply by your price for each MTU.
The segment_bot_audit package does these steps for you. It also handles the tables and
columns that some setups do not have, such as a missing identifies table.
dbt deps
dbt build --select package:segment_bot_audit
dbt run-operation segment_bot_audit.print_report
How to stop bots from counting as MTUs
Segment counts an MTU when an event reaches Segment. Destination filters act after that point, so they do not reduce MTUs. To stop the MTU, do not load analytics.js for bots.
In the standard snippet, remove the analytics.load and analytics.page calls
from the end of the snippet. Then add this code after the snippet:
<script>
var botPattern = /bot|crawl|spider|slurp|headless|phantomjs|puppeteer|playwright|selenium|lighthouse|pingdom|uptime/i;
var isBot = navigator.webdriver === true || botPattern.test(navigator.userAgent);
if (!isBot) {
analytics.load("YOUR_WRITE_KEY");
analytics.page();
}
</script>
If you use the npm package, make the same check before you load it:
import { AnalyticsBrowser } from '@segment/analytics-next';
const botPattern = /bot|crawl|spider|slurp|headless|phantomjs|puppeteer|playwright|selenium|lighthouse|pingdom|uptime/i;
const isBot = navigator.webdriver === true || botPattern.test(navigator.userAgent);
export const analytics = isBot ? null : AnalyticsBrowser.load({ writeKey: 'YOUR_WRITE_KEY' });
Do a test before you deploy. A wrong pattern can block real visitors. Measure first with the audit, and then block only the patterns that you see in your own data.
This check stops the bots that identify themselves. Bots that copy a real browser need the server-side signals in this guide, such as the IP address and the timing.
Get a free review of your audit results
Paste the output of dbt run-operation segment_bot_audit.print_report. We read it and reply to you
by email. We tell you which bots cost you the most, and how to block them.
You do not have results yet? Run the audit first. It takes a few minutes in your dbt project.
We use your email and your results only to reply to you. We do not share them.
Questions
Can I see bot traffic in the Segment warehouse tables?
Yes, for most bots. Each page call from analytics.js has the user agent, the IP address, the page path and a timestamp. These columns are enough to find crawlers, headless browsers, scanners and scripts.
Do I need identify calls to find bots?
No. The signals in this guide use only the data that analytics.js collects automatically on each page call. A visitor with a userId is usually a real person, but the signals do not need one.
Does Segment filter bots for me?
No. Segment has no built-in bot filter. Destination filters remove bots from some tools, but the bots still count as MTUs in Segment.
How do I exclude bots from Segment?
Do not load analytics.js for bots. Do a check of the user agent and navigator.webdriver before you call analytics.load. This stops the event before Segment counts an MTU.
Read next: Segment MTU pricing: how much of your bill is bots?