Skip to content

feat: per-host request gate with adaptive cooldown for concurrent fetches#10

Merged
AnderEnder merged 1 commit into
mainfrom
feat/per-host-rate-limit-gate
Jul 10, 2026
Merged

feat: per-host request gate with adaptive cooldown for concurrent fetches#10
AnderEnder merged 1 commit into
mainfrom
feat/per-host-rate-limit-gate

Conversation

@AnderEnder

Copy link
Copy Markdown
Owner

Problem

Concurrent fetches to the same host trip provider per-domain rate limits (HTTP 429). The worst case is the MCP server: fetch_feed takes a single URL and built a fresh HttpClient per call, sharing only the cache — so several concurrent tool calls burst one host with no coordination. A single in-flight retry (ADR-0015) can't help across concurrent requests. An empirical probe also showed some providers send no Retry-After and throttle even serial requests, so serialization alone isn't enough.

Change

A process-wide per-host request gate (src/ratelimit.rs) owned by a reused HttpClient that RssServer builds once and shares across tool calls (also restoring keep-alive). Keyed on authority (host:port):

  • Concurrency cap (default 1) — serializes same-host requests; distinct hosts stay fully parallel.
  • Adaptive cooldown — honors Retry-After (delta-seconds and HTTP-date) when sent; otherwise a bounded escalating backoff (2s·2^(n-1), capped 60s) that resets on success.
  • Sticky spacing — ~1s between requests once a host is warm; zero cost on hosts that never throttle.
  • Server absorbs waits up to a bound, then fails fast with a structured RATE_LIMITED error carrying retry_after so an agent can pace.

Paired: descriptive User-Agent; HTTP-date Retry-After parsing. All tunables are env-overridable (RSS_HOST_CONCURRENCY, RSS_MAX_COOLDOWN_SECS, RSS_MAX_GATE_WAIT_SECS).

Contract / invariants

  • SCHEMA_VERSION stays "1"RATE_LIMITED is an additive error code; details are free-form.
  • CLI + MCP share core (wiring is additive: fetch_feeds_with(&HttpClient)); no pacing logic in mcp.rs.
  • The ADR-0015 in-flight retry is unchanged and its cap (RETRY_MAX_DELAY 5s) is kept distinct from the sibling-facing cooldown cap.

See ADR-0016 for rationale and alternatives.

Testing

cargo fmt --check, cargo clippy --all-targets -D warnings, and cargo test all pass (87 lib + integration). New tests cover gate serialization, per-host isolation, cooldown escalation/cap, sticky spacing, fail-fast ceiling, cancellation, Retry-After parsing (both forms), and the RATE_LIMITED contract. Reviewed via a multi-dimension adversarial pass; a live fetch confirms the happy path adds no latency.

Follow-ups (deliberately out of scope)

  • Multi-URL fetch_feed (urls: [..]) so the server paces a batch internally — larger contract surface; possible future ADR.
  • Cross-host redirect attribution: the gate keys on the request host; a cross-host redirect's throttle is attributed to the origin (documented, low impact).

…ent fetches

Concurrent same-host fetches — most visibly the MCP server firing several
`fetch_feed` tool calls at once, each building a fresh HttpClient that shared no
state — tripped provider per-domain rate limits (HTTP 429). Nothing local to one
call could coordinate the burst.

Add a process-wide per-host gate (src/ratelimit.rs) owned by a reused HttpClient
that the MCP server builds once and shares across tool calls (also restoring
connection reuse). Per authority (host:port): a concurrency cap (default 1) plus
an adaptive cooldown — honoring `Retry-After` when sent, otherwise a bounded
escalating backoff — and sticky spacing once a host is warm. The server absorbs
waits up to a bound, then fails fast with a structured RATE_LIMITED error the
caller can pace on. Paired changes: descriptive User-Agent, HTTP-date
`Retry-After` parsing.

Wiring is additive (fetch_feeds_with(&HttpClient)); SCHEMA_VERSION stays "1"
(RATE_LIMITED is an additive error code, details are free-form). See ADR-0016.
@AnderEnder AnderEnder changed the title feat(fetch): per-host request gate with adaptive cooldown for concurrent fetches feat: per-host request gate with adaptive cooldown for concurrent fetches Jul 10, 2026
@AnderEnder AnderEnder merged commit 240509b into main Jul 10, 2026
6 checks passed
AnderEnder added a commit that referenced this pull request Jul 10, 2026
First release since v0.1.0. Bundles the accumulated unreleased work:
per-host request gate (#10), all crate deps updated to latest, rmcp
1.x -> 2.2.0, and 3 security advisories resolved (#11).
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.

1 participant