{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreie7635shlpx5emnvttjunytkpmlvqy3skwcogv2vc3b3tadudzk5m",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mp5tfsb2yay2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiccujsutv3fmlgjxxvszsrfjt64k5saubmtvddvtfmiyhm6s2k5ee"
},
"mimeType": "image/webp",
"size": 74320
},
"path": "/riversea/a-one-line-cache-key-bug-cost-me-187month-and-leaked-advertiser-data-across-tenants-5eea",
"publishedAt": "2026-06-26T01:11:46.000Z",
"site": "https://dev.to",
"tags": [
"ai",
"aiagents",
"mcp",
"cloudflare",
"Full post →"
],
"textContent": "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\n\n // before\n const cacheKey = `mcp:context:${requestId}`;\n\n // after\n const 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` being served to sessions belonging to tenant `b3c1`. 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` hook 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` per request — doesn't translate cleanly to long-running Node processes on something like Fly.io. There, `AsyncLocalStorage` is 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` hook implementation, the KV/D1 cache key enforcement pattern, and the cases where this isolation design is overkill — over on riversealab.com.\n\nFull post →",
"title": "A one-line cache key bug cost me $187/month and leaked advertiser data across tenants"
}