From 6093d79a4ec39e17e8df26e2c0b50d98a8453f27 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Wed, 18 Mar 2026 11:26:51 -0300 Subject: [PATCH 1/3] feat(cache): prevent bots from writing to cache or triggering revalidation Bots can read from cache but must not write to it or trigger background revalidation. They often hit arbitrary URLs with unique query params that would pollute all cache tiers with one-hit entries. - Detect bot requests via User-Agent (existing isBot utility) - Skip cache.put() for bot requests - Skip background revalidation for bot requests - Bots still get served stale/cached responses (read-only) Co-Authored-By: Claude Opus 4.6 (1M context) --- blocks/loader.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/blocks/loader.ts b/blocks/loader.ts index 9e40027c9..95f2ba953 100644 --- a/blocks/loader.ts +++ b/blocks/loader.ts @@ -16,6 +16,7 @@ import { } from "../observability/otel/metrics.ts"; import { caches, ENABLE_LOADER_CACHE } from "../runtime/caches/mod.ts"; import { inFuture } from "../runtime/caches/utils.ts"; +import { isBot } from "../utils/userAgent.ts"; import type { DebugProperties } from "../utils/vary.ts"; import type { HttpContext } from "./handler.ts"; import { @@ -207,6 +208,10 @@ const wrapLoader = ( const loader = ctx.resolverId || "unknown"; const start = performance.now(); let status: "bypass" | "miss" | "stale" | "hit" | undefined; + // Bots can read from cache but must not write to it or trigger background + // revalidation — they often hit arbitrary URLs with many query params and + // would pollute the cache with one-hit entries. + const isBotRequest = isBot(req); const isCacheEngineDefined = isCache(maybeCache); const isCacheDisabled = !ENABLE_LOADER_CACHE || @@ -305,16 +310,20 @@ const wrapLoader = ( ["Content-Length", "" + jsonStringEncoded.length], ]; - // Cache write goes through the full chain (LRU → filesystem) - // so the LRU registers the key for fast match lookups. - // The filesystem layer offloads the actual I/O to a worker thread - // when DECO_CACHE_WRITE_WORKER=true. - cache.put( - request, - new Response(jsonStringEncoded, { - headers: Object.fromEntries(headerPairs), - }), - ).catch((error) => logger.error(`loader error ${error}`)); + // Bots must not write to cache — they hit arbitrary URLs and would + // pollute all cache tiers with one-hit entries. + if (!isBotRequest) { + // Cache write goes through the full chain (LRU → filesystem) + // so the LRU registers the key for fast match lookups. + // The filesystem layer offloads the actual I/O to a worker thread + // when DECO_CACHE_WRITE_WORKER=true. + cache.put( + request, + new Response(jsonStringEncoded, { + headers: Object.fromEntries(headerPairs), + }), + ).catch((error) => logger.error(`loader error ${error}`)); + } return json; }; @@ -336,8 +345,13 @@ const wrapLoader = ( status = "stale"; stats.cache.add(1, { status, loader }); - bgFlights.do(request.url, callHandlerAndCache) - .catch((error) => logger.error(`loader error ${error}`)); + // Bots get the stale response but must not trigger revalidation — + // running the handler for a bot request would waste CPU and still + // not write to cache. + if (!isBotRequest) { + bgFlights.do(request.url, callHandlerAndCache) + .catch((error) => logger.error(`loader error ${error}`)); + } } else { status = "hit"; stats.cache.add(1, { status, loader }); From b34270de0a4757b76e777a7cd00ae29f0b982c9f Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Wed, 18 Mar 2026 11:47:05 -0300 Subject: [PATCH 2/3] fix(cache): prevent bot requests from leading shared singleFlight Bot requests skip cache writes and revalidation. If a bot became the singleFlight leader, concurrent non-bot callers would inherit that behavior and miss their cache write. Fix: bots bypass singleFlight entirely and run staleWhileRevalidate directly. Co-Authored-By: Claude Opus 4.6 (1M context) --- blocks/loader.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/blocks/loader.ts b/blocks/loader.ts index 95f2ba953..b6ef35d87 100644 --- a/blocks/loader.ts +++ b/blocks/loader.ts @@ -360,6 +360,12 @@ const wrapLoader = ( return await matched.json(); }; + if (isBotRequest) { + // Bots must not participate in shared flights — if a bot becomes the + // leader, its closure skips cache.put and revalidation, which would + // suppress writes for all concurrent non-bot callers sharing the flight. + return await staleWhileRevalidate(); + } return await flights.do(request.url, staleWhileRevalidate); } finally { const dimension = { loader, status }; From 844cac8671e71a4b6d9871fca25a3e28be053844 Mon Sep 17 00:00:00 2001 From: Guilherme Rodrigues Date: Wed, 18 Mar 2026 11:58:56 -0300 Subject: [PATCH 3/3] fix(cache): use separate singleFlight keys for bot vs non-bot requests Instead of bypassing singleFlight entirely (which wastes CPU on duplicate bot requests), use a prefixed flight key so bots still deduplicate among themselves but never become leader for non-bot callers. Co-Authored-By: Claude Opus 4.6 (1M context) --- blocks/loader.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blocks/loader.ts b/blocks/loader.ts index b6ef35d87..17826c2a3 100644 --- a/blocks/loader.ts +++ b/blocks/loader.ts @@ -360,13 +360,13 @@ const wrapLoader = ( return await matched.json(); }; - if (isBotRequest) { - // Bots must not participate in shared flights — if a bot becomes the - // leader, its closure skips cache.put and revalidation, which would - // suppress writes for all concurrent non-bot callers sharing the flight. - return await staleWhileRevalidate(); - } - return await flights.do(request.url, staleWhileRevalidate); + // Bots use a separate flight key so they deduplicate among themselves + // but never become leader for non-bot requests (bot leaders skip + // cache.put and revalidation, which would suppress writes for non-bots). + const flightKey = isBotRequest + ? `bot:${request.url}` + : request.url; + return await flights.do(flightKey, staleWhileRevalidate); } finally { const dimension = { loader, status }; if (OTEL_ENABLE_EXTRA_METRICS) {