Skip to content

Per-entrypoint Workers Cache does not intercept ctx.exports.<Entrypoint>.fetch() loopbacks (routing correct, placement off, config verified) #6865

Description

@vinayakkulkarni

Per-entrypoint Workers Cache does not intercept ctx.exports.<Entrypoint>.fetch() loopbacks (routing correct, placement off, config verified)

Summary

With the new per-entrypoint Workers Cache (launched 2026-07-06), a cached named WorkerEntrypoint invoked via ctx.exports.<Name>.fetch() from the default entrypoint is routed correctly but the cache layer never engages. The callee runs on every request, Cf-Cache-Status is never set on the loopback response, and Cache-Tag is never stripped — the platform's own signal that the response never transited the cache.

This appears distinct from #6215: in my case the fetch is not misrouted (it reaches the correct entrypoint), and Smart Placement is disabled. The cache simply is not sitting in front of the entrypoint.

Environment

  • compatibility_date: 2026-07-06 (Workers Cache launch date)
  • wrangler: 4.107.0 (per-entrypoint cache support)
  • Placement: not set (verified via API: resources.script_runtime.placement not set, worker settings.placement = {})
  • usage_model: standard
  • Single Worker, cloudflare_module-style default export.

Configuration (verified deployed via API)

resources.script_runtime:

  • cache_options.enabled: true
  • exports.default.cache.enabled: false
  • exports.Backend.cache.enabled: true

Wrangler config:

{
  "compatibility_date": "2026-07-06",
  "cache": { "enabled": true },
  "exports": {
    "default":  { "type": "worker", "cache": { "enabled": false } },
    "Backend":  { "type": "worker", "cache": { "enabled": true } }
  }
}

This is line-for-line the canonical gateway pattern from
https://developers.cloudflare.com/workers/cache/examples/#cache-authenticated-responses

Minimal reproduction

import { WorkerEntrypoint } from 'cloudflare:workers';

// Cached entrypoint (exports.Backend.cache.enabled = true)
export class Backend extends WorkerEntrypoint {
  async fetch(request: Request): Promise<Response> {
    console.log('[BACKEND-RAN]', new URL(request.url).pathname);
    const body = JSON.stringify({ generatedAt: Date.now() });
    return new Response(body, {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'public, max-age=604800', // plain, no SWR/private/no-store
        'Cache-Tag': 'backend:demo',
      },
    });
  }
}

// Default entrypoint (exports.default.cache.enabled = false)
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // Diagnostic route: prove routing + inspect cache status on two loopback
    // calls to the SAME url, from the top-level frame.
    if (url.pathname === '/probe') {
      const target = 'https://example.com/data/' + Math.floor(Math.random() * 1e6);
      const r1 = await ctx.exports.Backend.fetch(new Request(target));
      await new Promise((res) => setTimeout(res, 1500));
      const r2 = await ctx.exports.Backend.fetch(new Request(target));
      return new Response(
        JSON.stringify(
          {
            target,
            req1_cfcache: r1.headers.get('cf-cache-status'),
            req2_cfcache: r2.headers.get('cf-cache-status'),
            req2_cachetag: r2.headers.get('cache-tag'),
          },
          null,
          2,
        ),
        { headers: { 'content-type': 'application/json' } },
      );
    }

    // Clean GET, no Authorization — no bypass trigger.
    const clean = new URL(url);
    clean.search = '';
    return ctx.exports.Backend.fetch(new Request(clean, { method: 'GET' }));
  },
};

Results

Routing is correct — calling ctx.exports.Backend.fetch(<bogus path>) runs Backend.fetch (not the default handler), and RPC methods on the same entrypoint resolve correctly. So this is not the #6215 misrouting.

Cache never engages/probe output (two loopback calls to the same URL, 1.5s apart, top-level frame):

{
  "req1_cfcache": null,
  "req2_cfcache": null,
  "req2_cachetag": "backend:demo"
}

Cf-Cache-Status is absent on both, Cache-Tag is never stripped, and [BACKEND-RAN] logs on every request (N runs for N requests) — including consecutive requests to the same URL within a single top-level invocation.

Isolation summary

Variable State Result
Config (cache_options, per-entrypoint exports) verified correct via API cache still absent
compatibility_date 2026-07-06 cache still absent
Smart Placement off (not set) cache still absent (so not #6215)
Request shape clean GET, no Authorization, query stripped cache still absent
Cache-Control plain public, max-age=604800 cache still absent
Routing (ctx.exports.Backend.fetch) reaches Backend.fetch correct
RPC on same entrypoint resolves correctly correct
Cf-Cache-Status on loopback response never present
Cache-Tag on loopback response never stripped

Expected vs actual

  • Expected: the second loopback call to the same URL returns Cf-Cache-Status: HIT and Backend.fetch does not run (per docs: "loopback fetch() calls between entrypoints via ctx.exports" go through the cache).
  • Actual: no Cf-Cache-Status, Cache-Tag never stripped, Backend.fetch runs every request.

Question

Is per-entrypoint Workers Cache expected to be fully functional for ctx.exports loopbacks on all accounts as of the 2026-07-06 launch, or is there a rollout/account gate? Given routing is correct and placement is off, what would prevent the cache layer from sitting in front of a cache.enabled: true entrypoint?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions