Skip to content

negative-cache 404s, materialize desktop-only flag#19

Merged
gavdevs merged 2 commits into
mainfrom
fix/turso-read-amplification
Jul 15, 2026
Merged

negative-cache 404s, materialize desktop-only flag#19
gavdevs merged 2 commits into
mainfrom
fix/turso-read-amplification

Conversation

@gavdevs

@gavdevs gavdevs commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

Closes the two remaining Turso read-amplification leaks behind the July rows-read spike: uncached 404s (scrapers re-render dead slugs forever) and the desktopOnlyAppIds subquery that re-scanned the whole app_tags join on every list-query cache miss.

Key Changes

  • ⚡ Rendered 404s are edge-cached for 1 hour on every route, not just routes with a cache rule — repeat probes of the same dead URL stop at the colo. 200 caching is unchanged.
  • ⚡ Null results now land in the per-isolate memory cache. They deliberately stay out of KV: the free plan's 1k writes/day is too scarce to spend on scraper probes.
  • ⚡ New apps.desktop_only column, computed once at import time (db/seed/lib/desktop-only.ts, wired into seed:import). listApps, searchApps, and getRecentApps filter on the column instead of scanning ~30k app_tags rows per cache miss.
  • 🧹 One-off pnpm db:backfill-desktop-only script for the existing database.

Motivation

Rows read peaked at ~78M/day on Jul 12 (baseline ~10M). The scraper botnet + KV TTL-expiry wave exposed both leaks: every 404 probe cost a full SSR + Turso round trips, and every list-query miss paid the app_tags scan three ways.

Testing

  • Build, typecheck, lint pass.
  • Backfill SQL verified against a SQLite fixture with 8 edge cases (cross-platform, untagged, category-slug collision): flags exactly the same set as the old subquery.
  • Not verified locally: end-to-end app behavior (no Turso creds on this machine) and whether production's Cache API stores 404s (local workerd mocks the cache). Post-deploy probe: curl -sI https://unclouded.app/apps/no-such-slug twice → second response should be 404 + X-Edge-Cache: HIT.

Risks / deploy order

The new queries reference apps.desktop_only, so the column must exist and be populated before the worker deploys:

pnpm db:push                     # additive column — safe under the old worker
pnpm db:backfill-desktop-only    # populate flags
pnpm deploy

If db:push proposes anything beyond a single ALTER TABLE apps ADD COLUMN (e.g. table recreation from unrelated drift), stop and review first.

gavdevs added 2 commits July 13, 2026 18:52
Scrapers probing nonexistent slugs and junk paths were re-running full
SSR + Turso queries on every hit, forever: null results are skipped by
the KV cache (a stored null reads back as a miss, and the 1k/day
free-plan write quota is too scarce to spend on probe traffic), and
only 200s entered the edge cache.

- Edge-cache rendered 404s for an hour on every route, not just routes
  with a cache rule, so repeat probes of the same URL stop at the colo.
- Remember null results in the per-isolate memory cache so repeat
  lookups within an isolate skip Turso without burning KV writes.
The desktopOnlyAppIds subquery scanned the entire app_tags join
(~30k rows at 5.8k apps) inside listApps, searchApps, and
getRecentApps on every cache miss — the row-read multiplier behind the
July Turso spike whenever KV misses piled up.

The set only changes at reseed, so compute it once into a new
apps.desktop_only column (db/seed/lib/desktop-only.ts, run during
seed:import) and filter on the column in the hot queries.

Deploy order: pnpm db:push (additive column, safe under the old
worker), pnpm db:backfill-desktop-only, then deploy — the new queries
reference the column, so it must exist and be populated first.
@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 d710821 Commit Preview URL

Branch Preview URL
Jul 14 2026, 01:00 AM

@gavdevs
gavdevs merged commit 8fb6ee5 into main Jul 15, 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