This page describes exactly what happens to each request, for administrators tuning the module and developers reading the event log.
Inspection pipeline
Traffic Guard listens to the Laminas route event at priority 999, which fires before routing, before any controller and before any Doctrine query. Command-line requests are ignored. For every HTTP request:
- If Enable Traffic Guard protection is off, stop; the request is allowed.
- Determine the client IP from
REMOTE_ADDR, or from the first entry ofX-Forwarded-Forwhen Trust X-Forwarded-For is on. - Allowlist. If the IP is private or reserved (and the auto-allowlist setting is on), or matches an entry in
tg_ip_allowlist, allow. Nothing is counted or logged. - Active ban. If
tg_banshas an unexpired, unlifted ban for the IP, log an event with categorybanned, score 100, signalactive_ban, and return HTTP 403. This applies to admin paths too. - Classify the path into
admin,api,search,itemorglobal. Admin paths are allowed here with no counting or logging. - Count the request in the fixed 60-second window for its category and read back the count.
- Score the request (see below). A user-agent allow rule returns immediately with allow and no log entry.
- Decide. Score at or above the block threshold: block (HTTP 403). At or above the throttle threshold: throttle (HTTP 429 with
Retry-After: 60). Otherwise allow. - Auto-ban. On a block decision with auto-ban on, insert a ban for the IP unless one is already active.
- Log. Throttle and block decisions are always logged; allow decisions only when Log allowed requests too is on.
Any exception anywhere in this pipeline is written to the PHP error log with a [TrafficGuard] prefix and the request is allowed.
Path categories
| Category | Matches | Rate limited |
|---|---|---|
admin |
Paths starting with /admin |
No |
api |
Paths starting with /api |
Yes |
search |
/s/{site}/search and anything under it |
Yes |
item |
/s/{site}/item, /s/{site}/item-set, /s/{site}/media and anything under them |
Yes |
global |
Everything else (site home pages, custom pages, feeds, the install root) | Yes |
Each category has its own counter, so 20 searches and 20 item views in the same minute are counted separately.
Signals and weights
The score is the sum of the weights of the signals that fired, clamped to 0 to 100. If any single signal has a weight of 80 or more, the score is raised to at least 80 so that a clearly bad indicator cannot be hidden. Weights depend on the protection mode.
| Signal | Fires when | Normal | Elevated | Under Attack |
|---|---|---|---|---|
ua_rule_block |
An enabled user-agent rule with action block matches (rules are checked first, in priority order) | 80 | 80 | 80 |
empty_ua |
The User-Agent header is missing or blank |
25 | 35 | 50 |
bot_ua_pattern |
The user agent contains one of the built-in bot substrings (only checked if the header is not empty) | 30 | 40 | 60 |
rate_exceeded_soft |
This window's count is above 80 percent of the category limit but not above the limit | 20 | 30 | 40 |
rate_exceeded_hard |
This window's count is above the category limit | 30 | 45 | 60 |
deep_pagination |
The query string has page greater than 50 |
15 | 20 | 30 |
excessive_search |
The path contains /search and the query string is longer than 200 characters |
10 | 15 | 25 |
A matching user-agent allow rule is recorded as ua_rule_allow and ends scoring; no other signal is evaluated.
Worked examples in Normal mode (throttle 40, block 70):
- A scraper with
python-requeststhat has exceeded the search limit: 30 + 30 = 60, throttled. - The same scraper also requesting
?page=75: 60 + 15 = 75, blocked and auto-banned. - A browser that exceeds the item limit with a normal user agent: 30, allowed. In Elevated mode it scores 45 and is throttled.
Built-in bot user-agent substrings
Matched case-insensitively anywhere in the header:
masscan, zgrab, nikto, sqlmap, nmap,
python-requests, go-http-client, okhttp,
headless, phantomjs, selenium, puppeteer, playwright,
scrapy, wget/, curl/,
ahrefsbot, semrushbot, dotbot, mj12bot, blexbot, petalbot, bytespider
Well-known search engine crawlers such as Googlebot and Bingbot are not on the list. Add allow rules for them if you want them exempt from rate limiting; see User-Agent Rules.
Rate-limit windows
Counters are fixed windows aligned to the clock: a 60-second window runs from hh:mm:00 to hh:mm:59 UTC and the count resets at the next minute. Counts are incremented with a single atomic INSERT ... ON DUPLICATE KEY UPDATE, so concurrent requests are counted correctly. Stale counter rows older than three windows are pruned on roughly one request in a hundred, 500 rows at a time.
Responses sent to blocked clients
| Decision | Status | Body | Headers |
|---|---|---|---|
| throttle | 429 | Too Many Requests |
Content-Type: text/plain, Retry-After: 60 |
| block | 403 | Forbidden |
Content-Type: text/plain |
The response is sent directly from the route listener; no theme or error template is rendered.