
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Thu, 09 Jul 2026 09:21:43 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Your Worker can now have its own cache in front of it]]></title>
            <link>https://blog.cloudflare.com/workers-cache/</link>
            <pubDate>Mon, 06 Jul 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[ We are launching Workers Cache, a regionally tiered cache that sits directly in front of your Worker entrypoints. Infinitely composable, configured via standard HTTP headers ]]></description>
            <content:encoded><![CDATA[ <p>Today we are launching <b>Workers Cache</b>: a <a href="https://developers.cloudflare.com/cache/how-to/tiered-cache/"><u>tiered cache</u></a> that sits in front of your Worker, configured by a single line of Wrangler config and the same <code>Cache-Control</code> headers you already know.</p><p>When Workers Cache is enabled, every <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.3"><u>cacheable</u></a> request to your Worker hits Cloudflare's cache first. If there's a fresh cached response, Cloudflare returns it directly — your Worker doesn't run, and you don't pay CPU time for it. On a miss, your Worker runs, and if your response is cacheable, Cloudflare stores it for the next request. The next request from anywhere on Earth can be served straight from cache.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2Xyk1rkPl9p44y48mdoPk/2178d252281c0165e03e7cb9245a4c3a/BLOG-3262_image1.png" />
          </figure><p>The whole thing is one config block:</p>
            <pre><code>{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": {
    "enabled": true
  }
}</code></pre>
            <p>After that, you control caching the way HTTP has always wanted you to — by setting headers on your responses:</p>
            <pre><code>return new Response(body, {
  headers: {
    "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
    "Cache-Tag": "products,product:123",
  },
});</code></pre>
            <p>And when content changes, your Worker purges its own cache:</p>
            <pre><code>await ctx.cache.purge({ tags: ["product:123"] });</code></pre>
            <p>That's the whole API. There is no zone to configure, no rules engine to set up, no separate cache to provision, and no second product to log into. The Worker's code is the configuration surface, and the cache follows the Worker wherever it runs — on a custom domain, on <code>workers.dev</code>, behind a service binding, in a preview, in a <a href="https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/"><u>Workers for Platforms</u></a> tenant. One Worker, one cache, configured once.</p><p>That's the surface area. There’s a lot underneath: tiered caching across our entire network, full support for <a href="https://developers.cloudflare.com/changelog/post/2026-02-26-async-stale-while-revalidate/"><code><u>stale-while-revalidate</u></code></a> so stale responses never block a user, content negotiation via <a href="https://developers.cloudflare.com/workers/cache/#content-negotiation-with-vary"><code><u>Vary</u></code></a>, multi-tenant-safe cache keys via <a href="https://developers.cloudflare.com/workers/runtime-apis/context/#props"><code><u>ctx.props</u></code></a>, programmatic purges by tag or path prefix, and — the part we think is the biggest unlock — a cache that sits in front of every Worker entrypoint, not just the public one, with per-entrypoint control over which ones cache and which don't. That last piece means you can compose caching directly into the structure of your app: a chain of entrypoints with cache stages slotted in wherever you want them, configured by the code on either side. We'll walk through all of it below.</p><p>Workers Cache is available today to every Worker on any plan, enabled in Wrangler.</p><p>This is the caching API we've always wanted Workers to have. Here's why it took us this long, what becomes possible because of it, and what's coming next.</p>
    <div>
      <h2>Why server-rendered apps need a cache in front</h2>
      <a href="#why-server-rendered-apps-need-a-cache-in-front">
        
      </a>
    </div>
    <p>When we <a href="https://blog.cloudflare.com/introducing-cloudflare-workers/"><u>introduced Workers in 2017</u></a>, the pitch was that you could run code on Cloudflare's network to transform requests on their way to your origin. The Worker sat <i>in front of</i> the cache and the origin:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1MoDb2Es6lrS9KoNqnakGF/1b67e2672e892c6313c186e3b9c708af/BLOG-3262_image5.png" />
          </figure><p>This was the right model for the use cases we were targeting. If you wanted to add a header to every request, rewrite a URL, do an A/B split, or filter traffic before it reached your origin, putting the Worker in front of the cache and the origin gave you full control over what got cached and what didn't. Customers built incredible things with it.</p><p>But the world changed. Workers stopped being a thing you bolted onto an origin and started being <i>the</i> origin. Frameworks like <a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/"><u>Astro</u></a>, <a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start/"><u>TanStack Start</u></a>, <a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/"><u>Next.js</u></a>, <a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/remix/"><u>Remix</u></a>, and <a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/svelte/"><u>SvelteKit</u></a> all ship a Cloudflare adapter that builds your app as a Worker. There's no origin behind them. The Worker <i>is</i> the server.</p><p>When the Worker is the origin, the original architecture has nothing to cache. Every request runs your code, even when the response would be byte-for-byte identical to the one you returned a second ago. The <a href="https://x.com/KentonVarda/status/1783996343813652801"><u>Workers runtime is fast enough that this works</u></a> — it routinely handles tens of millions of requests per second without breaking a sweat — but "fast enough to render every request" still costs you latency on every page load and CPU time on every invocation. And on a server-rendered app, every page load is, by definition, a render.</p><p>Workers Cache flips the architecture. Cloudflare's cache now sits in front of the Worker:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/19tIUZXwZR2RQKIsT39JBR/1587def81b84c1d0b5f9bee005b3c88f/BLOG-3262_image9.png" />
          </figure><p>On a cache hit, your Worker doesn't run at all. Cloudflare returns the cached response and your CPU billing stays at zero. On a miss, your Worker runs once, populates the cache, and the next request — from anywhere — gets served from cache without invoking your code.</p><p>This is what was missing for server-side rendering on Workers. You used to have to choose between two unsatisfying options:</p><ul><li><p><b>Prerender everything at build time</b> ("static site generation"). Fast page loads, but every change requires a full rebuild and redeploy. For a docs site with a few thousand pages, that's 5–10 minutes. For a large e-commerce site, it's worse — and the build runs every single time you touch anything.</p></li><li><p><b>Render every page on every request.</b> Up-to-date content, but every page load pays the rendering cost and every visitor pays the latency.</p></li></ul><p>Workers Cache gives you a third option: server-render on demand, cache the rendered response, refresh it on a time-to-live (<a href="https://developers.cloudflare.com/cache/how-to/edge-browser-cache-ttl/"><u>TTL</u></a>) you choose. The first request to a new page still renders. Every subsequent request, until the cache expires, is served as if the page were static. When the cache expires, the next request triggers a re-render — and with <code>stale-while-revalidate</code>, even that one doesn't wait.</p><p>You get the speed of a static site without the build time, and the freshness of server rendering without the cost. No framework-specific machinery like Incremental Static Regeneration. Just HTTP caching, working the way it was designed to work, in front of code that was designed to be the origin.</p>
    <div>
      <h2><code>stale-while-revalidate</code> is the part that makes it feel instant</h2>
      <a href="#stale-while-revalidate-is-the-part-that-makes-it-feel-instant">
        
      </a>
    </div>
    <p>The <a href="https://datatracker.ietf.org/doc/html/rfc5861#section-3"><code><u>stale-while-revalidate</u></code></a> directive tells Cloudflare that when a cached response expires, it's allowed to serve the stale copy immediately <i>while it refreshes the response in the background</i>. Cloudflare <a href="https://developers.cloudflare.com/changelog/post/2026-02-26-async-stale-while-revalidate/"><u>shipped full support for </u><code><u>stale-while-revalidate</u></code><u> earlier this year</u></a>, and it's the directive that turns "we cache your Worker" into "your Worker's site feels static."</p><p>Without it, the first request after a cache entry expires has to wait for the Worker to render the page from scratch. The user sees that latency. With it, the first request after expiration gets the stale page immediately (with a <code>Cf-Cache-Status: UPDATING</code> header), and the Worker runs in the background to refill the cache. Every user, including the one who triggered the refresh, gets a cache-speed response.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5xWY9m7E5NKrcecXkJXNpi/2a9fc007577d0131c5e9efabc00bd24e/BLOG-3262_image3.png" />
          </figure><p>In practice, this looks like:</p>
            <pre><code> export default {
  async fetch(request) {
    const html = await renderPage(request);
    return new Response(html, {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
        // Treat as fresh for 5 minutes; serve stale for up to an hour
        // while a background refresh runs.
        "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
      },
    });
  },
};</code></pre>
            <p>The mental model that makes this click:</p><ul><li><p><b>Fresh window</b> (<code>max-age</code>): Cloudflare serves the cached response. Your Worker doesn't run.</p></li><li><p><b>Stale window</b> (<code>stale-while-revalidate</code>): Cloudflare serves the cached response. Your Worker runs in the background to refresh it. No user waits.</p></li><li><p><b>Outside both windows</b>: Cloudflare runs your Worker to generate a fresh response, and the user waits for that one render.</p></li></ul><p>You pick the windows. For a product catalog that updates every few minutes, <code>max-age=300</code>, <code>stale-while-revalidate=3600</code> means visitors basically never wait, and your Worker still runs often enough to keep content fresh. For a blog archive that almost never changes, <code>max-age=86400</code>, <code>stale-while-revalidate=2592000</code> means your Worker runs once a day per page.</p><p>The first request to a brand-new page is the only one that pays the full render cost. After that, the page behaves like static output for visitors, while your Worker still owns how the page gets generated.</p>
    <div>
      <h2>One URL, many representations: <code>Vary</code> works</h2>
      <a href="#one-url-many-representations-vary-works">
        
      </a>
    </div>
    <p>Real apps rarely return the same bytes to every client. The same product page might be HTML for a browser and JSON for an API client. The same image might be WebP for clients that support it and JPEG for the ones that don't. The same homepage might come back in English, French, or Japanese depending on the user.</p><p>Doing this without a cache is easy — your Worker just reads the request header and returns the right thing. Doing it <i>with</i> a cache is where it usually gets ugly. Most caches give you two bad options: cache nothing on URLs that have multiple representations, or cache one representation and serve it to everyone.</p><p>Workers Cache supports the standard HTTP <code>Vary</code> header, which is the right way to solve this. When your Worker returns a response with <code>Vary: Accept-Encoding</code> (or <code>Accept</code>, or <code>Accept-Language</code>, or any other request header), Cloudflare stores a separate cached variant per distinct combination of those headers — and only returns a variant whose stored values match the incoming request.</p>
            <pre><code>export default {
  async fetch(request) {
    const accept = request.headers.get("Accept") ?? "";
    const wantsWebp = accept.includes("image/webp");

    const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage();

    return new Response(body, {
      headers: {
        "Content-Type": wantsWebp ? "image/webp" : "image/jpeg",
        "Cache-Control": "public, max-age=3600",
        // Cache a separate variant per distinct Accept header value.
        Vary: "Accept",
      },
    });
  },
};</code></pre>
            <p>One URL, two cached variants. A browser that sends <code>Accept: image/webp,*/*</code> gets the WebP. A browser that sends <code>Accept: image/jpeg</code> gets the JPEG. Both come from cache. Your Worker writes both variants on the first request to each, and then runs zero times for either after that.</p><p>This is the well-trodden HTTP standard for content negotiation, and Workers Cache implements it the way <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-vary"><u>RFC 9110</u></a> and <a href="https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-cache-keys-with"><u>RFC 9111</u></a> describe. There's no allowlist of what headers you can <code>Vary</code> on. You list whatever you need, and Cloudflare keys variants on the verbatim values. The docs go through the <a href="https://developers.cloudflare.com/workers/cache/configuration/#vary"><u>edge cases</u></a> — how to keep variant fan-out under control by normalizing headers in a gateway Worker, why purges invalidate all variants of a URL together, and the one case (<code>Vary: *</code>) that disables caching entirely.</p>
    <div>
      <h2>This is your Worker's cache, not your zone's</h2>
      <a href="#this-is-your-workers-cache-not-your-zones">
        
      </a>
    </div>
    <p>Before we get to what becomes possible with all this, there's a conceptual shift worth naming.</p><p>Cloudflare has had a cache forever. It's configured at the zone level: Cache Rules, Page Rules, the cached-file-extensions list, Cache Reserve, Tiered Cache topology, custom cache keys. All of it is set per zone, and historically a Worker had to either fit into that zone's configuration or work around it.</p><p>Workers Cache is different. It's <b>your Worker's cache</b> — it belongs to the Worker, not to a zone. This has a bunch of consequences that turn out to matter:</p><ul><li><p><b>There is no zone configuration to manage.</b> Cache Rules, cache level settings, the file-extensions list, Page Rules — none of them apply to Workers Cache. The Worker's <code>Cache-Control</code> headers are the configuration.</p></li><li><p><b>The cache follows the Worker, not the hostname.</b> A Worker that's bound to <code>api.example.com</code>, <code>api.example.net</code>, and invoked over a service binding shares one cache across all three. A request to <code>/users/42</code> hits the same cached entry regardless of which way in it came.</p></li><li><p><b>The cache works on </b><code><b>workers.dev</b></code><b>.</b> It works in <a href="https://developers.cloudflare.com/workers/configuration/previews/"><u>preview URLs</u></a> (each preview gets its own cache, so testing a change doesn't poison production). It works in <a href="https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/"><u>Workers for Platforms</u></a> (each user Worker has its own cache, isolated from the dispatcher and from other tenants). All of these used to be second-class citizens for caching. They aren't anymore.</p></li><li><p><b>Purges are scoped to the Worker’s entrypoint.</b> When you call <code>ctx.cache.purge({ purgeEverything: true })</code>, you're only purging your Worker entrypoint's cache. No risk of nuking your zone's other content. No risk of one Worker's deploy invalidating another's data.</p></li></ul><p>What you configure about caching, you configure in code: which paths get longer TTLs (branch on the path and set a different <code>max-age</code>), which requests bypass the cache (return <code>Cache-Control: private</code>), how the cache key is shaped (control what gets into <code>ctx.props</code>, normalize the URL in a gateway Worker before dispatching). The Worker you already wrote is the configuration surface.</p><p>The full docs go deep on this in <a href="https://developers.cloudflare.com/workers/cache/"><u>Workers Cache: your Worker's cache</u></a>.</p>
    <div>
      <h2>Two tiers, every Worker, no configuration</h2>
      <a href="#two-tiers-every-worker-no-configuration">
        
      </a>
    </div>
    <p>Workers Cache is <b>regionally tiered by default</b>. There are two layers:</p><ul><li><p><b>A lower tier</b> in the Cloudflare data center closest to the user. Every data center that receives traffic for your Worker has its own lower-tier cache.</p></li><li><p><b>An upper tier</b> that aggregates fills across the whole network. There are fewer of these, and every lower tier consults the upper tier on a miss.</p></li></ul><p>A request hits the lower tier first. On a hit, the response is served and that's the end of it. On a miss, the lower tier asks the upper tier. On a hit there, the response is returned and also stored in the lower tier on the way back. Only if both tiers miss does your Worker actually run — and the response from that run gets stored in both tiers.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/Qn6sTGmQebZz5fQZKo5us/37c11406ad6844a5131d78f628acd2f0/BLOG-3262_image2.png" />
          </figure><p>The reason this matters is that the <b>first request anywhere in the world</b> populates the upper tier. Every subsequent request, from any data center, can be served from the upper tier without your Worker running — even if the lower tier at that data center has never seen the request before. Cache hit ratios are dramatically higher than they would be with a single flat cache layer, which is exactly what you want when your Worker is the origin.</p><p>This is the same topology that powers <a href="https://developers.cloudflare.com/cache/how-to/tiered-cache/"><u>Tiered Cache</u></a> for zones today, except you don't configure it. There is no dialog for "turn on tiered cache for my Worker." Every Worker that has caching enabled gets tiering for free.</p><p>If your Worker uses <a href="https://developers.cloudflare.com/workers/configuration/placement/"><u>Smart Placement</u></a>, the cache composes cleanly with it: tiers are consulted first, and only if both miss does Smart Placement route execution close to your origin. We have more to say about how those layers interact, including a few rough edges we're <a href="https://developers.cloudflare.com/workers/cache/#smart-placement-and-the-cache"><u>planning to smooth out</u></a>, in the docs.</p>
    <div>
      <h2>Run your app near the user <i>and</i> near the data</h2>
      <a href="#run-your-app-near-the-user-and-near-the-data">
        
      </a>
    </div>
    <p>There's a recurring tension in web performance that nobody has fully resolved: you want your code to run close to the user (because the round-trip between user and server is on the critical path), and you want your code to run close to the data (because every database query is also a round-trip). Pick one, and the other gets slow.</p><p>We've spent years chasing both. Our network puts us <a href="https://www.cloudflare.com/network/"><u>within ~50ms</u></a> of about 95% of the world's Internet users. <a href="https://blog.cloudflare.com/announcing-workers-smart-placement/"><u>Smart Placement</u></a> and <a href="https://developers.cloudflare.com/changelog/post/2026-01-22-explicit-placement-hints/"><u>Placement Hints</u></a> let you keep your code close to your data without ever having to think about cloud regions. But until now, the two pieces didn't fully compose. You could do "near the user" or "near the data," and if you wanted both halves of your app to be in the right place at the same time, you had to be a Cloudflare expert. <a href="https://sunilpai.dev/posts/spatial-compute/"><u>We knew we could do better.</u></a></p><p>Workers Cache is the piece that closes the gap. Because the cache belongs to the Worker (not the zone), and because <a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/"><u>service bindings</u></a> and <a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/"><code><u>ctx.exports</u></code></a> calls between Workers go through the cache, you can build an app as a chain of Workers — each one running where it should run — with the cache as the seam between them.</p><p>The architecture looks like this:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5vOhRNLQF3RWl2u6gTdpWm/60ee01a9b099357564769de3b9354d3b/BLOG-3262_image8.png" />
          </figure><ul><li><p><b>Worker A</b> runs near the user. It handles the cheap, latency-sensitive parts of every request: authentication, rate limiting, routing, header normalization, rendering the <a href="https://developers.cloudflare.com/workers/examples/spa-shell/"><u>outer "shell"</u></a> of an HTML page that doesn't depend on data.</p></li><li><p><b>Worker B</b> runs near the data, courtesy of Smart Placement or an explicit Placement Hint. It does the heavy work: server-rendering pages that fetch data, reading product catalogs, generating search results, aggregating APIs, expensive transforms.</p></li><li><p><b>Workers Cache sits in front of Worker B.</b> When Worker A calls Worker B over a service binding, Cloudflare checks Worker B's cache first. On a hit, Worker A receives the response and Worker B doesn't run at all — no data-center hop, no database query, no rendering work.</p></li></ul><p>The cache hit path becomes: user → Worker A near the user → cache hit for Worker B → response. The data hop is paid only on a miss. Your hot pages run at the speed of code-in-front-of-the-user, and your cold pages still benefit from running near the data when they do execute.</p><p>You don't have to architect anything special to get this. Write your app as two Workers, point one at the other with a service binding, turn caching on in Worker B’s <code>wrangler.jsonc</code> file, and you're done.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4Xh7r02F9CCFAbrMuuRRpn/901c4c1cf8d24f515485e3810fa9aa4d/BLOG-3262_image7.png" />
          </figure>
    <div>
      <h2>Multi-tenant by default, with <code>ctx.props</code></h2>
      <a href="#multi-tenant-by-default-with-ctx-props">
        
      </a>
    </div>
    <p>If you're caching a Worker that returns user-specific data — say, an API that serves different content per logged-in user — you need a way to make sure one user can never see another user's cached response. The standard solution is "don't cache authenticated requests," and Cloudflare's <a href="https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass"><u>automatic bypass</u></a> for Authorization headers does exactly that. But "don't cache anything" gives up the entire performance win.</p><p>Workers Cache solves this by making the caller's <a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#ctxprops"><code><u>ctx.props</u></code></a> part of the cache key. When one Worker calls another over a service binding and passes <code>ctx.props</code> with a user ID, tenant ID, or any other identifier, callers with different props get separate cache entries. One user's response can never leak into another user's cache.</p>
            <pre><code>import { WorkerEntrypoint } from "cloudflare:workers";

interface Props { userId: string; }

export default class Backend extends WorkerEntrypoint&lt;Env, Props&gt; {
  async fetch(request: Request): Promise&lt;Response&gt; {
    // ctx.props.userId is part of the cache key. User A and User B
    // requesting the same URL get separate cached entries.
    const { userId } = this.ctx.props;
    const data = await loadUserData(userId);

    return new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=300",
      },
    });
  }
}</code></pre>
            <p>The typical pattern is to authenticate the request in a gateway Worker, strip the <code>Authorization</code> header, set the authenticated user's ID into <code>ctx.props</code>, and then call the cached backend Worker. The gateway runs on every request (it has to, to authenticate), but the expensive backend only runs when there's no cache entry for that user yet. Auth'd APIs go from "uncacheable" to "cached per user with full safety," and the cache key does the isolation for you. The docs walk through this in detail in <a href="https://developers.cloudflare.com/workers/cache/cache-keys/#multi-tenant-safety-with-ctxprops"><u>Multi-tenant safety with ctx.props</u></a> and the example in <a href="https://developers.cloudflare.com/workers/cache/examples/#per-user-authenticated-responses"><u>Per-user authenticated responses</u></a>.</p><p>Other CDNs make you choose between correctness and hit ratio: key the cache by each user’s token, or send every request back to origin for authorization. Workers Cache lets you share cached API responses at the edge while preserving per-request authorization boundaries. We don’t know of another CDN that offers this as a built-in model for authenticated, multi-tenant APIs. We’re pretty proud of it.</p>
    <div>
      <h2>A cache between every Worker entrypoint</h2>
      <a href="#a-cache-between-every-worker-entrypoint">
        
      </a>
    </div>
    <p>Here is the part of Workers Cache that we think is the biggest unlock, and it's the part that's hardest to see if you're thinking about it as "a CDN cache that happens to work in front of Workers."</p><p><b>Workers Cache sits in front of every Worker entrypoint</b> — the default export, every named <a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints"><code><u>WorkerEntrypoint</u></code></a>, and every call between entrypoints in the same Worker via <a href="https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/"><code><u>ctx.exports</u></code></a>. That last clause is the one that changes what you can build.</p><p>When one entrypoint calls another via <code>ctx.exports</code>, the cache evaluates that call the same way it would evaluate a request from a browser. A hit returns the cached response and the callee never runs. A miss runs the callee and stores its response under its own cache key — keyed by the callee's entrypoint, path, query string, and <code>ctx.props</code>. The caller still runs on every request, but anything it hands off to the callee is memoized independently.</p><p>You decide, per entrypoint, which ones cache. In your Wrangler config, the <code>exports</code> map lets you turn caching on or off for each entrypoint by name (<code>"default"</code> is the default export). Opt an entrypoint <b>in</b> to cache the responses it produces; opt one <b>out</b> to keep it running on every request. A gateway or router entrypoint — anything that authenticates, normalizes, or dispatches — should be opted out, so it always runs, and its own output is never served from cache.</p><p>That gives you a primitive you can compose. You can author a Worker as a chain of small entrypoints — auth, normalization, routing, the expensive read, the data layer — and let Workers Cache slot in wherever you want it. Each cached entrypoint is a unit of memoization with its own key, its own TTL, and its own tag namespace for purging. Anything you would want to configure about caching — when it runs, what it keys on, when it invalidates — is expressed as ordinary Worker code: which entrypoint you call, what request you forward, what <code>ctx.props</code> you pass, what <code>Cache-Control</code> you set.</p><p>To make this concrete, here's a single Worker that does three things you couldn't easily do together on any other platform: it authenticates every request, caches the expensive backend behind a multi-tenant-safe cache key, and invalidates that cache when data changes.</p><p>Caching is configured per entrypoint. The gateway must run on every request — both to authenticate and because a cached gateway response would skip that auth check — so we disable caching on the default entrypoint and enable it only on the inner one:</p>
            <pre><code>{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-01",
  "cache": { "enabled": true },
  "exports": {
    // The gateway runs on every request — don't cache it.
    "default": { "type": "worker", "cache": { "enabled": false } },
    // Cache the expensive inner entrypoint.
    "CachedBackend": { "type": "worker", "cache": { "enabled": true } }
  }
}</code></pre>
            
            <pre><code>import { WorkerEntrypoint } from "cloudflare:workers";

interface Env { API_TOKEN: string; }
interface Props { userId: string; }

// Inner entrypoint: the expensive work. Workers Cache sits in front
// of this — on a hit, this code never runs.
export class CachedBackend extends WorkerEntrypoint&lt;Env, Props&gt; {
  async fetch(request: Request): Promise&lt;Response&gt; {
    // ctx.props.userId is part of the cache key, so this is cached
    // separately for every user.
    const { userId } = this.ctx.props;
    const data = await loadExpensiveData(userId);

    return new Response(JSON.stringify(data), {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
        "Cache-Tag": `user:${userId}`,
      },
    });
  }

  // Invalidate a user's cached response. purge() is scoped to the
  // entrypoint that calls it, so it must run inside CachedBackend —
  // the entrypoint that owns the cached response.
  async invalidate(userId: string): Promise&lt;void&gt; {
    await this.ctx.cache.purge({ tags: [`user:${userId}`] });
  }
}

// Outer entrypoint: runs on every request to authenticate and route.
// Caching is disabled for it in Wrangler config (above), so it always
// runs and the auth check is never skipped by a cache hit.
export default {
  async fetch(request, env, ctx): Promise&lt;Response&gt; {
    const userId = await authenticate(request, env);
    if (!userId) return new Response("Unauthorized", { status: 401 });

    // Invalidate this user's cache on writes, from the entrypoint that
    // owns it.
    if (request.method === "POST") {
      await handleWrite(request, userId);
      await ctx.exports.CachedBackend.invalidate(userId);
      return new Response("OK");
    }

    // For reads: strip Authorization (otherwise Cloudflare's automatic
    // bypass fires and nothing caches), then dispatch to the cached
    // backend with the authenticated user's identity in ctx.props.
    const forwarded = new Request(request);
    forwarded.headers.delete("Authorization");

    return ctx.exports.CachedBackend.fetch(forwarded, {
      props: { userId },
    });
  },
} satisfies ExportedHandler&lt;Env&gt;;</code></pre>
            <p>The whole thing is one Worker. One source file. One deploy. But there are two execution stages — caching is turned off for the gateway and on for the backend in one small <code>exports</code> block — and a cache sits between them, keyed per user, invalidated by the write path, and serving stale during background refreshes. The cache stage isn't something you bolted on. It's a layer of the program, written in code.</p><p>The patterns this composes into are open-ended. The same shape works for:</p><ul><li><p><b>Caching a Durable Object.</b> Wrap the Durable Object behind an entrypoint, set <code>Cache-Control</code> on the response, and reads stop touching the Durable Object on a hit. Writes go to the DO directly and purge the cache by tag. The DO stays unaware that caching is happening.</p></li><li><p><b>Normalizing </b><code><b>Accept-Encoding</b></code><b> before </b><code><b>Vary</b></code><b>.</b> The outer entrypoint restores the original encoding from <code>request.cf.clientAcceptEncoding</code> (Cloudflare's front line normalizes it for cache efficiency) and forwards to a cached entrypoint that varies on the real value. Hit ratios stay high; clients get the right encoding.</p></li><li><p><b>Stripping tracking parameters before caching.</b> The outer entrypoint canonicalizes the URL — or sets a <a href="https://developers.cloudflare.com/workers/cache/cache-keys/#custom-cache-keys"><u>custom cache key</u></a> with <code>cf.cacheKey</code> on the <code>ctx.exports</code> call — so the cached inner entrypoint sees only the canonical form, and <code>?utm_source=anything</code> collapses to a single cache entry.</p></li></ul><p>Stack them. A single Worker can have an outer entrypoint that authenticates and routes, a normalization entrypoint that strips tracking parameters and restores encoding headers, a cached entrypoint that fronts a Durable Object, and a separate cached entrypoint for an unauthenticated public API — each connected by a cache stage you didn't configure, just decided where to put. The <a href="https://developers.cloudflare.com/workers/cache/examples/"><u>Examples page in the docs</u></a> walks through several of these end-to-end.</p><p>We don't know of another platform where you can do this. CDN caches sit in front of an origin. Function platforms run functions. We don't know of another platform that gives you a cache that sits inside a single deployable unit, between the parts of your application, with each cache stage configured by the code on either side of it. That's what Workers Cache is. And because it composes with everything else the platform already gives you — Smart Placement, Durable Objects, service bindings, <code>ctx.props</code>, <code>ctx.exports</code> — the patterns you can build are open-ended. We've barely scratched the surface in this post.</p>
    <div>
      <h2>First-class support in your framework</h2>
      <a href="#first-class-support-in-your-framework">
        
      </a>
    </div>
    <p>If you're building with Astro, the Cloudflare adapter wires up Workers Cache for you. Just add the <a href="https://docs.astro.build/en/guides/caching/#cloudflare"><u>cacheCloudflare provider</u></a> to your configuration:</p>
            <pre><code>// astro.config.mjs
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import { cacheCloudflare } from "@astrojs/cloudflare/cache";

export default defineConfig({
  adapter: cloudflare(),
  output: "server",
  experimental: {
    cache: { provider: cacheCloudflare() },
    routeRules: {
      "/products/*": { maxAge: 300, swr: 3600, tags: ["products"] },
      "/blog/*":     { maxAge: 60,  swr: 86400, tags: ["blog"] },
    },
  },
});</code></pre>
            <p>The adapter enables the cache, sets the right headers on the responses Astro generates, attaches <code>Cache-Tag</code> values for invalidation, and gives you a <code>cache.invalidate()</code> helper for purging tags when content changes. Astro pages that opt into server rendering automatically get the "render once, cache, refresh in the background" flow described above — no per-route configuration required, no framework-specific runtime layer to learn.</p><p>We're working with the maintainers of other frameworks to ship the same integration. If you build a framework adapter for Cloudflare, the <a href="https://developers.cloudflare.com/workers/cache/"><u>Workers Cache APIs</u></a> are exactly what you'd want them to be — header-driven configuration, programmatic purges, no platform-specific concepts to model.</p>
    <div>
      <h2>See your cache on the same dashboard as your Worker</h2>
      <a href="#see-your-cache-on-the-same-dashboard-as-your-worker">
        
      </a>
    </div>
    <p>Caching is only useful if you can see what it's doing. The <a href="https://developers.cloudflare.com/workers/observability/"><u>Workers Observability dashboard</u></a> now surfaces cache hit information per invocation:</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3EkUcGZeoQ5chRJFYLaJz/78cd1fbac2d8cb18152f85e97ccb27d0/BLOG-3262_image6.png" />
          </figure><p>You can see, per Worker:</p><ul><li><p><b>Cache hit ratio</b> over time. The number you want trending up after you enable caching.</p></li><li><p><b>Hits, misses, updates, bypasses</b> broken down. If your hit ratio is low, this is where you find out why — too many <code>BYPASS</code> responses (because something is setting a cookie?), too many <code>MISS</code> responses (because the cache key is partitioning more than you thought?), too many <code>UPDATING</code> responses (because <code>max-age</code> is shorter than your traffic interval?).</p></li></ul><p>Because all of this lives on the same dashboard as your Worker's other observability — logs, exceptions, CPU time, request counts — you don't have to context-switch between looking at your zone and your Worker to understand what's happening.</p>
    <div>
      <h2>Billing</h2>
      <a href="#billing">
        
      </a>
    </div>
    <p>Cache hits don't run your Worker, and they don't bill CPU time. They do count as a request at the standard <a href="https://developers.cloudflare.com/workers/platform/pricing/"><u>Workers request rate</u></a>, the same as any other invocation. Cache misses and bypasses bill normally — request + CPU time, exactly as they would without caching.</p><table><tr><td><p><b>Outcome</b></p></td><td><p><b>Request charge</b></p></td><td><p><b>CPU time charge</b></p></td></tr><tr><td><p>Cache <code>HIT</code> (Worker does not run)</p></td><td><p>Standard rate</p></td><td><p>Not billed</p></td></tr><tr><td><p>Cache <code>MISS</code> (Worker runs)</p></td><td><p>Standard rate</p></td><td><p>Billed</p></td></tr><tr><td><p>Cache <code>BYPASS</code> (Worker runs)</p></td><td><p>Standard rate</p></td><td><p>Billed</p></td></tr><tr><td><p>Static asset request</p></td><td><p>Standard rate</p></td><td><p>Not billed</p></td></tr><tr><td><p>Worker-to-worker invocation</p></td><td><p>Standard rate</p></td><td><p>Billed if the Worker runs</p></td></tr></table><p>There's no separate Workers Cache SKU and no per-GB cache storage fee. Tiered caching, purges, <code>stale-while-revalidate</code>, and the analytics described above are all included.  If a request would have run your Worker and Workers Cache serves it as a hit instead, you still pay the standard request rate, but you pay no CPU time for that request. Because of this, that cache hit costs less than rendering the same response in your Worker.</p><p>One thing to watch: when caching is enabled, requests that are normally free — <a href="https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/"><u>static asset requests</u></a> and <a href="https://developers.cloudflare.com/workers/platform/pricing/#service-bindings"><u>worker-to-worker invocations</u></a> through service bindings or <code>ctx.exports</code> — are billed at the standard request rate, because each one now consults the cache in front of your Worker.</p>
    <div>
      <h2>What's next</h2>
      <a href="#whats-next">
        
      </a>
    </div>
    <p>Things we know we want to do next:</p><ul><li><p><b>Smarter co-location with Smart Placement.</b> Today, Cloudflare chooses the upper-tier cache and Smart Placement target separately. On a full miss, the request may travel between Cloudflare locations twice: once to check the upper tier, and again to run your Worker near its data. We're working to coordinate those choices, so a miss only makes that long-distance trip once.</p></li><li><p><b>Larger response size limits.</b> At launch, all responses follow the <a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cacheable-size-limits"><u>Free plan’s cacheable size limit</u></a> (512 MB), regardless of your account. That’s temporary — the standard per-plan cache limits will apply once we finish a few rollout steps.</p></li><li><p><b>More framework integrations.</b> Astro has <a href="https://docs.astro.build/en/guides/caching/#cloudflare"><u>built-in integration with Workers Cache</u></a>. We’re working with maintainers to add similar integrations to other frameworks, including <a href="https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start/"><u>TanStack Start</u></a> and Next.js via <a href="https://vinext.dev/"><u>Vinext</u></a>.</p></li><li><p><b>An API to mark cached responses stale. </b><code>ctx.cache.purge()</code> removes matching responses from cache. We’re looking at a <code>ctx.cache.invalidate()</code> API that makes matching responses behave as expired, so the next request can still get a fast stale response with stale-while-revalidate while your Worker refreshes the cache in the background.</p></li></ul>
    <div>
      <h2>Try it</h2>
      <a href="#try-it">
        
      </a>
    </div>
    <p>Workers Cache is available today to every Worker on any plan.</p><p>To get started, add <code>"cache": { "enabled": true }</code> to your <code>wrangler.jsonc</code>, redeploy, and start setting <code>Cache-Control</code> headers. The <a href="https://developers.cloudflare.com/workers/cache/"><u>Workers Cache documentation</u></a> walks through the full feature surface — including the <a href="https://developers.cloudflare.com/workers/cache/#quickstart"><u>quickstart</u></a>, <a href="https://developers.cloudflare.com/workers/cache/cache-keys/"><u>cache keys</u></a>, <a href="https://developers.cloudflare.com/workers/cache/purge/"><u>purging</u></a>, <a href="https://developers.cloudflare.com/workers/cache/examples/"><u>composition patterns and examples</u></a>, and <a href="https://developers.cloudflare.com/workers/cache/debugging/"><u>debugging</u></a>.</p><p>Workers used to run in front of the cache. Now they can also run behind it. Use whichever side you need — or, with service bindings, both at once.</p><p>We can't wait to see what you build.</p> ]]></content:encoded>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Cache]]></category>
            <category><![CDATA[Tiered Cache]]></category>
            <category><![CDATA[Performance]]></category>
            <category><![CDATA[Developers]]></category>
            <category><![CDATA[Serverless]]></category>
            <guid isPermaLink="false">1WYEYtVwQSo4H3jKHTcKmO</guid>
            <dc:creator>Dan Lapid</dc:creator>
            <dc:creator> Connor Harwood</dc:creator>
        </item>
        <item>
            <title><![CDATA[Introducing Dynamic Workflows: durable execution that follows the tenant]]></title>
            <link>https://blog.cloudflare.com/dynamic-workflows/</link>
            <pubDate>Fri, 01 May 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[ Dynamic Workflows is a library that lets you route durable execution to tenant-provided code on the fly. Built on Dynamic Workers, it enables platforms to serve millions of unique workflows at near-zero idle cost. ]]></description>
            <content:encoded><![CDATA[ <p>When we first launched Workers eight years ago, it was a direct-to-developers platform. Over the years, we have expanded and scaled the ecosystem so that platforms could not only build on Workers directly, but they could also enable <i>their</i> customers to ship code to <i>us </i>through many multi-tenant applications. We now see on Workers: Applications where users describe what they want, and the AI writes the implementation. Multi-tenant SaaS where every customer's business logic is, at runtime, some TypeScript the platform has never seen before. Agents that write and run their own tools. CI/CD products where every repo defines its own pipeline.</p><p>Last month, when we shipped the <a href="https://blog.cloudflare.com/dynamic-workers/"><u>Dynamic Workers open beta</u></a>, we gave those platforms a clean primitive for the <i>compute</i> side: hand the Workers runtime some code at runtime, get back an isolated, sandboxed Worker, on the same machine, in single-digit milliseconds. <a href="https://blog.cloudflare.com/durable-object-facets-dynamic-workers/"><u>Durable Object Facets</u></a> extended the same idea to <i>storage</i> — each dynamically-loaded app can have its own SQLite database, spun up on demand, with the platform sitting in front, as a supervisor. <a href="https://blog.cloudflare.com/artifacts-git-for-agents-beta/"><u>Artifacts</u></a> did the same for <i>source control</i>: a Git-native, versioned filesystem you can create by the tens of millions, one per agent, one per session, one per tenant. So, we have dynamic deployment for storage and source control. What’s next?</p><p>Today, we are bridging durable execution and dynamic deployment with <a href="https://github.com/cloudflare/dynamic-workflows"><b><u>Dynamic Workflows</u></b></a>.</p>
    <div>
      <h2>The gap between durable and dynamic execution</h2>
      <a href="#the-gap-between-durable-and-dynamic-execution">
        
      </a>
    </div>
    <p><a href="https://developers.cloudflare.com/workflows/"><u>Cloudflare Workflows</u></a> is our durable execution engine. It turns a <code>run(event, step)</code> function into a program where every step survives failures, can sleep for hours or days, can wait for external events, and resumes exactly where it left off when the isolate is recycled. It's the right primitive for anything that has to "keep going" past a single request: onboarding flows, video transcoding pipelines, multi-stage billing, long-running agent loops, and — as of <a href="https://blog.cloudflare.com/workflows-v2/"><u>Workflows V2</u></a> — up to 50,000 concurrent instances and 300 new instances per second per account, redesigned for the agentic era.</p><p>But Workflows has always had one assumption baked in: the workflow code is part of your deployment. Your <code>wrangler.jsonc</code> has a block that says <i>"when the engine calls into </i><code><i>WORKFLOWS</i></code><i>, run the class called </i><code><i>MyWorkflow</i></code><i>."</i> One binding, one class. Per deploy.</p><p>That works fine if you own all the code. It's fine if you're running a traditional application.</p><p>It stops working the moment you want to let your customer ship <i>their</i> workflow.</p><p>Say you're building an app platform where the AI writes TypeScript for every tenant. Say you're running a CI/CD product where each repository has its own pipeline. Say you're using an agents SDK where each agent writes its own durable plan. In every one of these cases, the workflow is different for every tenant, every agent, every request. There is no single class to bind.</p><p>This is the same shape of problem that Dynamic Workers solved for compute and that Durable Object Facets solved for storage. We just hadn't solved it for durable execution yet.</p>
    <div>
      <h2>Dynamic Workflows</h2>
      <a href="#dynamic-workflows">
        
      </a>
    </div>
    <p><code>@cloudflare/dynamic-workflows</code> is a small library. Roughly 300 lines of TypeScript. It lets a single Worker — the <b>Worker Loader</b> — route every <code>create()</code> call to a different tenant's code, and, critically, have the Workflows engine dispatch <code>run(event, step)</code> back to that same code when the workflow actually executes, seconds or hours or days later.</p><p>Here's the whole pattern. A Worker Loader:</p>
            <pre><code>import {
  createDynamicWorkflowEntrypoint,
  DynamicWorkflowBinding,
  wrapWorkflowBinding,
} from '@cloudflare/dynamic-workflows';

// The library looks this class up on cloudflare:workers exports.
export { DynamicWorkflowBinding };

function loadTenant(env, tenantId) {
  return env.LOADER.get(tenantId, async () =&gt; ({
    compatibilityDate: '2026-01-01',
    mainModule: 'index.js',
    modules: { 'index.js': await fetchTenantCode(tenantId) },
    // The tenant sees this as a normal Workflow binding.
    env: { WORKFLOWS: wrapWorkflowBinding({ tenantId }) },
  }));
}

// Register this as class_name in wrangler.jsonc.
export const DynamicWorkflow = createDynamicWorkflowEntrypoint&lt;Env&gt;(
  async ({ env, metadata }) =&gt; {
    const stub = loadTenant(env, metadata.tenantId);
    return stub.getEntrypoint('TenantWorkflow');
  }
);

export default {
  fetch(request, env) {
    const tenantId = request.headers.get('x-tenant-id');
    return loadTenant(env, tenantId).getEntrypoint().fetch(request);
  },
};</code></pre>
            <p>Add to your <code>wrangler.jsonc</code>:</p>
            <pre><code>"workflows": [
		{
			"name": "dynamic-workflow",
			"binding": "WORKFLOW",
			"class_name": "DynamicWorkflow"
		}
	]</code></pre>
            <p>The tenant writes plain, idiomatic Workflows code. They have no idea they're being dispatched:</p>
            <pre><code>import { WorkflowEntrypoint } from 'cloudflare:workers';

export class TenantWorkflow extends WorkflowEntrypoint {
  async run(event, step) {
    return step.do('greet', async () =&gt; `Hello, ${event.payload.name}!`);
  }
}

export default {
  async fetch(request, env) {
    const instance = await env.WORKFLOWS.create({ params: await request.json() });
    return Response.json({ id: await instance.id });
  },
};</code></pre>
            <p>That's it. The tenant calls <code>env.WORKFLOWS.create(...)</code> against what looks like a perfectly normal Workflow binding. Workflow IDs, <code>.status()</code>, <code>.pause()</code>, retries, hibernation, durable steps, <code>step.sleep('24 hours')</code>, <code>step.waitForEvent()</code> — everything works the way it always has.</p><p>The library handles one thing: making sure that when the Workflows engine eventually wakes up and calls <code>run(event, step)</code>, it ends up inside the <i>right tenant's</i> code.</p>
    <div>
      <h2>How it works</h2>
      <a href="#how-it-works">
        
      </a>
    </div>
    <p>Three layers: the Workflows engine (platform) on top, your Worker Loader in the middle, your tenant's code (a Dynamic Worker) on the bottom. </p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3D8fGfZalW4N4h7QngR8tN/59ef281c194cfcba12ea6cfb6ec240d7/image2.png" />
          </figure><p>When a request reaches the Worker Loader, it routes the execution to the correct dynamic code on the fly. The rest of the execution is a handoff between these three layers, left-to-right in time: the request enters, bounces up to the engine, is persisted, and later bounces back down again.</p><p>Walking the flow:</p><p><b>① → ② Entering the tenant's code.</b> The Worker Loader receives an HTTP request, figures out which tenant it's for, loads that tenant's code via the Worker Loader, and forwards the request to its <code>default.fetch</code>. The <code>env</code> it hands the tenant contains <code>WORKFLOWS: wrapWorkflowBinding({ tenantId })</code>. As far as the tenant is concerned, that looks and acts like a real Workflow binding.</p><p><b>③ Up to the Worker Loader.</b> When the tenant calls <code>env.WORKFLOWS.create({ params })</code>, it's actually making a Remote Procedure Call (RPC) into the Worker Loader — the wrapped binding is a <code>WorkerEntrypoint</code> subclass (<code>DynamicWorkflowBinding</code>) that the runtime specialized with the tenant's metadata at load time. That's why you have to <code>export { DynamicWorkflowBinding }</code> from your Worker Loader: the runtime builds per-tenant stubs by looking the class up in <code>cloudflare:workers</code> exports. Bindings that cross the Dynamic Worker boundary <i>have</i> to be RPC stubs — a plain <code>{ create, get }</code> object can't be structured-cloned, and the raw <code>Workflow</code> binding isn't serializable either.</p><p>Inside the Worker Loader, the wrapped binding transparently rewrites the payload:</p>
            <pre><code>tenant calls:  create({ params: { name: 'Alice' } })
                            │
                            ▼
engine sees:   create({ params: {
                  __workerLoaderMetadata: { tenantId: 't-42' },
                  params: { name: 'Alice' }
               }})
</code></pre>
            <p><b>④ Up to the engine.</b> The Worker Loader then calls <code>.create()</code> on the <i>real</i> <code>WORKFLOWS</code> binding with the envelope as the params. From here the Workflows engine takes over. It persists <code>event.payload</code> — which now includes the envelope — and schedules the run. Every time the engine later wakes up the workflow (whether that’s after a 24-hour sleep, a crash, or a deploy), the metadata rides along with the payload, waiting to route the run.</p><p>One implication: treat the metadata as a routing hint, not as authorization. The tenant can read it back via <code>instance.status()</code>. Don't put secrets in there.</p><p><b>⑤ → ⑥ The engine comes back down.</b> When the engine is ready to run a step, it calls <code>.run(event, step)</code> on the class you registered in <code>wrangler.jsonc</code> — the one <code>createDynamicWorkflowEntrypoint</code> gave you. That class unwraps the envelope, hands the metadata to the <code>loadRunner</code> callback <i>you</i> wrote, and forwards the unwrapped event through to whatever runner the callback returns.</p><p>The callback is where everything interesting happens, and it's entirely yours. Fetch the tenant's latest source from R2. Check their plan tier and pick a region. Attach a tail Worker for per-tenant logging. Bundle TypeScript on the fly with <a href="https://www.npmjs.com/package/@cloudflare/worker-bundler"><code><u>@cloudflare/worker-bundler</u></code></a>. In the common case, you just hand off to the Worker Loader:</p>
            <pre><code>const stub = env.LOADER.get(tenantId, () =&gt; loadTenantCode(tenantId));
return stub.getEntrypoint('TenantWorkflow');</code></pre>
            <p>
The Worker Loader caches by ID, so a workflow that runs many steps over many hours reuses the same dynamic Worker across them. When the isolate eventually gets evicted, the next <code>step.do()</code> pulls the code again and keeps going — the tenant's workflow has no idea anything happened. A Dynamic Worker boots in single-digit milliseconds using a few megabytes of memory, so the dispatch overhead is essentially free. You can have a million tenants, each with their own distinct workflow code, each spun up lazily on the step boundary where it's needed, and none of them cost anything while idle.</p>
    <div>
      <h3>The escape hatch</h3>
      <a href="#the-escape-hatch">
        
      </a>
    </div>
    <p>If you want to subclass <code>WorkflowEntrypoint</code> yourself — to add logging around <code>run()</code>, wire up per-tenant observability, or thread custom state through — the library exposes the lower-level <code>dispatchWorkflow</code> primitive that <code>createDynamicWorkflowEntrypoint</code> is built on:</p>
            <pre><code>import { dispatchWorkflow } from '@cloudflare/dynamic-workflows';

export class MyDynamicWorkflow extends WorkflowEntrypoint {
  async run(event, step) {
    return dispatchWorkflow(
      { env: this.env, ctx: this.ctx },
      event,
      step,
      ({ metadata, env }) =&gt; loadRunnerForTenant(env, metadata),
    );
  }
}
</code></pre>
            <p>Everything else — IDs, pause/resume, <code>sendEvent</code>, retries — falls through to the real Workflows engine untouched.</p>
    <div>
      <h2>Dynamic Workers are the primitive</h2>
      <a href="#dynamic-workers-are-the-primitive">
        
      </a>
    </div>
    <p>Step back from the specifics for a second. Every interesting line of this library is either a wrapper around <code>.create()</code> on the outbound side or a wrapper around <code>WorkflowEntrypoint</code> on the inbound side. The actual work — spinning up the tenant's code, sandboxing it, routing RPC across the boundary, caching the isolate, hibernating between steps — is all done by Dynamic Workers underneath.</p><p>That's the real story, and it's a lot bigger than Workflows</p><p>Dynamic Workers is the primitive that swallows everything. <a href="https://blog.cloudflare.com/durable-object-facets-dynamic-workers/"><u>Durable Object Facets</u></a> is the same pattern applied to Durable Objects. Dynamic Workflows is that same pattern applied to <code>WorkflowEntrypoint</code>. Each one is the same small amount of envelope-and-unwrap glue between the static binding you've always had and the dynamic version you can now hand to your customers.</p><p>And we're not stopping at Workflows. Every binding that Workers currently exposes is heading for a dynamic counterpart — queues where each producer ships its own handler, caches, databases, object stores, AI bindings, and MCP servers where every tenant brings their own tools. Whatever you bind to a Worker today, you will soon be able to bind dynamically: dispatched per tenant, per agent, per request, at zero idle cost.</p><p>The unit economics of running a platform like this are, frankly, absurd. Shipping a multi-tenant product used to mean giving every customer their own container, their own database, their own disk, their own scheduler, and stitching it together with orchestration glue, service meshes, and hair-pulling billing math. Many of these applications have to support thousands of customers at the very least; millions, at the most. On Dynamic Workers and everything composing on top of them, idle tenants cost approximately nothing and active tenants share the same hardware through isolate-level multi-tenancy. The floor drops several orders of magnitude. A platform that used to cap out at thousands of paying customers can now reasonably serve tens of millions.</p>
    <div>
      <h2>What this unlocks</h2>
      <a href="#what-this-unlocks">
        
      </a>
    </div>
    
    <div>
      <h3>Agent platforms that plan like engineers</h3>
      <a href="#agent-platforms-that-plan-like-engineers">
        
      </a>
    </div>
    <p>Coding agents — <a href="https://opencode.ai"><u>OpenCode</u></a>, <a href="https://code.claude.com/docs/en/overview"><u>Claude Code</u></a>, Codex, Pi — have been proving for the past year that LLMs are far better at <i>writing code</i> than at making sequential tool calls. The <a href="https://developers.cloudflare.com/agents/"><u>Cloudflare Agents SDK</u></a> and <a href="https://blog.cloudflare.com/project-think"><u>Project Think</u></a> extend that insight into durable execution: with primitives like fibers and sub-agents, an agent's long-running plan can survive crashes, hibernation, and redeploys without the user noticing.</p><p>Dynamic Workflows is the piece that lets that plan be a <i>first-class Cloudflare Workflow</i> — something the agent literally writes and the platform literally runs, with the full durability machinery behind it. A <code>run(event, step)</code> function the model wrote a minute ago, where every <code>step.do(...)</code> is independently retryable, every <code>step.sleep('24 hours')</code> hibernates for free, and every <code>step.waitForEvent(...)</code> waits indefinitely for the human to approve the next action. The agent writes the workflow; the platform runs it; neither has to know ahead of time what the plan looks like.</p>
    <div>
      <h3>SDKs and frameworks where the user brings the logic</h3>
      <a href="#sdks-and-frameworks-where-the-user-brings-the-logic">
        
      </a>
    </div>
    <p>If you're shipping a framework where your customer writes the <code>run(event, step)</code> function — a workflow builder UI, a visual automation tool, a per-tenant extension system, a low-code tool for non-developers — Dynamic Workflows is now the primitive that makes it work without compromise. You call <code>wrapWorkflowBinding({ tenantId })</code> once, hand the result to their code as <code>WORKFLOWS</code>, and every workflow instance they create is automatically tagged, routed back, and executed in their sandbox. The framework owns the Worker Loader; the user owns the workflow; neither has to care about the other.</p>
    <div>
      <h3>CI/CD at primitive speed</h3>
      <a href="#ci-cd-at-primitive-speed">
        
      </a>
    </div>
    <p>Here's the use case that's been getting us most excited.</p><p>Every CI/CD platform in existence is, underneath, a dispatcher of per-repo configuration files: <i>"run these steps, in this order, with these secrets, cache these directories, upload these artifacts."</i> Each repo has its own pipeline. Each branch might have its own variant. Each pull request spawns an instance of that pipeline that has to run to completion, survive a machine crash, retry a flaky step, stream logs, pause for approvals, and persist results.</p><p>That's <i>exactly</i> the shape of a durable workflow. The reason CI hasn't been built that way until now is that nobody had a cloud primitive where <b>the workflow itself is different for every repo, dispatched at runtime, at zero provisioning cost.</b> Now you do.</p><p>Here's what a CI pipeline looks like when it's just code your customer ships with their repo — say, in <code>.cloudflare/ci.ts</code>. The workflow itself is real; the <code>runInSandbox() / summarise()</code> / GitHub binding helpers below are platform-provided glue, the kind of thing you'd ship once in your dispatcher:</p>
            <pre><code>import { WorkflowEntrypoint } from 'cloudflare:workers';

export class CIPipeline extends WorkflowEntrypoint {
  async run(event, step) {
    const { repo, sha, branch, pr } = event.payload;

    // Fork an isolated copy of the repo at this commit. Seconds, not minutes.
    const workspace = await step.do('checkout', () =&gt;
      this.env.ARTIFACTS.fork(repo, { sha })
    );

    await step.do('install', () =&gt; runInSandbox(workspace, ['pnpm', 'install']));

    // Each parallel step is independently retryable.
    const [lint, test, build] = await Promise.all([
      step.do('lint',  () =&gt; runInSandbox(workspace, ['pnpm', 'lint'])),
      step.do('test',  () =&gt; runInSandbox(workspace, ['pnpm', 'test'])),
      step.do('build', () =&gt; runInSandbox(workspace, ['pnpm', 'build'])),
    ]);

    if (pr) {
      await step.do('comment', () =&gt;
        this.env.GITHUB.commentOnPR(repo, pr, summarise({ lint, test, build }))
      );
    }

    // Workflow hibernates until approval arrives. No VM held open.
    if (branch === 'main') {
      await step.waitForEvent('approval', { type: 'deploy-approval', timeout: '24 hours' });
      await step.do('deploy', () =&gt; runInSandbox(workspace, ['pnpm', 'deploy']));
    }
  }
}</code></pre>
            <p>The platform owns the dispatcher. It ingests a webhook, figures out which repo it came from, loads <i>that repo's</i> <code>CIPipeline</code> class as a Dynamic Worker, and hands the run-off to Dynamic Workflows. The platform doesn't know what's in the pipeline. It doesn't need to. It's running a durable function that happens to live in the customer's repo.</p><p>Now line up what each step actually does:</p><ul><li><p><a href="https://blog.cloudflare.com/artifacts-git-for-agents-beta/"><b><u>Artifacts</u></b></a> gives every repo a Git-native, versioned filesystem that lives on Cloudflare's globally distributed network. <a href="https://github.com/cloudflare/artifact-fs"><u>ArtifactFS</u></a> hydrates the tree lazily, so even a multi-GB repo is ready to work within single-digit seconds — and <code>fork()</code> gives each CI run its own isolated copy, with no <code>git clone</code> tax.</p></li><li><p><a href="https://blog.cloudflare.com/dynamic-workers/"><b><u>Dynamic Workers</u></b></a> run each lightweight step (lint, format, typecheck, bundle) in a sandboxed isolate that boots in milliseconds, on the same machine as the repo's data. No VM provisioning, no image pull, no cold start.</p></li><li><p><a href="https://developers.cloudflare.com/dynamic-workers/usage/dynamic-workflows/"><b><u>Dynamic Workflows</u></b></a> holds the whole run together. Steps are retryable and durable. The run hibernates for free while waiting on approvals. State and progress survive deploys, evictions, and crashes.</p></li><li><p><a href="https://blog.cloudflare.com/sandbox-ga/"><b><u>Sandboxes</u></b></a> handle the heavy corners — the step that needs <code>docker build</code>, the integration suite that needs Postgres running, the Rust compile that needs 8 cores. Snapshots to R2 mean even those warm-start in a couple of seconds.</p></li></ul><p>A traditional CI run for a mid-sized JS repo looks something like: <i>allocate VM (15-30s) → pull base image (10s) → </i><code><i>git clone</i></code><i> (10s) → </i><code><i>npm ci</i></code><i> (30-60s) → run tests (actual work) → tear down</i>. Several minutes of ceremony before the first test runs, and you pay for the whole VM the whole time.</p><p>The same pipeline on this stack looks like: <i>edge fork of the repo (seconds) → each step boots a fresh isolate or snapshot-restored sandbox in milliseconds → runs the actual work → hibernates.</i> Nothing has to cold-start. Nothing has to be provisioned ahead of time. Nothing has to be kept warm. The repo doesn't move — the compute comes to it.</p><p>CI has never been this fast, and the reason it hasn't is that none of these primitives have existed together in one place. Now they do.</p>
    <div>
      <h2>Try it</h2>
      <a href="#try-it">
        
      </a>
    </div>
    <p><code>@cloudflare/dynamic-workflows</code> is MIT-licensed and on npm today:</p>
            <pre><code>npm install @cloudflare/dynamic-workflows</code></pre>
            <p>It runs on top of Dynamic Workers, which is in open beta on the Workers Paid plan. The <a href="https://github.com/cloudflare/dynamic-workflows"><u>repo</u></a> includes a working example — an interactive browser playground where you write a <code>TenantWorkflow</code> class, hit <b>Run</b>, and watch the steps execute with live-streaming logs and a per-step checklist that lights up as each <code>step.do()</code> commits. Clone it, deploy it, show it to a coworker.</p><p>If you're a platform, an SDK, a framework, or a CI/CD product, and you want to give your customers their own workflows without running their code in your own process: this is the primitive we built for you. If you're building agents that write durable plans, this is the primitive that makes those plans <i>real</i> Workflows. If you're just watching all of this, and it looks fun to build on top of: we'd love to see what you make.</p><p>Find us in the <a href="https://discord.cloudflare.com/"><u>Cloudflare Developers Discord</u></a>.</p> ]]></content:encoded>
            <category><![CDATA[Workflows]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Durable Execution]]></category>
            <category><![CDATA[Developer Platform]]></category>
            <category><![CDATA[Developers]]></category>
            <guid isPermaLink="false">4JZIrwtIvEd4qwAd4JAThB</guid>
            <dc:creator>Dan Lapid</dc:creator>
            <dc:creator>Luís Duarte</dc:creator>
        </item>
        <item>
            <title><![CDATA[Bringing streamable HTTP transport and Python language support to MCP servers]]></title>
            <link>https://blog.cloudflare.com/streamable-http-mcp-servers-python/</link>
            <pubDate>Wed, 30 Apr 2025 14:00:00 GMT</pubDate>
            <description><![CDATA[ We're continuing to make it easier for developers to bring their services into the AI ecosystem with the Model Context Protocol (MCP) with two new updates. ]]></description>
            <content:encoded><![CDATA[ <p>We’re <a href="https://blog.cloudflare.com/building-ai-agents-with-mcp-authn-authz-and-durable-objects/"><u>continuing</u></a> to make it easier for developers to <a href="https://blog.cloudflare.com/remote-model-context-protocol-servers-mcp/"><u>bring their services into the AI ecosystem</u></a> with the <a href="https://www.cloudflare.com/learning/ai/what-is-model-context-protocol-mcp/">Model Context Protocol</a> (MCP). Today, we’re announcing two new capabilities:</p><ul><li><p><b>Streamable HTTP Transport</b>: The <a href="https://agents.cloudflare.com/"><u>Agents SDK</u></a> now supports the <a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http"><u>new Streamable HTTP transport</u></a>, allowing you to future-proof your MCP server. <a href="https://developers.cloudflare.com/agents/model-context-protocol/transport/"><u>Our implementation</u></a> allows your MCP server to simultaneously handle both the new Streamable HTTP transport and the existing SSE transport, maintaining backward compatibility with all remote MCP clients.</p></li><li><p><b>Deploy MCP servers written in Python</b>: In 2024, we <a href="https://blog.cloudflare.com/python-workers/"><u>introduced first-class Python language support</u></a> in <a href="https://www.cloudflare.com/developer-platform/products/workers/">Cloudflare Workers</a>, and now you can build MCP servers on Cloudflare that are entirely written in Python.</p></li></ul><p>Click “Deploy to Cloudflare” to <a href="https://developers.cloudflare.com/agents/guides/remote-mcp-server/"><u>get started</u></a> with a <a href="https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless"><u>remote MCP server</u></a> that supports the new Streamable HTTP transport method, with backwards compatibility with the SSE transport. </p><a href="https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-authless"><img src="https://deploy.workers.cloudflare.com/button" /></a>
<p></p>
    <div>
      <h3>Streamable HTTP: A simpler way for AI agents to communicate with services via MCP</h3>
      <a href="#streamable-http-a-simpler-way-for-ai-agents-to-communicate-with-services-via-mcp">
        
      </a>
    </div>
    <p><a href="https://spec.modelcontextprotocol.io/specification/2025-03-26/"><u>The MCP spec</u></a> was <a href="https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/"><u>updated</u></a> on March 26 to introduce a new transport mechanism for remote MCP, called <a href="https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/transports/#streamable-http"><u>Streamable HTTP</u></a>. The new transport simplifies how <a href="https://www.cloudflare.com/learning/ai/what-is-agentic-ai/">AI agents</a> can interact with services by using a single HTTP endpoint for sending and receiving responses between the client and the server, replacing the need to implement separate endpoints for initializing the connection and for sending messages. </p>
    <div>
      <h4>Upgrading your MCP server to use the new transport method</h4>
      <a href="#upgrading-your-mcp-server-to-use-the-new-transport-method">
        
      </a>
    </div>
    <p>If you've already built a remote MCP server on Cloudflare using the Cloudflare Agents SDK, then <a href="https://developers.cloudflare.com/agents/model-context-protocol/transport/"><u>adding support for Streamable HTTP</u></a> is straightforward. The SDK has been updated to support both the existing Server-Sent Events (SSE) transport and the new Streamable HTTP transport concurrently. </p><p>Here's how you can configure your server to handle both transports:​</p>
            <pre><code>export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const { pathname }  = new URL(request.url);
    if (pathname.startsWith('/sse')) {
      return MyMcpAgent.serveSSE('/sse').fetch(request, env, ctx);
    }
    if (pathname.startsWith('/mcp')) {
      return MyMcpAgent.serve('/mcp').fetch(request, env, ctx);
    }
  },
};</code></pre>
            <p>Or, if you’re using Hono:</p>
            <pre><code>const app = new Hono()
app.mount('/sse', MyMCP.serveSSE('/sse').fetch, { replaceRequest: false })
app.mount('/mcp', MyMCP.serve('/mcp').fetch, { replaceRequest: false )
export default app</code></pre>
            <p>Or if your MCP server implements <a href="https://developers.cloudflare.com/agents/model-context-protocol/authorization/"><u>authentication &amp; authorization</u></a> using the Workers <a href="https://github.com/cloudflare/workers-oauth-provider"><u>OAuth Provider Library</u></a>: </p>
            <pre><code>export default new OAuthProvider({
 apiHandlers: {
   '/sse': MyMCP.serveSSE('/sse'),
   '/mcp': MyMCP.serve('/mcp'),
 },
 // ...
})</code></pre>
            <p>The key changes are: </p><ul><li><p>Use <code>MyMcpAgent.serveSSE('/sse')</code> for the existing SSE transport. Previously, this would have been <code>MyMcpAgent.mount('/sse')</code>, which has been kept as an alias.</p></li><li><p>Add a new path with <code>MyMcpAgent.serve('/mcp')</code> to support the new Streamable HTTP transport</p></li></ul><p>That's it! With these few lines of code, your MCP server will support both transport methods, making it compatible with both existing and new clients.</p>
    <div>
      <h4>Using Streamable HTTP from an MCP client</h4>
      <a href="#using-streamable-http-from-an-mcp-client">
        
      </a>
    </div>
    <p>While most MCP clients haven’t yet adopted the new Streamable HTTP transport, you can start testing it today using<a href="https://www.npmjs.com/package/mcp-remote"> mcp-remote</a>, an adapter that lets MCP clients like Claude Desktop that otherwise only support local connections work with remote MCP servers. This tool allows any MCP client to connect to remote MCP servers via either SSE or Streamable HTTP, even if the client doesn't natively support remote connections or the new transport method. </p>
    <div>
      <h4>So, what’s new with Streamable HTTP? </h4>
      <a href="#so-whats-new-with-streamable-http">
        
      </a>
    </div>
    <p>Initially, remote MCP communication between AI agents and services used a single connection but required interactions with two different endpoints: one endpoint (<code>/sse</code>) to establish a persistent Server-Sent Events (SSE) connection that the client keeps open for receiving responses and updates from the server, and another endpoint (<code>/sse/messages</code>) where the client sends requests for tool calls. </p><p>While this works, it's like having a conversation with two phones, one for listening and one for speaking. This adds complexity to the setup, makes it harder to scale, and requires connections to be kept open for long periods of time. This is because SSE operates as a persistent one-way channel where servers push updates to clients. If this connection closes prematurely, clients will miss responses or updates sent from the MCP server during long-running operations. </p><p>The new Streamable HTTP transport addresses these challenges by enabling: </p><ul><li><p><b>Communication through a single endpoint: </b>All MCP interactions now flow through one endpoint, eliminating the need to manage separate endpoints for requests and responses, reducing complexity.</p></li><li><p><b>Bi-directional communication: </b>Servers can send notifications and requests back to clients on the same connection, enabling the server to prompt for additional information or provide real-time updates. </p></li><li><p><b>Automatic connection upgrades: </b>Connections start as standard HTTP requests, but can dynamically upgrade to SSE (Server-Sent Events) to stream responses during long-running tasks.</p></li></ul><p>Now, when an AI agent wants to call a tool on a remote MCP server, it can do so with a single <code>POST</code> request to one endpoint (<code>/mcp</code>). Depending on the tool call, the server will either respond immediately or decide to upgrade the connection to use SSE to stream responses or notifications as they become available — all over the same request.</p><p>Our current implementation of Streamable HTTP provides feature parity with the previous SSE transport. We're actively working to implement the full capabilities defined in the specification, including <a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#resumability-and-redelivery"><u>resumability</u></a>, cancellability, and <a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#session-management"><u>session management</u></a> to enable more complex, reliable, and scalable agent-to-agent interactions. </p>
    <div>
      <h4>What’s coming next? </h4>
      <a href="#whats-coming-next">
        
      </a>
    </div>
    <p>The <a href="https://modelcontextprotocol.io/specification/2025-03-26"><u>MCP specification</u></a> is rapidly evolving, and we're committed to bringing these changes to the Agents SDK to keep your MCP server compatible with all clients. We're actively tracking developments across both transport and authorization, adding support as they land, and maintaining backward compatibility to prevent breaking changes as adoption grows. Our goal is to handle the complexity behind the scenes, so you can stay focused on building great agent experiences.</p><p>On the transport side, here are some of the improvements coming soon to the Agents SDK:</p><ul><li><p><b>Resumability:</b> If a connection drops during a long-running operation, clients will be able to resume exactly where they left off without missing any responses. This eliminates the need to keep connections open continuously, making it ideal for AI agents that run for hours.</p></li><li><p><b>Cancellability</b>: Clients will have explicit mechanisms to cancel operations, enabling cleaner termination of long-running processes.</p></li><li><p><b>Session management</b>: We're implementing secure session handling with unique session IDs that maintain state across multiple connections, helping build more sophisticated agent-to-agent communication patterns.</p></li></ul>
    <div>
      <h3>Deploying Python MCP Servers on Cloudflare</h3>
      <a href="#deploying-python-mcp-servers-on-cloudflare">
        
      </a>
    </div>
    <p>In 2024, we <a href="https://blog.cloudflare.com/python-workers/"><u>introduced Python Workers</u></a>, which lets you write Cloudflare Workers entirely in Python. Now, you can use them to build and deploy remote MCP servers powered by the <a href="https://github.com/modelcontextprotocol/python-sdk"><u>Python MCP SDK</u></a> — a library for defining tools and resources using regular Python functions.</p><p>You can deploy a Python MCP server to your Cloudflare account with the button below, or read the code <a href="https://github.com/cloudflare/ai/tree/main/demos/python-workers-mcp"><u>here</u></a>. </p><a href="https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/ai/tree/main/demos/python-workers-mcp"><img src="https://deploy.workers.cloudflare.com/button" /></a>
<p></p><p>Here’s how you can define tools and resources in the MCP server:</p>
            <pre><code>class FastMCPServer(DurableObject):
    def __init__(self, ctx, env):
        self.ctx = ctx
        self.env = env
        from mcp.server.fastmcp import FastMCP
        self.mcp = FastMCP("Demo")

        @mcp.tool()
        def calculate_bmi(weight_kg: float, height_m: float) -&gt; float:
            """Calculate BMI given weight in kg and height in meters"""
            return weight_kg / (height_m**2)

        @mcp.resource("greeting://{name}")
        def get_greeting(name: str) -&gt; str:
            """Get a personalized greeting"""
            return f"Hello, {name}!"

        self.app = mcp.sse_app()

    async def call(self, request):
        import asgi
        return await asgi.fetch(self.app, request, self.env, self.ctx)



async def on_fetch(request, env):
    id = env.ns.idFromName("example")
    obj = env.ns.get(id)
    return await obj.call(request)</code></pre>
            <p>If you're already building APIs with<a href="https://fastapi.tiangolo.com/"> <u>FastAPI</u></a>, a popular Python package for quickly building high performance API servers, you can use <a href="https://github.com/cloudflare/ai/tree/main/packages/fastapi-mcp"><u>FastAPI-MCP</u></a> to expose your existing endpoints as MCP tools. It handles the protocol boilerplate for you, making it easy to bring FastAPI-based services into the agent ecosystem.</p><p>With recent updates like <a href="https://blog.cloudflare.com/python-workers/"><u>support for Durable Objects</u></a> and <a href="https://developers.cloudflare.com/changelog/2025-04-22-python-worker-cron-triggers/"><u>Cron Triggers in Python Workers</u></a>, it’s now easier to run stateful logic and scheduled tasks directly in your MCP server. </p>
    <div>
      <h3>Start building a remote MCP server today! </h3>
      <a href="#start-building-a-remote-mcp-server-today">
        
      </a>
    </div>
    <p>On Cloudflare, <a href="https://developers.cloudflare.com/agents/guides/remote-mcp-server/"><u>you can start building today</u></a>. We’re ready for you, and ready to help build with you. Email us at <a href="#"><u>1800-mcp@cloudflare.com</u></a>, and we’ll help get you going. There’s lots more to come with MCP, and we’re excited to see what you build.</p>
          <figure>
          <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/77k853sJHhvZ1UQwrQWyy2/22264b8bda63bc40b6568f88ae99804c/image2.png" />
          </figure><p></p> ]]></content:encoded>
            <category><![CDATA[Phython]]></category>
            <category><![CDATA[MCP]]></category>
            <category><![CDATA[AI]]></category>
            <category><![CDATA[Cloudflare Workers]]></category>
            <category><![CDATA[Durable Objects]]></category>
            <guid isPermaLink="false">5BMzZem6hjKhNsSnI5l3BZ</guid>
            <dc:creator>Jeremy Morrell</dc:creator>
            <dc:creator>Dan Lapid</dc:creator>
        </item>
    </channel>
</rss>