The word "bot" looks simple until a system starts using it in several different places.
For example, there may be an authenticated automated account performing allowed actions through normal authentication.
Separately, there may be a guest HTTP request coming from a search crawler or another automated client.
Both may casually be called bots, but technically and semantically they are different things.
One describes an account. The other describes a particular request.
I Start by Separating Two Different Questions
Question 1 What kind of account is this? - regular user- automated account Question 2 What kind of HTTP request is this? - regular browser- crawler- another automated clientAccount classification lives longer than one request.
It describes the user record itself and may be established by an administrator or another trusted source.
Crawler detection belongs to a particular HTTP request. The next request may have different characteristics.
Even the lifetime of these signals is different.
One is_bot Flag Hides Where the Signal Came From
if ($user?->is_bot || $requestIsBot) { $isBot = true;}After this merge, downstream code no longer knows why the value became true.
Was it an automated account?
Was it a guest crawler?
Was it an authenticated user making one request through an unusual automated client?
The distinction may look unimportant for one small if, but it quickly matters for analytics, reporting, and later rules.
Account Classification Describes the Actor
enum AccountKind: string{ case HUMAN = 'human'; case AUTOMATED = 'automated';}This is only one possible representation.
The important part is that the classification belongs to the account and has a known source.
For example, the system may explicitly know that a particular account exists for automated work.
That fact does not need to be guessed from the User-Agent on every request.
Crawler Detection Describes the Request, Not the User
final readonly class RequestClassification{ public function __construct( public bool $isCrawler, public ?string $crawlerFamily, ) {}}This result belongs to the current request.
It may be derived from headers, User-Agent, known crawler families, or other bounded signals.
It is still a property of the request, not a permanent property of an account.
A Guest Crawler May Have No Account at All
HTTP request | +--> authenticated user? | +--> NO | v guest request | +--> crawler detectedThis is another reason to keep the concepts separate.
For a large portion of crawler traffic there is no User object at all.
Request-level detection therefore has to work independently from account classification.
An Automated Account May Use Normal Authentication
Account kind = automatedauthenticated = yes Current request crawler detected = no It is stillan automated account,even if the current requestdoes not look like a crawler.An automated account may use a normal API client without crawler-like request headers.
If the system relies only on request detection, it loses known information about the account itself.
The reverse is also true: a regular user should not become a "bot account" because of one suspicious request.
I Do Not Promote a Request Heuristic into Permanent Account State
if ($crawlerDetector->matches($request)) { $user?->update([ 'is_bot' => true, ]);}User-Agent can be changed. A request signal may be incomplete or wrong.
I therefore do not want one request-level heuristic to permanently change account classification.
Account classification should have its own trusted source and its own update rules.
Analytics Needs the Two Signals Separately as Well
Dimension 1 account_kind- human- automated- guest Dimension 2 request_kind- browser- crawler- other If analytics stores only is_bot=true, it becomes impossible to answer what was actually filtered later.
Was it crawler traffic?
Activity from known automated accounts?
Both together?
Separate signals let reporting rules evolve without losing the original semantics.
Classification and Filtering Are Different Decisions
There is another boundary I keep separate: classification itself and the decision about how that classification affects reporting.
For example, a crawler request may be excluded from human views while still contributing to crawler analytics.
An automated account may be excluded from one audience metric while remaining relevant to an operational report.
"We classified it" should not automatically mean "we delete or ignore all related data".
Reclassification Should Not Rewrite User History
If an account is later classified as automated, I do not want to physically delete its previous reactions, comments, or other history merely because the classification changed.
Classification describes how the system interprets the actor.
Reporting rules decide which data belongs in a particular metric.
This keeps reclassification from becoming a destructive rewrite of history.
Long-Lived Classification Should Have a Known Source
final readonly class AccountClassification{ public function __construct( public AccountKind $kind, public string $source, ) {}}If a value lives for a long time and affects reporting or system behavior, it is useful to know where it came from.
The source may be an explicit administrative decision, a known system account, or another trusted origin.
This is much easier to reason about than an unexplained is_bot=true set by some heuristic in the past.
I Derive Both Signals Independently
Request | +--> authenticated user? | | | +--> account classification | +--> request classifier | +--> crawler? +--> crawler family? Then each consumerdecides which signalsmatter for its purpose.What I Verify in Tests
1. Guest + crawler request -> no account classification -> request classified as crawler 2. Regular authenticated user + regular request -> human account -> not crawler 3. Automated account + regular request -> automated account -> not crawler What I Check Before Adding is_bot
Before adding one is_bot field, I ask:
- Am I classifying an account or one particular request?
- How long does the signal live: one request or the account lifetime?
- Where does the classification come from?
- Is it a trusted fact or only a heuristic?
- Can a guest crawler exist without a User?
- Can an automated account make a request without crawler signals?
- Does analytics need both signals separately?
- Does filtering accidentally turn classification into historical data deletion?
One Word "Bot" Does Not Mean One Model
An automated account and a crawler request may both look like "bots", but they describe different levels of the system.
Account classification answers who is acting.
Request detection answers what is known about a particular HTTP request.
If both are collapsed into one boolean, the system loses the source of the signal and quickly starts building rules on top of an ambiguous value.
I would rather keep two simple signals with clear semantics than one convenient is_bot flag that every module interprets differently.
When one boolean can mean two different things, the problem is usually not the field name but the missing boundary between concepts.



