Welcome to the Crawler & Politeness Engine Guide!
This document provides a comprehensive tour of how SEO Lens navigates the web: how it discovers and queues URLs, respects web servers with adaptive congestion control, normalizes link targets to prevent duplicate visits, and parses robots and sitemaps with zero-copy efficiency.
SEO Lens uses an asynchronous producer-consumer pipeline built on the Tokio runtime:
flowchart TD
subgraph Discovery ["1. URL Discovery & Frontier"]
Queue["URL Frontier (FIFO VecDeque)"]
VisitedSet["Visited Set (SwissTable 64-bit Hashes)"]
SitemapEngine["Sitemap Ingestion (quick-xml)"]
RobotsEngine["Robots.txt Parser (RFC 9309)"]
end
SitemapEngine -->|Seed URLs| Queue
subgraph ConcurrencyPipeline ["2. Concurrency & Politeness"]
Queue -->|"Pop URL & Depth"| Worker["Worker Task (tokio::spawn)"]
Worker -->|Check Permission| RobotsEngine
Worker --> RateGate["AIMD Rate Controller (Dynamic Delay)"]
RateGate --> HTTPClient["reqwest HTTP/2 Client Pool"]
end
subgraph StreamProcessing ["3. Stream Ingestion & Feedback"]
HTTPClient --> Telemetry["Latency & Error Feedback Loop"]
Telemetry -->|Tune Delay & Concurrency| RateGate
HTTPClient --> Parser["Streaming lol_html Tokenizer"]
Parser --> LinkFilter["Domain Boundary & Link Filter"]
LinkFilter -->|Unseen URLs| VisitedSet
VisitedSet -->|New URLs| Queue
end
- Discovery & Frontier: Seed URLs (or URLs discovered in sitemaps) are pushed into the Frontier Queue. Before any URL is enqueued, it is normalized and checked against the visited set using 64-bit fast hashing.
- Concurrency & Politeness Gate: Worker tasks request permission from the RFC 9309
robots.txtengine and pass through the AIMD rate controller, which dynamically modulates delays based on server health. - Stream Processing & Feedback: As responses stream in, the client measures response latency (TTFB) and HTTP status codes, feeding this telemetry back to the AIMD controller to dynamically speed up or slow down the crawl rate.
Crawling websites with aggressive concurrency without rate limiting can easily crash origin databases, trigger 429 Too Many Requests errors, or prompt Cloudflare/WAF IP bans.
To solve this, SEO Lens implements Additive-Increase / Multiplicative-Decrease (AIMD) congestion control—the same foundational algorithm that powers TCP Reno on the Internet:
flowchart TD
Window["Sample Window (N = 50 requests)"]
Compute["Compute: Error Rate (E) & p95 TTFB"]
Window --> Compute
Compute -->|"Degradation Trigger<br/>(E > 8% OR p95 > 1.5x baseline)"| Backoff["Multiplicative Backoff (Load Halved)<br/>delay = min(delay * 2.0, 10,000ms)<br/>concurrency = max(floor(c * 0.5), 1)"]
Compute -->|"Healthy Recovery<br/>(E == 0% AND p95 < 500ms)"| StepUp["Additive Recovery (Gentle Ramp)<br/>delay = max(delay - 25ms, delay_floor)<br/>concurrency = min(c + 1, c_max)"]
| Parameter | Symbol | Default Value | Engineering Rationale |
|---|---|---|---|
| Sample Window | 50 requests |
Smooths statistical outliers while reacting to server distress in |
|
| Error Rate Ceiling |
0.08 (8%) |
If |
|
| Latency Multiplier | 1.5 |
If p95 TTFB exceeds |
|
| Multiplicative Backoff | 2.0 |
Instantly halves origin load when strain or rate limiting is detected. | |
| Additive Recovery Step | 25 ms |
Gently reduces delay by 25ms per successful window without shocking the origin. | |
| Max Delay Ceiling | 10,000 ms |
Prevents infinite stalls by capping maximum request delay at 10 seconds. | |
| Delay Floor | Strictly honors Crawl-Delay directives in /robots.txt. |
Tip
When testing local staging environments or high-throughput benchmarks, you can bypass dynamic AIMD rate adjustments using the --no-aimd flag.
Discovered links often point to the same destination under different formats (e.g. https://example.com, http://example.com/, https://example.com/?utm_source=twitter).
To guarantee that each unique page is crawled exactly once, all URLs pass through an 8-stage normalization pipeline in src/core/url.rs:
Raw Href ──> [1. Scheme] ──> [2. Hostname] ──> [3. Port] ──> [4. Path]
──> [5. Trailing Slash] ──> [6. Strip Fragments]
──> [7. Strip Tracking Query] ──> [8. Sort Query] ──> Normalized URL
-
Scheme Lowercasing: Converts scheme to lowercase (
HTTP$\rightarrow$ http). Resolves protocol-relative URLs (//cdn.example.com$\rightarrow$ https://cdn.example.com). -
Hostname Lowercasing & Root Dot Removal: Lowercases domain names (
Example.COM$\rightarrow$ example.com) and removes trailing root dots (example.com.$\rightarrow$ example.com). -
Default Port Stripping: Drops standard ports (
:80for HTTP,:443for HTTPS). -
Path Segment Resolution: Resolves dot segments (
/a/b/../c$\rightarrow$ /a/c) and collapses consecutive slashes (/blog//post$\rightarrow$ /blog/post). -
Root Path Enforcement: If path is empty, ensures root
/is present (https://example.com$\rightarrow$ https://example.com/). -
Fragment Removal: Drops anchor fragments (
/page#reviews$\rightarrow$ /page). -
Tracking Parameter Stripping: Automatically strips marketing and analytics tracking noise:
-
utm_source,utm_medium,utm_campaign,utm_term,utm_content -
fbclid,gclid,msclkid,mc_eid,_ga,_gl,ref
-
-
Deterministic Query Sorting: Lexicographically sorts legitimate query parameters (
?b=2&a=1$\rightarrow$ ?a=1&b=2).
E-commerce and filtered catalog sites often generate infinite calendar loops or faceted navigation spider traps. SEO Lens provides two built-in guardrails:
--max-query-params <N>(default:2): Limits the number of permissible query parameters before flagging or pruning candidate URLs.--ignore-sorting-facets(default:true): Automatically strips sorting and display facets (e.g.sort=price_desc,order=date,view=grid) that produce duplicate content.
After normalization, each URL string is converted into a 64-bit AHash (u64):
- The
VisitedSetinsrc/crawler/frontier.rsstores onlyu64values in a SwissTable (hashbrown::HashSet<u64>). - Storing 50,000 URLs is estimated at ~400–600 KB of RAM based on SwissTable table overhead (compared to
$>10$ MB for raw string vectors).
The crawler frontier schedules pending URLs and enforces crawl boundaries:
pub struct FrontierEntry {
pub url: String,
pub depth: u16,
pub source_url: Option<String>,
}- Breadth-First Search (BFS) (Default):
- Uses
VecDeque<FrontierEntry>(FIFO queue). - Ensures shallow, high-PageRank category pages are audited first before digging into deep pagination or leaf nodes.
- Uses
- Depth Limiting:
- When a link is discovered on a page at
depth = d, candidate entries are assigneddepth = d + 1. - If
d + 1 > max_depth, the URL is added to the site link graph as an edge but is never enqueued for crawling.
- When a link is discovered on a page at
- Domain Boundary Control:
- Internal Domain: Host matches the starting domain (or subdomains if enabled). Crawled recursively.
- External Domain: Outbound link. Validated with a lightweight status check, but outgoing links are not extracted.
SEO Lens adheres strictly to RFC 9309 (Robots Exclusion Protocol):
-
User-Agent Matching:
- Specific user-agent matches take precedence over wildcard
*. - Priority hierarchy:
SEOLens$\rightarrow$ Googlebot$\rightarrow$ *.
- Specific user-agent matches take precedence over wildcard
-
Longest Match Precedence:
- When multiple directives match a path, the rule with the longest character pattern wins:
Allow: /products/ Disallow: /products/archived/ # URL: /products/archived/item-1 -> DISALLOWED (20 chars vs 10 chars) -
Allow Overrides Disallow on Equal Length:
- If
Allow: /blogandDisallow: /bloghave identical pattern lengths,Allowtakes precedence per RFC 9309.
- If
-
Wildcards: Fully supports
*(zero or more characters) and$(end of URL pattern).
Uses quick-xml for zero-allocation streaming XML parsing:
- Auto-Discovery:
- Checks
Sitemap:declarations in/robots.txt. - Probes standard paths:
/sitemap.xml,/sitemap_index.xml,/wp-sitemap.xml.
- Checks
- Sitemap Index Recursion:
- Recursively expands nested
<sitemapindex>entries up to 3 levels deep.
- Recursively expands nested
- Compressed Sitemaps (
.xml.gz):- Automatically detects compressed
.gzsitemaps and streams decompression on the fly usingflate2.
- Automatically detects compressed
- Orphan Page Detection:
- Sourced URLs are tagged with
is_sitemap_url = true. - If post-crawl analysis finds that a sitemap URL has 0 incoming internal links from crawled HTML pages,
ALERT_GRAPH_ORPHAN_PAGEis triggered.
- Sourced URLs are tagged with
When crawling client sites protected by Cloudflare, Akamai, or DataDome, firewalls often return HTTP 200 or 403 with a JavaScript challenge screen. Parsing challenge pages blindly would flood audit reports with false alarms ("missing title", "zero words", "missing H1").
SEO Lens inspects response bodies for known challenge signatures:
pub struct WafProbe {
pub provider: &'static str,
pub signatures: &'static [&'static str],
}
pub const WAF_SIGNATURES: &[WafProbe] = &[
WafProbe {
provider: "Cloudflare",
signatures: &[
"cf-browser-verification",
"checking your browser before accessing",
"cloudflare ray id",
"/cdn-cgi/challenge-platform/",
],
},
WafProbe {
provider: "Akamai",
signatures: &[
"akamai bot manager",
"_abck=",
"reference number:",
"bm-sz=",
],
},
WafProbe {
provider: "DataDome",
signatures: &[
"geo.captcha-delivery.com",
"datadome",
"dd_cookie_test",
],
},
WafProbe {
provider: "Imperva",
signatures: &[
"incapsula incident id",
"_incap_ses",
"visid_incap",
],
},
];When a challenge signature matches:
- Flags
ALERT_WAF_BOT_CHALLENGEon the URL. - Suppresses false-positive warnings for missing titles, H1s, or content.
- Advises the user in the report: "Blocked by Cloudflare/Akamai WAF challenge. Whitelist the crawler IP or supply a session cookie."