{
  "$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"
}