Skip to content

KV quota fixes: edge caching, compare scraper gate, kv-cache hardening#18

Merged
gavdevs merged 1 commit into
mainfrom
fix/kv-quota-edge-cache
Jul 13, 2026
Merged

KV quota fixes: edge caching, compare scraper gate, kv-cache hardening#18
gavdevs merged 1 commit into
mainfrom
fix/kv-quota-edge-cache

Conversation

@gavdevs

@gavdevs gavdevs commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

A residential-proxy scraper botnet crawling the ~25k noindexed /compare pages tripled traffic on Jul 11–12 (~25–30k → 84–94k req/day) and exhausted the 100k/day free-plan KV read quota (~108–112k reads/day). Every kvCached call costs a read op hit or miss, and Worker-generated responses never entered Cloudflare's CDN cache — the s-maxage headers were decorative without a Cache API put.

Changes

  • Edge caching (src/routes/-worker-entry.ts): cacheable GET responses (pages, sitemaps, robots, /_serverFn GETs) are stored in the CDN cache via caches.default, keyed on the full URL (pagination query params survive). Repeat hits are served with X-Edge-Cache: HIT and skip SSR, KV, and Turso entirely. /search stays no-store; responses with Set-Cookie are never cached. The reseed flow's purge_everything already invalidates this cache.
  • Scraper gate: /compare/* HTML is only served to genuine browser navigations (Accept: text/html + Sec-Fetch-Mode: navigate); everything else gets a cheap 403 before SSR runs. Real users are unaffected — in-app nav goes through /_serverFn. Also adds Disallow: /compare to robots.txt (pages are already noindexed).
  • KV cache hardening (db/kv-cache.ts): per-isolate in-memory hot cache (5 min TTL, 256 entries) so repeat lookups of hot keys don't spend KV reads; cache writes kept alive past the response with waitUntil and failures logged instead of swallowed; kv.get failures (e.g. quota exhaustion) fall through to the database instead of 500ing the page.

Verification (against production)

  • Second GET of a page/sitemap returns X-Edge-Cache: HIT
  • Scraper-style request to /compare/... → 403; browser-navigation headers → 200
  • Fresh page render → getAppBySlug:* key readable via wrangler kv key get --remote seconds later
  • First post-deploy 15-min analytics bucket shows KV reads at ~half the preceding rate, before the edge cache has warmed

A scraper botnet crawling the 25k noindexed /compare pages tripled
traffic and blew through the 100k/day free-plan KV read quota — every
kvCached call burns a read op whether it hits or misses, and Worker
responses never entered the CDN cache because Cache-Control headers
alone don't do that.

- Store cacheable GET responses (pages, sitemaps, server fns) in the
  Cloudflare edge cache via the Cache API; repeat hits skip SSR, KV,
  and Turso entirely. Reseed flow already runs purge_everything.
- Refuse /compare HTML to anything that isn't a real browser
  navigation (Accept: text/html + Sec-Fetch-Mode: navigate) and add
  Disallow: /compare to robots.txt.
- kv-cache: per-isolate in-memory hot cache so repeat lookups don't
  spend KV reads; keep cache writes alive past the response with
  waitUntil; log put failures instead of swallowing them; fall through
  to the database when KV reads throw (quota exhaustion) instead of
  500ing the page.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
unclouded edae8fd Commit Preview URL

Branch Preview URL
Jul 13 2026, 01:59 AM

@gavdevs
gavdevs merged commit 80c2a75 into main Jul 13, 2026
2 checks passed
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