{"slug": "a-one-line-cache-key-bug-cost-me-187-month-and-leaked-advertiser-data-across", "title": "A one-line cache key bug cost me $187/month and leaked advertiser data across tenants", "summary": "A developer at an ad analytics SaaS discovered that a missing tenant ID in an MCP router cache key caused Cloudflare Workers to serve cached Vectorize results from one advertiser to another, leading to a $187/month cost overrun and data leakage. The one-line fix reduced Anthropic API costs by 60% and Vectorize queries by 40%. The bug exploited the fact that V8 isolate boundaries do not isolate concurrent requests on the same warm Worker instance, allowing module-level cache objects to be shared across tenants.", "body_md": "60% of my $312 Anthropic bill last month came from a single bug: an MCP router cache key that was missing a tenant ID.\n\nThe fix was literally this:\n\n``` js\n// before\nconst cacheKey = `mcp:context:${requestId}`;\n\n// after\nconst cacheKey = `mcp:context:${tenantId}:${requestId}`;\n```\n\nThat one missing segment meant warm Cloudflare Worker instances were serving cached Vectorize results from advertiser A into advertiser B's tool responses. In a production ad analytics SaaS. Not a demo.\n\nThe counterintuitive part: I assumed V8 isolate boundaries protected me. They don't — not in the way most people think. Isolate-level isolation applies *between separate Worker deployments*, not between two concurrent requests hitting the same warm Worker instance. Module-scope variables survive across requests. So any context manager or cache object you initialize at module level is shared state, even on Workers.\n\nThe failure mode was subtle enough to take 6 weeks to find. Vectorize query volume was 3× expected — that was the first signal. Digging into logs, I found cache hits for tenant `a9f2`\n\nbeing served to sessions belonging to tenant `b3c1`\n\n. The corrupted cache contained vector search results, so every bad hit triggered a downstream re-fetch chain. That cascade is what blew up the token spend: wrong cache data → Claude retries with fresh context → Sonnet input tokens accumulate fast.\n\nAfter fixing the cache key namespace and adding a `PostToolUse`\n\nhook that throws on tenant ID mismatch in tool response metadata, Sonnet input costs dropped from ~$187/month to ~$94. Vectorize queries fell ~40% over the same period.\n\nOne thing worth flagging for anyone on a similar stack: this specific fix — scoping everything to Workers' `ExecutionContext`\n\nper request — doesn't translate cleanly to long-running Node processes on something like Fly.io. There, `AsyncLocalStorage`\n\nis the right primitive. Porting the Workers pattern directly will give you a false sense of safety.\n\nI wrote up the full breakdown — including the `PostToolUse`\n\nhook implementation, the KV/D1 cache key enforcement pattern, and the cases where this isolation design is overkill — over on riversealab.com.", "url": "https://wpnews.pro/news/a-one-line-cache-key-bug-cost-me-187-month-and-leaked-advertiser-data-across", "canonical_source": "https://dev.to/riversea/a-one-line-cache-key-bug-cost-me-187month-and-leaked-advertiser-data-across-tenants-5eea", "published_at": "2026-06-26 01:11:46+00:00", "updated_at": "2026-06-26 02:03:47.824442+00:00", "lang": "en", "topics": ["ai-infrastructure", "developer-tools", "ai-safety", "large-language-models", "ai-products"], "entities": ["Anthropic", "Cloudflare Workers", "Vectorize", "Claude Sonnet", "MCP", "V8", "Fly.io", "riversealab.com"], "alternates": {"html": "https://wpnews.pro/news/a-one-line-cache-key-bug-cost-me-187-month-and-leaked-advertiser-data-across", "markdown": "https://wpnews.pro/news/a-one-line-cache-key-bug-cost-me-187-month-and-leaked-advertiser-data-across.md", "text": "https://wpnews.pro/news/a-one-line-cache-key-bug-cost-me-187-month-and-leaked-advertiser-data-across.txt", "jsonld": "https://wpnews.pro/news/a-one-line-cache-key-bug-cost-me-187-month-and-leaked-advertiser-data-across.jsonld"}}