Skip to content

crawler: enforce Crawl-delay, back off on 429/503, surface politeness counters#30

Open
andreimarinescu wants to merge 3 commits into
mainfrom
fix/crawler-politeness
Open

crawler: enforce Crawl-delay, back off on 429/503, surface politeness counters#30
andreimarinescu wants to merge 3 commits into
mainfrom
fix/crawler-politeness

Conversation

@andreimarinescu

Copy link
Copy Markdown
Collaborator

Two field incidents motivated this: a temporary IP-level ban from news.ycombinator.com (robots.txt asks Crawl-delay: 30; the crawler fetched at ~1/s regardless) and a 429 storm from huggingface.co (an 823-item feed crawled with no backoff — 424 throttle responses in one pass, roughly half the feed lost to terminal errored entries).

Crawl-delay was parsed but effectively unenforced. The delay was slept per-worker after the host-gate slot was reserved, so concurrent workers still received slots at per_host_delay_ms spacing and the effective per-host rate ignored robots.txt entirely. robots.txt is now consulted before the gate and the delay widens the slot reservation itself (hostGate.WaitFor). Fractional values (Crawl-delay: 0.5) parse, and a new crawler.max_crawl_delay_ms (default 2 min) clamps hostile robots.txt values.

429 (and 503 with Retry-After) no longer error the frontier entry. Errored entries are terminal, so every throttled URL was lost until a manual recrawl. Now the host's next slot is deferred by Retry-After (or an exponential fallback, capped at 5 min) and the URL is requeued up to 3 times before failing. Rate-limited responses also stay out of hostStats so the success-ratio blacklist can't be poisoned by a healthy-but-busy server.

Politeness observability. Claim-time allowlist drops were silent — imports report success while every item is discarded. They're now counted, logged once per host, and exposed together with deferral counts via /stats (crawl_dropped_disallowed, crawl_rate_limited_deferrals) and /metrics.

/admin/crawl-enqueue accepts an optional lane (empty keeps the historical discovered default) so bulk feeds don't have to compete with link discovery.

Replaying the huggingface feed on a fresh index: 429s received 424 → 85 (each one deferring the host), URLs lost ~half → 1 of 823, no blacklisting. Tests cover the gate timing under concurrency (the HN failure mode), robots parsing, the previously untested fetchRSS paths, both 429 flows (recover and give-up), and lane routing; the suite is green with -race.

… counters

Two field incidents drove this: a temporary IP ban from news.ycombinator.com
(robots asks Crawl-delay: 30; we fetched at 1/s regardless) and a 424-response
429 storm from huggingface.co (an 823-item feed crawled with no backoff).

- Crawl-delay was parsed but effectively unenforced: the delay was slept
  per-worker AFTER the host-gate slot was reserved, so concurrent workers
  still received slots at per_host_delay_ms spacing and the per-host request
  rate ignored robots entirely. robots.txt is now consulted before the gate
  and the delay widens the slot reservation itself (hostGate.WaitFor).
  Fractional Crawl-delay values parse, and a new crawler.max_crawl_delay_ms
  (default 2 min) clamps hostile/misconfigured values.
- 429 (and 503 with Retry-After) no longer error the frontier entry: the
  host's next slot is deferred by Retry-After (or an exponential fallback,
  capped at 5 min) and the URL is requeued up to 3 times before failing.
  Rate-limited responses stay out of hostStats so the success-ratio
  blacklist can't be poisoned by a healthy-but-busy server.
- Claim-time allowlist drops were invisible (imports report success while
  every item is discarded): now counted, logged once per host, and exposed
  together with deferral counts via /stats (crawl_dropped_disallowed,
  crawl_rate_limited_deferrals) and /metrics.
- /admin/crawl-enqueue accepts an optional "lane" field (empty keeps the
  historical discovered default) so bulk feeds don't compete with discovery.
- Tests: gate WaitFor/Defer timing under concurrency, fractional/negative
  Crawl-delay parsing, first fetchRSS coverage (RSS2/Atom/failure paths),
  429 requeue-recover and give-up flows, enqueue lane routing.
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.72727% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/crawler/crawler.go 92.10% 3 Missing and 3 partials ⚠️
cmd/cosift/serve_setup.go 0.00% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

- robots Crawl-delay pacing through the gate under concurrent workers,
  fractional value in anger, and the max_crawl_delay_ms clamp
- exponential fallback when a 429 carries no Retry-After; 503 semantics
  (throttle only with the header)
- claim-time drop counter via frontier entries that predate an allowlist
  change
- /stats and /metrics politeness counters (present with the hook, absent
  without); gate.Defer no-op guard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants