Skip to content

feat(mcp-cloudflare): Share allowlisted cache entries across isolates through R2 - #1842

Open
sentry-junior[bot] wants to merge 1 commit into
feat/hosted-response-cache-memoryfrom
feat/hosted-response-cache-r2
Open

sentry-junior[bot] wants to merge 1 commit into
feat/hosted-response-cache-memoryfrom
feat/hosted-response-cache-r2

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Task 7.3 of the #1669 plan. It is stacked on #1833 (7.1) and adds the R2 tier behind MCP_RESPONSE_CACHE=tiered. Merge #1833 first, and only after the buckets below exist.

Still off everywhere. No config sets MCP_RESPONSE_CACHE, so the new binding is inert in every deployed environment. When tiered has no bucket, it logs one warning and caches in memory only.

What goes to R2 (src/server/lib/r2-cache-store.ts):

  • Allowlist only. It covers /api/0/organizations/{org}/events/{project}:{event_id}/ without averageColumn, and /api/0/projects/{org}/{project}/trace-items/{id}/ without debug. Every other URL uses the memory store unchanged.
  • Server opt-in. An entry is written only when the response has Cache-Control: private with a positive max-age, and no no-store or no-cache. Until feat(api): Add opt-in max-age mode to ConditionalGetResponseMixin sentry#128828 ships, R2 stores nothing.
  • Expiry. Each object records expiresAt = createdAt + min(max-age, 5 min) in custom metadata, and reads enforce it. An R2 hit is copied into memory only for the time it has left. A 1-day lifecycle rule is only garbage collection.
  • Writes run in waitUntil. Invalidation touches memory only, as in the design. A write under the same path, such as deleting a project, does not clear R2 within the 5-minute window. This is part of the 7.4 sign-off.

Identity isolation. Object keys are <grant identity>/<HMAC cache key>, and a read also rejects an entry whose stored identity differs. Vary: Authorization makes the policy keep request headers, so the cache now stores sha256: of the Authorization value, never the raw upstream token. That applies to memory too. The shared policy now keeps Cache-Control on entries so the store can decide.

Tests (workerd, real miniflare R2 bucket):

  • A cross-isolate allowlisted hit: a second isolate with an empty memory store is served from R2, and the hit is copied into its memory.
  • Two identities on the same URL: B misses and gets its own object, and A's object is never served to B.
  • No R2 write without server headers, for a public response, or for max-age=0. In those cases the entry stays memory-only.
  • Volatile and non-allowlisted URLs (issue lists, averageColumn, debug) are memory only.
  • Expiry is enforced on read: max-age=3600 is capped at 5 minutes, and max-age=60 expires at 60 s.
  • The stored object never contains the upstream token.
  • Mutation checks: removing the Authorization hashing breaks its test, and so does disabling the 5-minute cap.

Metrics (7.5): app.response_cache.r2.lookup (hit/miss/expired) and app.response_cache.r2.write, plus warning logs for R2 failures. Both are documented in docs/operations/monitoring.md.

Provisioning (required before merge). Deploys use --experimental-auto-create=false, so a Cloudflare admin must run:

pnpm exec wrangler r2 bucket create sentry-mcp-response-cache
pnpm exec wrangler r2 bucket lifecycle add sentry-mcp-response-cache expire-1d --expire-days 1 --force
pnpm exec wrangler r2 bucket create sentry-mcp-canary-response-cache
pnpm exec wrangler r2 bucket lifecycle add sentry-mcp-canary-response-cache expire-1d --expire-days 1 --force

I created nothing. The same steps are in docs/cloudflare/architecture.md.

Pending, needs a deployed Worker: 7.2 (zstd) and the 7.5 preview check, as described in #1833. With tiered on a preview, also check that r2.lookup shows up. r2.write stays at zero until the server headers ship. 7.4 is a human privacy/security sign-off.

Checks: pnpm run tsc and pnpm run lint pass, and lint also passes with no built dist, as in CI. docs:check and test:ci-projects pass. Tests pass for toolkit-core (381), mcp-core (1932, 6 skipped), and mcp-cloudflare (499).

via @BYK.

--

View Session [Sentry]

… through R2

Add the R2 tier of the hosted response cache behind
MCP_RESPONSE_CACHE=tiered. TieredStore reads memory, then R2, for
organization event details (without averageColumn) and project
trace-item details (without debug). It writes to R2 only when the
response carries Cache-Control: private with a positive max-age, and
expires objects at the lesser of max-age and 5 minutes, checked on read.
Writes run in waitUntil, and a 1-day bucket lifecycle rule collects
garbage.

Authorization values kept for Vary matching are stored as SHA-256
hashes, never as tokens.

Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
@vercel

vercel Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
sentry-local Ready Ready Preview Nov 3, 2026 12:58am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — ef733468 Deployed Nov 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant