Today, it is difficult for AI agents to try out new APIs. They often have to navigate through a login page designed for humans and not agents, contact a human to add a payment method, generate an API key, and then figure out how to call the API.
This flow is very difficult for agents for two reasons: Agents do not have a stable identifier to sign up for an API, and they do not have a native way to pay for APIs. Because they lack these things, they often struggle to onboard onto software, which limits the growth of agentic commerce. AI agents often give up on these tasks entirely, kicking registration, payment methods, and API key generation back to humans. This makes it very difficult for agents to try out and compare many APIs.
To solve this, we’ve created Cloudflare Wallets. Starting today, you can claim a Cloudflare Wallet handle for your account, which will provide a unique username to help you better connect with merchants. Soon, you will be able to set up and use your Cloudflare Wallet to pay for APIs and content.
Earlier this month, we announced the Monetization Gateway to help Cloudflare customers get paid for their websites and applications. Monetization Gateway will support micropayments using the x402 protocol, which allows for payments to be attached to HTTP requests. These micropayments will be able to pay for uses ranging from AI inference to data to content. If you want to pay or get paid for services behind Monetization Gateway and other x402-compatible endpoints, you’ll need a wallet.
Cloudflare Wallets will allow you to store stablecoins, purchase services, and receive funds across the web. Each account with a wallet will also be able to create Virtual Wallets for its agents to enable them to buy APIs, MCP Tools, content, and more. You will be able to define guardrails for your Virtual Wallets (such as an allowance, an allow list, and a maximum transaction size) to help your agent spend money safely from your account. This will allow your agent to try out many APIs with low friction and managed risk. Wallet users will have the option to share their Cloudflare Wallet handles, which will give them a stable identity when interacting with merchants.
Building the two-sided agentic market
Cloudflare’s Monetization Gateway will allow eligible Cloudflare customers to sell their resources (such as content or APIs) headlessly to agentic buyers. But for that market to truly develop, agents need more tools to buy from merchants in a machine-native way. Wallets will add another tool to Cloudflare’s Agents SDK, enabling AI agents to easily purchase necessary APIs and content using micropayments.
There will be two types of Cloudflare Wallets: Account Wallets and Virtual Wallets.
Account Wallets are designed for humans who are owners and users of Cloudflare accounts. They will be able to add funds, delegate spend to virtual wallets managed by agents, and remove funds as needed.
Virtual Wallets, by contrast, are designed for agents and operate via API keys. Within a Virtual Wallet, an agent will be able to spend funds according to its permissions. Its maximum spend will be capped by the limit set by the owner of the Account Wallet. This framework gives agents freedom to act on behalf of users without constant manual approval while limiting an agent’s ability to overspend.
The freedom to explore
Virtual Wallets are exciting because they will allow agents to do what they’re best at: explore dozens or hundreds of services and find the best one for a particular use case. Stablecoin micropayments via x402 will make it simple to try an API without an account, allowing agents to test new options with little friction. The spending caps on Virtual Wallets are designed so that humans can let agents explore autonomously within safe spending limits. These limits may seem like constraints, but counterintuitively they give agents more freedom. If an agent is responsible for $10, you can worry less about its spending than if it is responsible for $1,000. If an API only costs a few cents to try, then $10 is more than sufficient to pursue and evaluate many options.
Once you or your agent has picked an API to use, policies set by you in your Account Wallet will act as cost controls for Virtual Wallets. Want to give every employee a $100 per week budget for AI inference? Simply provision an Account Wallet with the right balance and create Virtual Wallets for each employee with that rule. Anyone who exceeds the limits on their Virtual Wallet will be able to request a manual override from a human who is authorized to make changes to the Account Wallet.
We want to make it easy for Account Wallets to set flexible yet firm spending policies that do not require daily, active monitoring. When something anomalous happens, such as unexpectedly fast spending, a human will be able to review and confirm whether everything is operating as intended. If the spend was intentional, then the administrator of the Account Wallet will be able to raise the limit or approve a one-time injection of funds. If the spend was unintentional, then the spending policies for adding funds to virtual wallets did their job by imposing caps.
We are working to make it as easy as possible to fund and use these wallets. We will start with simple ways to onramp and offramp funds within supported geographies, with self-funding via stablecoins available as an alternative for eligible users. The Internet will not shift completely overnight, but with a majority of traffic on the web now being driven by bots, we are excited to give agents and merchants first-class tools for agentic commerce.
Beyond payments alone
Allowing humans to delegate authority to agents to easily buy and sell services is a helpful starting point. But this delegation is not always obvious to the merchants as they interact with agents. Today, if an agent comes to your website, you may know little about them as a user, despite the fact that the agent is acting on behalf of an individual or an organization. This lack of attribution challenges many traditional web business models. It’s easy to give a one-week free trial or sign-up credits to a human or an organization. It’s hard to give these same perks to an agent that lacks a stable identity and when one human can spin up dozens of agents under their control.
We solve this problem by linking wallets to a Cloudflare account via cloudflare.pay. cloudflare.pay will allow agents to optionally identify themselves, since their identity is a delegate of the account. A research agent could live at research.example.cloudflare.pay, allowing merchants to know that it is an agent from a particular organization. This approach will permit agents to maintain consistent and persistent identities, making the experience better for all parties. It will be completely optional for agents to choose to declare their identity or not, and it will be up to businesses to decide whether they want to prioritize transacting with known agents.
Agent identifiers should be human-readable
We believe that the approach to dealing with agents will look like the approach to dealing with VPNs: If someone is unidentified, they are not inherently untrustworthy, but they need to prove themselves more. This is why we have Turnstile and other initiatives to detect bots within Bot Management. Our identity primitive will build on top of this prior work. For example, Web Bot Auth already allows agents to register their identity via a keypair. IDs attached to Cloudflare Wallets allow this keypair to become human-readable.
We know that agentic identity standards are changing quickly, which is why we wanted to keep our approach simple. We are proposing a human-readable identifier for a not-very-readable keypair, similar to the URL and IP-address pairings used in DNS. We are not trying to define a particular schema or other verification system. We only want to make identity simple to remember and easy to declare. As schemas to enrich agentic identity develop through the x402 Foundation’s initiatives, we will seek to adopt them and intend to encourage others to do the same.
The future of agentic commerce
At Cloudflare, we want to offer all the building blocks for agentic commerce to succeed. Monetization Gateway will offer a way for sellers to get paid without setting up traditional payment infrastructure. Wallets will offer a way for buyers to pay headlessly via agents. Identity will allow merchants to communicate with buyers who identify themselves or enforce identification requirements.
All of these building blocks will create a headless marketplace for the Internet. If you are excited about this and want to participate, you can claim your handle now. We’re excited to see what you build and monetize.
Everyone is talking about software factories: the idea that AI agents can be assembled into a pipeline that produces working software on their own, the way a factory turns raw materials into finished goods. There’s endless debate over whether that’s actually possible, how far the automation can really go, and whether the “loops” people are demoing count for anything. Some have already written them off as a failure.
Running alongside that is a quieter, more worried conversation: open source maintainers are burning out. The AI boom has made it nearly free to generate issues, pull requests, and security reports, and enormously expensive for a maintainer to read through them all. The old ways of keeping a project healthy are buckling under the volume.
Everyone has a hot take on both topics. We think we have something rarer to offer: real results. For the past several months we’ve run an automated triage pipeline on the Astro repository. It reads incoming bug reports, reproduces them in sandboxes, diagnoses the root cause, and ships preview releases for the reporter to verify. The engine underneath it grew into Flue, an open framework for building this kind of agent automation, and it’s the same tool you could use to build your own.
It wasn’t an instant success. But through a lot of iteration, we’ve used it to bring our open issues down from over 200 to about 30, and we expect to hit zero sometime in the next month. That would be the first time this repository has seen zero open issues in its 5+ year history.
We didn’t get there by declaring "issue bankruptcy," auto-closing cold tickets, or ignoring reports. We did it by automating issue triage with a team of isolated AI subagents running right inside GitHub Actions. Here’s the story of how we got there, and what you might take back to your own projects.
Starting with an agent skill
At the start of the year, we focused on automating one specific area of development: issue triage. As an open source project, manual issue triage can be one of the more time-consuming, least-rewarding parts of the job. A single issue can sometimes take hours just to reproduce, let alone fix. It was a natural (yet often overlooked) place for us to start our automation journey.
We began by developing an agent skill. This allowed us to develop and test the automation locally as maintainers, running a coding harness on our own machines. We could then run that same harness in a GitHub Action on our repo, and get total reuse of that exact same triage workflow skill.
The triage skill mirrors the exact steps we take during manual issue resolution:
Reproduce: Clone the provided reproduction repository to verify the reported issue.
Diagnose: Instrument the codebase and introduce logging to pinpoint the root cause of the bug.
Verify: Review relevant test suites, code comments, and documentation to determine if the behavior is genuinely a bug or intended functionality.
Fix: Convert the reproduction into failing unit tests, identify the appropriate solution via the architecture guide, and deploy the fix.
To prevent the frequent LLM bias toward forcing a solution when a bug might not actually exist, each phase is executed by an isolated subagent. These subagents pass information forward sequentially by compiling their discoveries into a report.md file.
Turning the skill into an automation
Following initial internal testing of the triage skill, our focus shifted toward building a fully automated pipeline. We specifically wanted to integrate this logic directly into a GitHub workflow, ensuring complete transparency so that anyone could easily audit the agent's sequential reasoning and operational steps.
As we wired it up, we realized the whole pipeline was really just a state machine driven by issue labels. Every new submission starts with the label triage needed, and once a user confirms a fix it moves to fix verified. Beyond those label transitions the pipeline holds no state of its own; it simply reads back through the issue’s existing comments to work out where a given issue is and what should happen next.
From there the flow runs on its own. When the agents land on a fix, the pipeline spins up a preview release with pkg.pr.new and posts everything back to the issue: a summary of what it found, the full logs, and instructions for installing the preview. The original reporter can then try the patch against their own project, and if they confirm it works, the automation opens a pull request linked to the issue.
From triage to a framework
As we built this out, we kept noticing that nothing about it was really specific to GitHub. Reacting to an event, running a sequence of isolated subagents, and separating their reasoning from the actions they’re allowed to take — it’s all just a workflow. One that could run just as well from a Slack message, a cron job, or a webhook as from a GitHub issue. Generalizing that realization into a runtime that works the same way regardless of where it’s deployed, or which model it’s driving, is what became Flue: an open, platform-agnostic framework for building durable agents and workflows.
Benefits of agent automation
When we first launched this automated system, we had shared concerns about its efficacy and the potential negative impacts it might have on our developer community. There was a valid fear that relying on automated bot responses might feel impersonal and create just one more disconnect between us as maintainers and our user base.
That did not happen. If anything, we talk to users more now, just in more useful places:
Engaging directly with our community members within Discord.
Actively participating in RFC discussions and addressing new feature requests.
Collaborating closely with contributors to help integrate their ideas into the framework.
Regarding the quality of automated patches, our core philosophy is that our AI agents should successfully resolve the vast majority of incoming issues. When an agent fails to identify a correct solution, we interpret that failure as an indicator of an underlying architectural or documentation issue within the codebase, pointing to one of three areas:
Opaque Abstractions: If an agent cannot interpret the boundaries between components, human developers likely struggle with the code structure as well.
Missing Documentation: Critical code segments lack explicit comments explaining the rationale behind their implementation.
Insufficient Testing: The repository suffers from a lack of comprehensive test coverage, particularly unit tests.
A clear example occurred with a series of related Hot Module Replacement (HMR) bugs. The triage bot repeatedly attempted to modify a specific if condition to resolve the issue. While this change fixed the targeted bug, it introduced regressions elsewhere due to a lack of test coverage for that specific condition. Once we added a descriptive comment explaining the exact logic governing that statement, the bot adapted and stopped attempting incorrect modifications in that area.
Every time we chase down one of these failures and add the missing comment, test, or clearer boundary, the bot gets noticeably better at that part of the codebase, and so does the next human who works on it.
Turning the workflow into a GitHub Action
Initially, our triage logic lived directly within the Astro monorepo. This coupling made iteration difficult; upgrading Flue or modifying the workflow felt like performing surgery on live infrastructure without a safety net. To solve this, we decoupled the logic into a standalone, testable repository: triagebot-action. This isolation allowed us to introduce automated testing and ensure stability before ever touching our primary codebase.
Today, this action powers issue management in Astro, and it has spread from there. Several other teams have picked it up, some using it directly, and others forking it to build their own automated "factories" tailored to their projects. That second path is really the point: triagebot-action is young and still actively evolving, so we’re sharing it less as a finished product and more as a working reference you can read, learn from, and adapt.
The wiring for the action itself looks like this:
Or point your own agent at the repository and have it read through the setup, including adding the labels the state machine relies on.
Whichever route you take, the underlying idea matters more than our specific implementation: a sustainable feedback loop that frees maintainers to focus on the framework itself instead of administering a backlog. The code is open. Fork it, strip it down, or just borrow the parts that fit your project.
Want to build something like this? Dig into the code of the triagebot-action to see how it works, or fork it as a starting point for your own repository’s automation. And if you’re building agent-based infrastructure more seriously, that’s exactly what Flue is for: dive into the Flue framework to build your own. We’d love to see what you build. Come share your "factory" stories in the Astro Discord.
Over the past four months, our AI code reviewer has flagged nearly a quarter of a million deviations from Cloudflare engineering standards (what we’ll call “violations” in this post) and blocked 16,000 merges. Our spec reviewer agent has evaluated close to 600 technical designs against the same standards before implementation began. Both systems draw from the Cloudflare Codex, a shared source of engineering guidance built for people and agents. This post explains why we built the Codex, how it supports the engineering lifecycle, and what we plan to do next.
Before the Codex (which we briefly introduced in a previous post about our AI engineering stack), developer guidance at Cloudflare lived in many places: formal documentation, repository files, chat threads, and the accumulated knowledge of individual engineers. Engineers often spent too much time searching for guidance instead of working on the problem they were trying to solve. Even after finding an answer, they could not always tell whether it was current, authoritative, or applicable to their situation.
As Cloudflare grew, that model became increasingly difficult to sustain. No engineer could read every standard, and reviewers could not reliably check every requirement. Institutional knowledge became harder to recover when people moved between teams, and guidance that was not consistently surfaced or enforced led to drift between projects.
We rebuilt this body of knowledge as the Cloudflare Codex: a governed set of engineering standards that agents can retrieve and apply at the point of work. The same guidance can now inform code review, technical design review, incident report review, and many other use cases, while engineers focus their time and judgment on the resulting findings.
Codex organization and workflow
A dedicated Codex governance model divides the Codex into distinct domains covering the engineering areas we care about. These include architectural matters (for example, frontend and control plane), cross-cutting concerns (security and reliability), specific languages (TypeScript and Rust), and several other areas. Each domain is led by an owner who is responsible for the content, consistency, and overall quality of the documents they oversee.
Codex standards use a Request for Comments (RFC) format. Requirements use the SHOULD and MUST keywords defined by RFC 2119. We also expect a front matter header to hold metadata such as the domain and RFC status. Any Cloudflare employee with a key interest and domain competency can propose an RFC through a merge request that follows the prescribed structure. The proposal then passes through several rounds of feedback from an increasingly broad group of reviewers. Once the domain owner gives final approval, the RFC becomes part of the Codex and is published to an Astro-powered internal site.
Approved RFCs can be consumed by Codex clients and agents, which may then start to flag Codex violations in code, configuration, or documentation immediately. However, they block based on Codex statements only after an RFC moves from the approved to the enforced lifecycle state. This separate promotion step gives teams time to absorb new requirements and accommodates cases where enforcement needs additional work.
The following diagram illustrates the steps in the Codex workflow:
A naive process could stop here and feed the entire Codex to a large language model (LLM) as is. Given the increasing number of RFCs we have already (60+ and counting), however, the corpus volume would put a lot of stress on the context window and impact LLM results negatively. To help guide models to the most relevant RFCs, we invoke a purpose-built agent to automatically extract and compact the SHOULD and MUST statements into a dedicated JSON structure and enrich it with metadata that supports lazy discovery and progressive disclosure. The following abridged excerpt shows the result for our control plane services RFC:
Each statement receives a stable slug identifier that remains unchanged during the extraction process even when its RFC is updated. The identifier lets us track the same statement across different systems over time, which is essential for monitoring, analysis, and exception handling.
Initially, we extracted the statements into another, more concise Markdown file rather than JSON. Over time, we moved to a richer structured format so that agents could filter the content they needed more accurately. We plan to include additional metadata for even tighter scoping, such as indicators for the software development life cycle (SDLC) stage a statement applies to (e.g., design, implementation, runtime).
Codex consumers
Several systems already use the Codex in day-to-day engineering work. Three agents show how the Codex works in practice: our AI code reviewer, spec reviewer, and incident report reviewer.
AI code reviewer
Our AI code reviewer agent, covered in a separate blog post, evaluates merge requests across several dimensions, including Codex compliance.
For each review, the agent retrieves the RFCs and parses the Codex statements. It loads full RFC bodies only when the model or coordinator needs additional context. In most cases, the statements provide enough information to explain a reported violation.
The distinction between SHOULD and MUST, together with an RFC’s status, determines how the reviewer responds. Findings from approved RFCs are non-blocking recommendations. Once an RFC is enforced, an unsatisfied MUST requirement causes the reviewer to withhold approval or block a merge request, depending on the severity.
Since the Codex’s inception earlier this year, the AI code reviewer has flagged close to 230,000 violations. Among these, almost 16,000 caused approval to be withheld (i.e., they referred to MUST statements on enforced RFCs).
Code review alternatives
A single AI code reviewer run usually takes a couple of minutes to complete due to the coordinator framework and sub-agent execution. Although the wait is very often worth the money (or tokens), engineers were calling out the delay and extra round trip involved in remediating the findings. We looked into how we could improve the experience and came up with two additional options:
For language-specific Codex requirements that can be verified mechanically, we provide custom linter configuration packages. These are aligned with our Codex specification and make it possible to surface problems in milliseconds. TypeScript was the first language to receive Codex linter support while also standardizing on oxlint (maintained by the VoidZero team who joined Cloudflare recently) for performant linter execution. A linter for Rust projects is currently under development, and Go will eventually follow to complete coverage of Cloudflare’s most commonly used languages.
To cut out the continuous integration (CI) leg from the review cycle, we made it possible to run the AI code reviewer locally through a command-line interface (CLI). It matches the coordinator functionality from CI and runs the same (OpenCode-based) agents against an automatically determined diff set, with results presented in the terminal.
We believe the linters would be useful to almost every developer and codebase, while the CLI remains an optional alternative for engineers who prefer it.
Spec reviewer
Engineers at Cloudflare regularly write design documents and technical specifications (or specs in short) before implementation. A significant subset of the Codex pertains to design, architecture, and other themes relevant to technical reviews. To catch architectural mistakes before implementation begins, we built the spec reviewer, an agent that discovers specs and evaluates them against relevant Codex requirements.
The spec reviewer operates on the Developer Platform: it runs as a Cloudflare Worker, stores its results and state in D1, routes model requests through AI Gateway, and kicks off scanning for new specs via a Cron Trigger. It starts by filtering the Codex by domains and sections relevant to specs (for example, language features and implementation-focused RFCs are disregarded). Several guiding prompts instruct the model on how to run the assessment and frame the results. The findings get rated based on severity (influenced by SHOULD and MUST keywords) and include general quality and architectural advice. On completion of a review run, a note is left on the spec document linking to a custom dashboard where review details can be inspected.
Since the beginning of May 2026, almost 600 unique open specs have been reviewed. Including reruns triggered on demand or by spec changes, we tracked over 3,200 review invocations to this date. The vast majority of findings had a “major” (65%) or “minor” (29%) severity, with “critical” findings being the minority (6%).
The following image gives an impression of what the spec reviewer UI looks like:
We plan to integrate the spec reviewer more tightly by posting comments directly on the spec documents, embedding human-agent conversations that can influence the review assessment, and flagging high-impact proposals for additional human review.
Incident report reviewer
The incident report reviewer applies the same approach to incident reports (also known as postmortems). In addition to checking that each report is complete, it evaluates whether the report clearly explains what happened, identifies contributing factors, documents the resolution, and proposes meaningful follow-up actions. These expectations are defined in a dedicated Codex RFC.
The incident report reviewer uses the same Developer Platform building blocks as the spec reviewer. This shared architecture is becoming a common pattern for our Codex agents.
Since May 2026, the reviewer has assessed more than 200 incident reports and identified gaps such as missing follow-up action items, incomplete timelines, and omitted detection signals. Among those reports, 93% covered incidents that were low-impact, internal-only, or declared preemptively. For high-severity incidents, we’ve made the reviewer mandatory as part of our comprehensive central review process, and reports are not considered complete until all findings have been addressed.
Future work
The Codex already supports agents that review code, technical designs, and incident reports. We plan to extend that model throughout the SDLC, allowing agents to surface issues consistently across design, implementation, and operations. The longer-term goal is for agents to identify issues as well as propose fixes with increasing autonomy, while engineers remain responsible for reviewing and approving those changes.
We are also expanding the Codex beyond engineering. Product, security, compliance, and trust and safety teams are beginning to add their own standards, allowing agents to evaluate work against considerations that extend beyond design and implementation alone.
Across a number of engineering workflows, Codex-backed agents have helped us surface issues sooner and apply standards more consistently. We have found AI most useful when it brings the right guidance to engineers at the point of work, and plan to keep extending the approach across Cloudflare.
We are moving toward a world in which you can store, build, test, and deploy your code fully on Cloudflare. We built the first piece with Artifacts, versioned code storage that scales to millions of repos.
We have stitched the store, build, and deploy steps together with the CI SDK, built on Cloudflare Workflows, so that you can run your continuous integration (CI) pipeline on Cloudflare. You can send artifact push events directly to your Workflow, triggering an instance of its execution — a CI job, essentially — through a new events field in your wrangler configuration file.
Then, directly from the Workflow with @cloudflare/ci installed, you can:
Automate builds: compile code from your Artifacts repo in a safe, isolated environment
Run linters and typechecks: enforce code style, catch type errors, and flag any potential issues
Cache dependencies: run your install once and cache dependencies across steps in the CI job
Execute unit tests: verify that each piece of your code works as expected
Self-heal: integrate an AI review agent to catch broken steps in your build and push commits to fix
Deploy conditionally: automatically deploy your code, only if your build step is successful
Today, everyone is building a platform, whether it’s an internal vibe coding platform or an extension of your customer-facing product via customization through code. Platforms are now using millions of repos on Artifacts to store their code, and their customers’ code, and version control across the two. But every team has their own needs for a continuous integration and deployment pipeline. For platforms, they might want to define a CI job for their own code differently from that of their customers.
Many of the end customers building on these platforms don’t want the extra headache of managing their continuous integration and continuous deployment (CI/CD) pipeline. Instead, the platform can manage the build process on their customers’ behalf: write the CI/CD pipeline once and share it across all the applications that their customers are building. Some of the platform’s customers might want to define their own CI; if so, they can write their own Workflow and run custom CI jobs on just their repo, facilitated by dynamic workflows. The beauty is, you don’t have to pick and choose: both platform-managed and custom CI can run at the same time, in the same namespace.
A CI/CD pipeline is just a Workflow
Before today, we had all the pieces to allow platforms to wire their CI/CD pipeline together on Cloudflare. Now, we’re bringing a better developer experience to make it simple.
A CI/CD pipeline — commonly orchestrated with GitHub Actions — is a series of steps that run in a specific order where, if any step fails, you stop running the pipeline and report the error. In essence, a CI/CD pipeline is just a Workflow. CI/CD, when defined by a YAML file, can get complicated quickly, given the constraints that so often lead to YAML fatigue. But each step in a CI/CD pipeline can translate simply to a Workflow step.do(). Instead of YAML, you can define your CI/CD pipeline in Typescript for greater customization and configurability.
We are launching new tools in the CI SDK that allow you to run each step in your CI pipeline (e.g. build, lint, and typecheck) in a safe, isolated environment, built directly on Cloudflare’s developer platform via Workflows and the Sandbox SDK. Plus, you can now kick off a CI job directly on push instead of configuring an event subscription, a queue, and a queue consumer.
Previously, you’d have to call the Sandbox API directly and manage state yourself across different steps in the CI pipeline. The SDK allows you to run each sandboxed command in its own Workflow step, providing the retries and timeouts built into Cloudflare Workflows.
You can also speed up your CI pipeline by caching step results — for example, your install step — so that you don’t need to reinstall for all subsequent operations. Dependency caching reduces the latency of your CI/CD pipeline since every CI step won’t need to rerun the install.
To define your CI job, all you need to do is:
Define your install step for any dependencies (external packages or tools that your CI job needs), such as bundlers (e.g. esbuild), linters (e.g. eslint), or test runners (e.g. vitest).
Specify the command for each step in the CI job (e.g. bun run build, bun run test, bun run lint). With your dependencies cached, each CI step can execute in parallel, reducing the latency of the overall run.
Pass wranglerdeploy in a deploy step. Your Worker will automatically deploy when the CI pipeline passes.
Writing your own CI pipeline in a Workflow allows you to customize as much as you want. For example, you could call an agent from your CI Workflow to give your CI jobs self-healing functionality: if a step in your build errors, the agent can fix it automatically, and push a commit for your approval.
To write your own CI Workflow, get started with import { CIWorkflow } from@cloudflare/ci. Start with an install step:
Download your dependencies, including any external tools or libraries that your CI steps will need (e.g. vite, react).
Specify your lockfile, which tracks whether your dependencies have changed.
Cache your dependencies via a sandbox snapshot so that all subsequent steps have access. The snapshot will be stored in an R2 bucket on your account.
Then define steps for the build and checks, each executed in its own safe, isolated sandbox environment.
By default, each step in a Workflow starts independently, meaning the steps will execute concurrently unless otherwise specified. Running each step in parallel reduces the latency of your CI run. To ensure that all checks complete before the CI pipeline continues (for example, finish build, lint, test, and typecheck before the deploy step starts), wrap in a Promise.all():
Now, to actually trigger your CI Workflow, add an events field to your Worker’s wrangler configuration, alongside your Workflow and Artifact bindings. The events field is a new field supported within your triggers field.
You could already subscribe to Artifacts through Cloudflare Queues via event subscriptions and kick off a build pipeline every time there’s a push event. But that requires setting up the event subscription, Queue, consumer, and queue handler. Now, you can target a Workflow with that event — every time that event fires, it will trigger an instance of the Workflow.
Specify the CI Workflow as your artifact push trigger’s target to automatically trigger a Workflow instance on every cf.artifacts.repo.pushed event. Each CI run surfaces as a Workflow instance so you can view its step-by-step execution and observability directly in the Workflows dashboard. This is an Artifacts-first integration; coming soon, the types will support events from sources across your Cloudflare account to allow for programmatic consumption across the product suite.
If you want to run the CI Workflow on every repo in your namespace — for example, if you are a platform running CI on all of your customers’ repositories — omit repoName and only specify the namespace in filter.
To fully configure your CI Workflow, add bindings to each piece of the infrastructure which powers the pipeline: artifacts, workflows, containers and durable_objects (+ exports config) bindings (to access your sandboxes), plus an r2 binding if you are using cache. The R2 binding is required as the snapshot of your install step sandbox is stored in a bucket.
Self-healing CI runs
To allow your CI job to self-heal, you’ll need two pieces: the LLM and its agent harness. In the example above, we included a Think agent using Workers AI to catch errors in your pipeline and run the fixes on your behalf. Your CI job can be run and re-run remotely — no need to watch with your laptop open or check back every few minutes. Instead, Cloudflare handles it in the cloud, running your healer agent alongside the CI steps in a container. Instead of babysitting the CI job, making a manual fix, and re-running the pipeline, you’ll just need to merge the commit after your agent has made the fix.
To set up an agent that self-heals your CI pipeline, add a Durable Object binding for your Think agent:
Create your Think agent — Healer — by extending the HealingAgent class, which includes a heal method for you to call on failure. Pass whichever model you’d like to use:
Then, wrap your steps in a try/catch block where a failure triggers the healing agent:
This example demonstrates a self-healing CI pipeline, but really, the Bring Your Own Workflow model allows you to customize the CI job however you want. This can be a place to add security rules, filters, or conditional CI steps. Using the BYO-W model, platforms can configure their CI/CD pipelines across different teams, customers, or applications according to each individual use case.
The benefits of using a Workflow
By running your CI pipeline on a Cloudflare Workflow, you automatically inherit:
Resilient retries (durable execution): if any step in your CI job fails, it will automatically retry with state persisted, meaning that no progress is lost. Every step supports custom retry and timeout behavior, so you can define different failure logic for each one. Plus, you can restart from a specific step, so if just lint fails, for example, you don’t have to rerun the entire CI pipeline.
Workflows observability: inspect your CI job step-by-step in the Workflows dashboard, where each instance surfaces the steps with their inputs, outputs, and wall and CPU time.You can visualize your CI job through Workflows diagrams in the dashboard, allowing you to easily see which steps run concurrently versus sequentially. You can also inspect Workflows logs through Workers Observability and GraphQL to understand more about runs of your CI job.
The power of code: by running CI in a Workflow, you can write a step for anything you want. For example, you might want to run an AI code reviewer as part of your CI/CD pipeline. You can make a call to your code review agent — or handle any custom logic you can put into code — with Workflows step.do(). Other examples might include writing build artifacts to R2 and sending an email when CI fails, completes, or merges to main.
What’s next
A CI/CD pipeline is just a Workflow — and with the CI SDK, you can define your CI across your code, and that of your customers, in simple Typescript rather than inflexible YAML. Building off the Cloudflare Workflows primitives, you can define whatever logic you’d like, whether that’s a healing agent, like our Think example, or writing build artifacts to R2. Running CI on Workflows helps bridge the gap between storage (via Artifacts), builds, and deployments. As a platform, this allows you to easily manage each step on your own code and on behalf of your customers.
Direct integrations for Workers & Workers for Platforms: build.preview() and build.deploy() primitives to automatically deploy on push to main and create previews on push to non-default branches
Gradual deployments: manage percentage-based rollouts via Workflows to customize your deployment progression and rollback logic
Monorepos: simplified management for multi-Worker deployments using one CI pipeline
Triggers: send push events from different sources to run CI jobs on a repo from any version control system, not just Artifacts
Two years ago, we introduced Workers RPC, built on Cap’n Proto RPC. This made it possible for Workers to call other Workers and Durable Objects’ methods, return live objects and call their methods, return functions, streams and get all the benefits of a Remote Procedure Call (RPC) system, without defining schemas or adding any dependencies. We called it “JavaScript-native RPC” because it made using RPC feel native to the language.
Last year, we made this work between web browsers and servers, and introduced Cap’n Web.
Now we’re taking it cross-language.
Normally, getting programs written in different languages to talk to each other is complicated: developers usually have to build custom APIs or adopt language-agnostic serialization formats like protobuf, so the two systems can understand each other. The RPC system built into Workers is able to translate across JavaScript and Python without any additional work.
You can now call methods defined in a Python Worker from a JavaScript Worker and vice versa. You can share objects across Python and JavaScript, and call methods on a Python object from TypeScript. It all just works.
If you define a method add() in a Worker written in TypeScript:
…you can simply call it from Python:
There are no dependencies needed. All you need to configure is a Service binding:
So, what can you do with it?
This RPC system allows you to build a complex multi-language system as if you are using a library. Here are some features of cross-language RPC.
Cross-language RPC calls behave like ordinary function calls that return promises in JavaScript/TypeScript and futures in Python. Exceptions are propagated and are thrown at the call site of the RPC method.
You can pass any Structured Cloneable types as the parameters or a return value of an RPC call. These get converted to the appropriate types in Python: for example, a JS Date is converted to a Python datetime
You can pass JavaScript functions to a Python Worker and return them, and vice versa. When the other side calls the function passed to it, they make a new RPC back for you.
Typically, RPC to another Worker does not cross a network. The other Worker usually runs in the same thread as the caller. There is near-zero performance overhead compared to running code in the same Worker.
But wait, how do you convert types across languages?
The main hurdle for making RPC seamless across the JavaScript and Python Workers is bridging their distinct type systems. JavaScript developers expect to work with native JavaScript types, and Python developers expect the same for Python. Bridging two distinct languages with their own type systems required a careful, deliberate type conversion strategy.
Consider how each language handles function arguments. A typical way to define a complex function in JavaScript is passing an Object as an argument:
In contrast, a Python developer would typically define the same function using keyword arguments:
Our goal was to make cross-language RPC completely transparent. Developers should feel like they are writing code for a single-language application without needing to worry about the underlying translation layer. We achieved this by combining Pyodide’s Foreign Function Interface (FFI) with a custom type-conversion layer for Python Workers.
Pyodide FFI already translates between Python and JavaScript types
Pyodide is the CPython interpreter compiled to WebAssembly, and it has powered Python Workers from the start. It includes a robust FFI that automatically translates types between JavaScript and Python.
When a Python Worker communicates with a JavaScript Worker via Service bindings, Pyodide’s FFI transparently converts objects during the RPC call. Developers on either side don’t need to know which language the other Worker is written in, and everything is handled under the hood.
Pyodide maps native types between both environments out of the box:
When direct translation isn’t possible (such as with custom classes or functions), Pyodide creates a Proxy object. This proxy forwards attribute accesses and method calls across the boundary, enabling patterns like passing a Python function directly as a callback to JavaScript handlers.
Pyodide FFI also maps Python’s keyword arguments directly to JavaScript’s object-style parameters. For example, imagine a JavaScript Worker with a method that takes an optional options object:
When calling this JavaScript Worker from Python, you could pass a Python dictionary to represent the JavaScript object:
However, you can also use native Python keyword arguments:
Pyodide FFI translates both calls into the exact structure the JavaScript Worker expects, giving Python developers a clean, natural API experience.
While Pyodide FFI seamlessly converts standard built-in types, it doesn’t automatically understand Web API objects such as Request, Response, Blob, or File. They are commonly used in Cloudflare Workers, but there is no direct built-in equivalent in Python.
As explained in the previous section, Pyodide, by default, treats these non-standard objects as JavaScript Proxies. Rather than converting them into Python objects, it creates a passthrough proxy for attribute lookups and method calls. While functional, this approach leaks underlying JavaScript implementation details into Python. Python developers would have to constantly remember they are interacting with JavaScript proxies, adding unnecessary mental overhead.
To fix this, we introduced the workers-runtime-sdk Python package. This acts as a thin conversion layer built specifically to handle custom Workers types over RPC. When you deploy a Python Worker using uv run pywrangler deploy, this package is included by default. In fact, if you import from the workers namespace, you’re already using it:
Behind the scenes, this SDK wraps the RPC stubs provided by the bindings. It intercepts objects crossing the language boundary and translates them into native forms that both JavaScript and Python Workers can work with naturally.
As a result, Python developers can work with familiar, idiomatic Python objects, making cross-language execution feel completely invisible.
Use Python packages from your JavaScript Worker
Have you ever wanted to use a great Python package, but your app is written in JavaScript? You can do this with Python Workers. Let’s look at an example.
Pygments is a popular syntax highlighting package, written in Python. To use it from JavaScript, you just need to expose a method from a Python Worker that calls the Pygments package.
We can call this method in our JavaScript by accessing the request’s env:
Now on the Python side, we define a Python Worker with this method like so:
Now all that’s left is to write the necessary code to do the highlighting in Python. A simplified version of this looks like so:
The JavaScript lives in its own Worker that is separate from the Python Worker. So you also need to define the Service bindings to ensure they can communicate. You can do so by putting this in the JavaScript Worker’s wrangler.jsonc file:
The name of the service needs to match the name of your Python Worker here.
To test these, you can run npx wrangler dev in the JavaScript Worker’s directory and uv run pywrangler dev in the Python Worker’s directory in two separate terminals.
A full example is available on GitHub. You can run it directly by using the following commands:
Try it now
In addition to those above, there are far more examples and information about RPC in our documentation.
Workers AI runs inference for some of the best open models in the world on GPUs in Cloudflare data centers close to your users. Two of the most capable, and most demanding, are Moonshot's Kimi K-series and Z.ai's GLM. They are large, long-context, mixture-of-experts models, and they are wonderful to use. They are also very hard to serve efficiently because of memory constraints.
We've written before about how we serve large models on Workers AI and about separating the prefill and decode phases of inference to get more out of each GPU. This post looks at three techniques we layer on top of that to fit these models into memory and keep them fast: quantizing the KV cache, compressing the model weights, and, because both of those pack more requests onto shared hardware, protecting the cache those requests share. These optimizations enable us to support more customers at lower costs, with no change in model accuracy.
All our experiments and production traffic are running and benchmarked with SGLang, an open-source inference serving framework. We found that SGLang offers the best performance in the market, and we work closely with the SGLang team to upstream patches and new features to make our work available to the open-source community.
Quantizing the KV cache
As a model generates text, it stores the attention keys (K) and values (V) for every token it has already processed in a structure called the KV cache. The cache is what lets the model extend a long conversation without re-reading the entire context on every new token. For a long-context model, it grows quickly, and it is usually the KV cache, not the model's weights, that fills up GPU memory first.
By default, the cache is stored in 16-bit precision (BF16). We store it in 8-bit floating point instead (FP8, e4m3), which halves its size. On Kimi K2.6, that raises the amount of context we can hold in memory from roughly 686,000 tokens to about 1.37 million, twice as much.
It's worth being precise about where the benefit comes from, because it isn't raw speed. Quantizing the cache adds a small amount of work per token, since the FP8 attention kernel has to convert values as it reads them. What it changes is how many requests we can keep resident at once. The following measurements are for Kimi K2.6 decoding on a disaggregated H200 deployment, comparing the attention kernels directly:
At any single concurrency level, BF16 is a few percent faster per token. But BF16 runs out of cache at 32 concurrent requests and can't admit a 33rd, while FP8 keeps going to 64 and reaches 2,192 tokens per second, about 41% higher than BF16's peak, for roughly 30% less cost per token. Because we run prefill and decode as separate pools, we can apply this where it helps most: prefill is compute-bound rather than memory-bound, so there we leave the cache in BF16 and keep its slightly higher throughput.
None of this would matter if it changed the model's answers, so we checked. Across our evaluation suite, FP8 and BF16 caches are indistinguishable:
Compressing the model weights
The KV cache is one demand on GPU memory; the model's weights are the other. For GLM 5.2, we compress the weights from 8-bit floating point down to 4-bit integers (INT4) with no loss in accuracy. The checkpoint shrinks from 705 GB to 421 GB, about 40%, and per-GPU memory across an 8-way tensor-parallel deployment drops from roughly 88 GB to 52 GB, which leaves room for around 1.18 million tokens of KV cache on the same hardware.
Across our evaluation suite, INT4 and FP8 weights are indistinguishable:
Smaller weights make the decode phase faster, and for a clear reason: generating each token means streaming the model's weights out of GPU memory, so decode speed is limited by memory bandwidth. Move less data and every token arrives sooner. The effect is largest at low concurrency, where per-request latency matters most:
Prefill behaves differently. It is compute-bound, and INT4 weights have to be expanded back out before the model can multiply with them, so that extra step makes prefill slower rather than faster, GLM sustains about 10,160 tokens per second of prefill in FP8 versus 8,660 in INT4. As with the KV cache, the disaggregated design turns this into a choice rather than a compromise: we run INT4 for decode, where it wins, and FP8 for prefill, where it wins. Model accuracy stays within 0.8 points of the FP8 model across every benchmark we run, making its quality indistinguishable.
Protecting a shared KV cache
Both techniques above have the same effect: they let many more requests share one GPU's memory at the same time. That efficiency is the whole point, but it also means hundreds of requests are reading and writing pages of the same physical KV cache. The mechanisms that make this fast, paged attention, continuous batching, cache reuse, all rely on getting the bookkeeping exactly right, and at our request volumes, even a one-in-a-billion mistake would show up regularly.
So we built KV cache integrity checking as a layer of defense. The idea is straightforward: every physical cache page gets a tag that changes whenever the page is reallocated, and the server records which pages and tags each request expects to use. Before supported decode operations read from the cache, those mappings are checked. If anything doesn't match, the affected request is aborted rather than allowed to return data from the wrong page.
The question that decides whether a safety check ships is what it costs. We measured it on a mid-sized production model in a two-prefill, two-decode configuration, with 8,192-token inputs and 1,000-token outputs:
The cost is under 1% on both throughput and tail latency, and even the upper bound of the 95% confidence interval stays near 1%. We kept it computationally cheap by running the validation as a separate batch check rather than fusing it into the attention kernel, which would have introduced a race between GPU thread groups. It's enabled per deployment, and the default path uses a no-op tracker with no measurable overhead, so deployments that don't need it pay nothing.
What's next
Serving frontier models efficiently is a moving target, and this is the ongoing work behind it. We're expanding FP8 KV caches across more of the fleet, validating NVFP4 weights on Blackwell (NVIDIA’s GPU architecture), and working toward making integrity checks something we can leave on everywhere at negligible cost. These optimizations will allow us to continue to support more customers at a lower cost and at the same accuracy.
If squeezing the best open models onto GPUs and serving them to millions of developers sounds like your kind of problem, come work with us.
Agents Week is about the shift already underway: agents write code, deploy Workers, and provision infrastructure on your behalf. That shift changes what you need to see. If a program is spending money in your Cloudflare account, you need to know what it's spending; throughout the day, per product, in a shape another program can consume. The dashboard is the right answer for humans. It's not the right answer for automation.
So we're launching a new Billable Usage API for self-serve accounts: a single endpoint that returns your account's usage and cost, broken down by product and by service period. It covers every usage-based Cloudflare product on the account, including Workers, R2, D1, Workers AI, Vectorize, Images, and Stream, all with one call. And if you already work in a FinOps toolchain, the column names should look familiar.
You'll get back an HTTP 200 OK with Content-Type: application/json and the usage rows in the response body. Today, usage and cost data are updated daily while we work towards providing more real time data.
What comes back
Each row in the response is one charge period for one product on your account.
ServiceName and ServiceFamilyName — which product ("Workers Standard" under the "Workers" family, "R2 Storage" under "R2", etc.).
ChargePeriodStart / ChargePeriodEnd — the window this row covers.
PricingQuantity and ConsumedUnit — how much you used, in the unit of measure we bill on (GB-months, GB-seconds, requests, etc.).
ContractedCost — what that period cost, in BillingCurrency.
CumulatedPricingQuantity and CumulatedContractedCost — running totals for the billing period.
ZoneId / ZoneName — when the usage is attributed to a specific zone.
Most of these map directly to columns in the FinOps Open Cost and Usage Specification (FOCUS), so if your team is already ingesting FOCUS data from another provider, the names and semantics should be familiar:
Responses use the standard Cloudflare API envelope — result is an array of rows, one per product per charge period, alongside success, errors, and messages.
Where we are on FOCUS
Matching FOCUS naming was a deliberate choice. AWS, Azure, Google Cloud, Oracle, and a growing list of SaaS providers already publish FOCUS formatted exports, and every serious cost-management tool speaks to it. That said, we're not yet claiming full conformance: a handful of columns the spec requires aren't in the payload today. Getting there is on our roadmap. Consider this the first step: familiar shape now, full conformance next.
Cloudflare spend, next to the rest of your cloud spend: our partnership with Vantage
We've partnered with Vantage on a native Cloudflare integration. Vantage is an infrastructure cost management platform that ingests cost and usage data from more than 30 providers, across AI, Cloud and SaaS providers, and brings it into a single view for reporting, allocation, and optimization. With this integration, your usage flows into the same Cost Reports, Budgets, and Cost Alerts you already use for the rest of your infrastructure.
Vantage connects to Cloudflare using a read-only API token with Billing Read access. Once connected, Vantage pulls your Billable Usage data daily and breaks it down by product (such as Workers and R2), zone, and account, so you can see which products drive your spend and attribute it to the teams and services behind it.
A few of the workflows this integration supports:
Cross-provider allocation. Group Cloudflare spend by product, zone, and account, then use Virtual Tags to allocate by team or product line alongside your AWS, Azure, and other provider costs, all in a single report.
Anomaly detection. Vantage Cost Alerts monitor every connected provider and notify you via Slack or E-Mail when spend deviates from its baseline, so a change in Workers or R2 spend surfaces the same way it does for any other provider.
FinOps agents and MCP. Ask the in-console Vantage FinOps agent a question such as "What was our biggest cost driver last week across every provider?", or query the same data from Claude or ChatGPT through Vantage's hosted MCP server. Cloudflare spend is included alongside your other connected providers.
Connect your Cloudflare account in the Vantage console, and your costs appear next to everything else you run. There are no manual exports, no invoice uploads, and no separate dashboard to maintain.
This FOCUS standardized API also works with other Fintech tooling.
Why we built this
Agents do more than write code. They deploy Workers, provision R2 buckets, and manage D1 databases. When you grant programmatic access to your Cloudflare account, you need programmatic visibility into what it's costing you. Not at the end of the month, but throughout the day, by product, in a shape a program can actually consume.
The Billable Usage API is that shape. And customers have been asking us for programmatic usage for years. Finance teams want to pull spend into their own systems and attribute cost back to internal projects, teams, and even their end customers. Developers want a curl they can drop in a script. Every one of those workflows used to involve a screenshot or a manual export. Now it's an HTTP call, or a configuration in Vantage.
What's next
Finer-grained time windows. Today the API returns charge-period rows, which for most products is daily. We're looking at more real time breakdowns for the products where it makes sense.
Forecasting.CumulatedContractedCost tells you where your spend is in the current billing cycle. We want to help you predict where you're going to end up. And not just at the account level, but the product level.
Enterprise coverage. This first release is self-serve only. An equivalent experience for Enterprise contracts is in the works.
Try it
The endpoint is live today for all self-serve accounts. Grab an API token with the Billing Read permission, point your curl at it, and you'll get back your current billing period broken down by product. Full reference is available on the Cloudflare API docs. To see it alongside the rest of your cloud spend, connect your Cloudflare account in the Vantage console.
Cloudflare has spent years making it easy to run more of your stack on our network. It's time we made it just as easy to see what that's costing you — on Cloudflare, and everywhere else.
AI is changing how people interact with computers, and voice is becoming an increasingly important part of that shift. Real-time assistants, AI-powered dictation, and other voice interfaces need low-latency communication between clients, models, and supporting services. Many developers use gRPC, a Remote Procedure Call (RPC) framework built on HTTP/2 and TCP, for this infrastructure.
Ever since Workers launched in 2017, we’ve been expanding their capabilities, including adding the ability to open outbound TCP connections and a JavaScript-native RPC system built on Cap’n Proto. And so as part of Agents Week, we’re extending Workers in the other direction, supporting inbound TCP connections and adding new ways to run gRPC applications on Cloudflare.
Today, we’re announcing:
connect(socket) — a new handler in the Workers runtime that lets your Worker directly accept an inbound TCP socket provided by Spectrum (Cloudflare’s ingress proxy for non-HTTP traffic)
Full-duplex, bi-directional gRPC from Cloudflare Containers — forward the socket from your Worker to your gRPC server running in a container
Workers can serve unary and server-streaming gRPC APIs and call gRPC servers — you write your code using gRPC-web, and Cloudflare automatically converts incoming and outgoing requests to gRPC
We’re introducing this in private beta — you can sign up here.
Let’s dig into each of these below.
connect(socket) from your Worker to Durable Objects and Containers
The Workers runtime now provides a connect() handler that accepts a socket that you can read from and write to:
You can pass this socket from one Worker to another Worker, or from a Worker to a Durable Object. This lets your Worker control where an incoming TCP connection is routed:
You can pass a socket from a Durable Object to its Container:
And then handle the socket in the container:
This gives you full control over the entire path from client to your server running in a container on Cloudflare, opening the door to full-duplex communication between client and server running any program, in any language, for any TCP-based protocol.
To expose the raw TCP socket to the client, we’re introducing a new type of Spectrum application, where you specify a Worker that you want incoming TCP connections to be routed to. Spectrum is Cloudflare’s ingress proxy for non-HTTP traffic, and allows Cloudflare to sit in front of any TCP or UDP application.
Bidirectional gRPC from Cloudflare Containers
gRPC is a well-established and popular Remote Procedure Call (RPC) framework that was initially released by Google almost 10 years ago, and is now used across mobile apps, distributed systems, and most recently — voice AI applications.
Real-time voice AI applications demand low-latency, and both client and server to be able to send messages to each other over a single, persistent connection. WebSockets and Durable Objects are excellent fits for this, and the Cloudflare Agents SDK provides @cloudflare/voice to make this easy. But there is a ton of software out there that uses gRPC for real-time client-server communication.
Using the APIs described above, you can now deploy gRPC servers to Cloudflare, written in any language, with full support for bidirectional streaming between client and server. This lets you take advantage of Cloudflare’s network of 330+ locations and handle requests much closer to clients than is possible elsewhere. We’re excited about the doors this opens up for low-latency voice and colocated inference.
For example, here’s a minimal gRPC server that echoes messages it receives back to the client:
With this, there’s pretty much no gRPC-based application that you can’t deploy to Cloudflare, no matter what language it’s in or dependencies it relies on. But what if you need to do something simpler, and just serve a basic gRPC server or connect from a Worker to a gRPC server running somewhere else?
Workers as gRPC servers and clients with gRPC to gRPC-web conversion — no container needed
gRPC-web is a browser-compatible version of gRPC. Web browsers don’t expose the lower-level HTTP/2 features that gRPC requires, and there is no raw TCP Socket API built into web browsers — this is why the WebSocket API exists, and why Workers have supported WebSockets since 2021.
HTTP/2 splits each request and response into small binary messages called frames. This is core to how a single HTTP/2 or HTTP/3 connection is able to multiplex — many requests can be interleaved over one connection. Each frame has a stream ID, allowing the receiver to reassemble it into the correct request or response. gRPC depends on this stream-level control for efficient streaming, cancellation, flow control, and trailers.
Web platform APIs like fetch() don’t provide this control. So how can we make it simple and easy to use gRPC from Cloudflare Workers — without clients needing to make any changes? We translate incoming gRPC to gRPC-web, and translate outgoing gRPC-web to gRPC.
We’ve actually used gRPC-web within Cloudflare’s reverse proxy since 2020, when we wrote about the Road to gRPC on the Cloudflare blog. We convert requests to HTTP/1.1 so that messages can be inspected and gRPC apps can benefit from Cloudflare’s security features, like WAF rules and Bot Management.
Now, in private beta and then rolling out to everyone, we’re extending this so that given a Protocol Buffer (protobuf) definition file like this:
You can write a unary gRPC server in a Worker in just a few lines of code, using the @connectrpc/connect open-source package:
You can make outbound requests to external gRPC servers this way too, by using the client built into @connectrpc/connect:
Your code uses gRPC-web, but when it speaks to the outside world, it is automatically translated into gRPC. This means that clients and servers that you already depend on don’t need to change. For example, you can:
Provide gRPC backends to mobile apps that speak gRPC — Many mobile apps already use gRPC to reduce network payloads, serialize data more efficiently, and generate strongly-typed client libraries. You can now build the backend server for mobile apps on Workers, while still using established gRPC native libraries like grpc-swift-2 and grpc-kotlin.
Put a Worker in front of an existing gRPC backend — So many developers already put Workers in front of existing REST APIs to move performance critical work closer to the user, or to incrementally move state into Durable Objects. Now you can do this with existing gRPC backends as well, or build new APIs and services that fetch data from your existing gRPC backend.
What’s next for Socket Workers and gRPC on Cloudflare
We’re introducing everything from this post in private beta — you can sign up here.
At Cloudflare, we use Cap’n Proto and Cap’n Web and the JavaScript-native RPC system that is built into Cloudflare Workers instead of gRPC. And when we ship things, we always aim to be using them ourselves. So in this case, we want to first work closely with a smaller set of developers using gRPC, and make sure we’ve nailed it before turning this on for everyone.
More broadly, we’re excited to continue to push the bounds of what types of traffic the Workers platform can serve, going beyond TCP and into UDP-based protocols. Keep telling us what you want to build on Workers, and we’ll keep pushing the bounds of what is possible.
Last year, we enabled Media over QUIC (MoQ) on every Cloudflare server and opened the network for anyone to test. It provided a global MoQ endpoint, but not the isolation and access controls needed to run an application.
Today, we’re adding those isolation and access controls. The new MoQ provisioning API lets you create an isolated relay for your application and issue separate credentials for publishers and subscribers. The relays you create are available across Cloudflare’s network within seconds, with no servers to deploy, size, or load balance. Cloudflare now supports the draft-14 and draft-16 versions of the MoQ Transport protocol with authentication support.
You can create relays through the API and the Cloudflare dashboard. They are completely free to use during beta.
A QUIC recap on MoQ
MoQ (originally short for Media over QUIC) is a new open protocol under development at the Internet Engineering Task Force (IETF), the standards body that also standardized HTTP, TLS, and QUIC. It is being developed in the open and will become a free public standard (an RFC) that anyone can implement. No single company owns it.
MoQ is a publish/subscribe system. A publisher sends out streams of data that have names, and subscribers ask for those streams by name. Between them sit relays, which are just CDN servers that copy each stream to everyone who wants it. A relay never has to look inside the data it forwards, so one publisher can reach a large audience without handling the fan-out itself.
Because relays don't care what's in the data, the same protocol can carry many things that each used to need a separate system: live video, video calls, low-latency messaging, and more. It runs on QUIC, the transport under HTTP/3, which is what keeps latency low.
The practical result is that you don't have to build and run your own fleet of specialized servers. You publish to a CDN through one simple API and get both low latency and large scale for much less cost.
How we got here: the MoQ open preview
Last year, we launched the first global MoQ relay network: every Cloudflare server in over 330 cities became a MoQ relay, free and open to anyone. Because these endpoints required no authentication, they were ideal for protocol testing and client development. More than 1,000 unique clients still connect each day to test against them.
But an unauthenticated relay isn't suitable for production, because you can't control who publishes and who subscribes. That rules out any application that needs confidentiality, access control, or a clear split between publisher and subscriber roles. Take a live auction site, where bids have to reach bidders in milliseconds. MoQ is a good fit, but publishers and subscribers need different permissions, so that a viewer's credentials can't be used to hijack the publisher's tracks.
What is a relay on the Cloudflare MoQ Network?
In most MoQ deployments today, a relay is a dedicated server or a dedicated process on a shared server. Scaling this architecture means running more instances, assigning clients to them, and adding load balancers as demand changes. This is not how any Cloudflare service works, including our Realtime SFU WebRTC service.
Provisioning a relay doesn’t start a virtual machine, container, or dedicated process. Instead, it creates an isolated scope across the existing global network.
That scope separates your namespaces, tracks, and objects from those belonging to other relays. It also defines who can enter the scope and whether they can publish or subscribe. Clients connect to the Anycast endpoint, and Cloudflare handles routing them across the network.
If you’re familiar with web hosting, creating a Cloudflare relay is more like adding a virtual host than starting a new web server. Since the infrastructure is already running, the provisioning API adds your application’s configuration and credentials. This makes the relay available immediately without choosing regions, estimating capacity, or setting up a load balancer.
The control plane API for MoQ at Cloudflare
The provisioning API is a control plane: it manages relays and the tokens used to reach them, and it never touches the media that flows through them.
There are two kinds of resources.
A relay is the isolated scope from the previous section, so one application's streams never mix with another's.
A token is a credential that grants a set of operations (publish, subscribe, or both) on a single relay. Handing publishers and subscribers different tokens is what stops a viewer from taking over a broadcaster's tracks.
Each token is scoped to the operations a client needs, can be given an expiration, and can be revoked on its own. That lets you grant exactly the access a client should have, and take it back later without disrupting anyone else.
For now, each token applies to an entire relay and permits publishing, subscribing, or both. We're working in the IETF and the wider MoQ community on a richer scheme that works for everyone. If you have opinions, tell us at [email protected].
Creating a relay takes a single API call and only needs a name:
Cloudflare returns a relay ID and the two default tokens: The first token can publish and subscribe, and the second can only subscribe.
To give a client narrower access, add more tokens. This one is a subscribe-only token for viewers that expires at the start of 2027:
In the Cloudflare dashboard
You can also create a relay in the dashboard:
Go to Media > Realtime > MoQ Relay. Select Create relay, give it a name, and then confirm.
Connect a publisher and a subscriber
You can create and manage tokens through the API or dashboard, just as you can the relay itself. Give your broadcaster the publish-and-subscribe token and your viewers the subscribe-only token. Each client sends its token when it opens a MoQ session, and the relay enforces what that token is allowed to do.
The token travels in the URL path. For example, with the open-source moq-rs tools, a broadcaster can publish a fragmented MP4 stream from ffmpeg:
A viewer connects with moq-sub:
The relay reads the token when the session opens and checks whether the requested operation is allowed.
What we changed to support draft-16
The provisioning API is only one part of what’s new. The MoQ transport itself is advancing fast, and Cloudflare now supports draft-16 of the IETF MoQ spec in its relays. This draft adds two features relevant to publishing and subscribing.
PUBLISH now lets a publisher send a track to a relay before a viewer requests it. Without PUBLISH, the first subscription must travel through the relay chain to the publisher before the publisher starts sending. With PUBLISH, the relay can already be receiving the track when the first viewer connects.
SUBSCRIBE_NAMESPACE lets a subscriber request every track announced under a namespace instead of requesting tracks individually. The subscription also covers tracks added later, such as a new video rendition or audio track introduced during a live stream.
You can now connect a draft-16 client to use both features.
Built in the open
MoQ is an open standard, developed at the IETF by engineers across the industry. This lets clients and relays implement a common protocol. That interoperability is less useful if every relay provider requires a different control plane for creating scopes and issuing credentials.
In that vein, we’re documenting the design behind this API in the MoQ CDN Provisioning Internet-Draft. The draft calls the provisioned resource a scope rather than a relay, but both terms refer to the same logical delivery context: a boundary that applications create and then enter with a credential.
The goal is for multiple CDN and relay implementations to support a common provisioning model. The document is still an Internet-Draft, not an RFC, and its API model may change as the working group develops it.
Available today, still free in beta
The MoQ relay provisioning API is available now, as part of the MoQ beta. It's free to use at any scale during this preview period.
The API will change as we develop it, so we recommend checking the developer docs for updates and breaking changes.
We’d also love to hear what you want next. Finer-grained permissions? Bring-your-own signing keys? Let us know at [email protected].
As of June 23, 2026, cdnjs, one of the Internet's busiest open-source CDNs, is running exclusively on Cloudflare’s Developer Platform. Along the way, cdnjs surfaced limits in the platform, and the platform grew to meet them.
cdnjs is a free, open-source content delivery network for JavaScript and CSS libraries. Instead of using a bundler or self-hosting jQuery, Bootstrap, or Lodash, you drop a <script> tag pointing to cdnjs.cloudflare.com and the library loads from Cloudflare's edge, instantly, anywhere in the world, with no signup, no API keys, and no rate limits. It's the infrastructure behind a significant portion of “intro to JavaScript” tutorials, CodePen demos, and Stack Overflow answers.
Community-driven, cdnjs is used on roughly 12% of all websites, a 48.3% share of the JavaScript CDN market. It serves an average of 108,000 requests per second, 9 billion per day, across more than 330 Cloudflare data centers, with a 98.6% cache hit rate. Pretty cool, Internet!
In 2011, when bundlers were exotic, npm was barely a year old, and "just drop a <script> tag" was how the web shipped JavaScript, Ryan Kirkman and Thomas Davis built cdnjs as a free, community-run mirror of every popular open-source library.
Cloudflare stepped in to host it free of charge months later, and took over project maintenance in 2019. Back then, Cloudflare didn't have a mature Developer Platform that could fully sustain the entire cdnjs ecosystem. Fifteen years and a lot of building blocks later, the platform is mature enough to run cdnjs end to end, on Workers, Workflows, D1, Queues, Workers Cache, R2, KV, and Containers.
Why cdnjs has 9 billion requests a day
The web has changed beyond recognition from those days. We have ES Modules (ESM), the standardized import / export syntax browsers understand natively. We have import maps, Vite, Bun, Turbopack. We have AI assistants that scaffold entire apps in seconds. Bundlers are everywhere. So why does a CDN for <script> tags still serve 9 billion requests a day?
One reason: LLMs love cdnjs. When ChatGPT, Claude, or Cursor scaffold a quick HTML demo, they reach for cdnjs because their training data is full of it. There have been 15 years of blog posts, GitHub READMEs, tutorial sites, and Q&A threads pointing to cdnjs.cloudflare.com. The URL pattern is consistent and versions are immutable — exactly the kind of dependency a model can produce reliably without hallucinating.
Every file on cdnjs has an SRI hash (we're still working on ensuring all the existing stored hashes match reality due to bugs in the old system), mirrors are auditable, and the whole project is open source. In a world increasingly worried about supply-chain attacks, an immutable, hash-verified mirror of well-known libraries is indispensable.
And it's free, forever, for everyone. No API keys. No rate limits. No "sign up to continue." That's a rare thing on today's Internet, and it's worth protecting.
Why we migrated
We didn't migrate because cdnjs was slow. We migrated because we want to keep improving it.
The previous architecture served users well: 98% cache hit, billions of requests, no outages. But internally, shipping anything new or fixing existing issues in how packages were processed was getting harder. Making a change meant coordinating deployments across GCP Functions, a VM, and Cloudflare. Observability was painful too.
The pain points
In 2020, we migrated cdnjs to serverless, moving file serving onto Cloudflare Workers and KV, with a bare-metal origin as fallback. That change dramatically improved resilience and scalability, and let us pre-compress every asset with Brotli and gzip for smaller, faster responses, but only on the serving side.
The publishing side — the pipeline that watches npm and GitHub for new library versions, downloads them, processes them, and writes the results so cdnjs can serve them — stayed on Google Cloud Platform (GCP). At the time, Cloudflare Workers were designed for fast, short-lived HTTP requests; they didn't yet have the building blocks for a long-running, multi-step pipeline that fetches large tarballs, runs CPU-heavy compression, and orchestrates work over hours. Workflows, Queues, Durable Objects, R2, and Containers didn't exist yet.
So we built the publishing bot on what was available: a chain of GCP Functions, a VM running git-sync, and a GitHub repository as the source of truth. It worked, but six years later, that architecture was showing its age. Here's a diagram of the previous architecture:
The architecture had five pain points. The one that hurt most was observability: debugging meant stitching logs together by hand. We'll start there.
No shared trace A single package update could pass through Cloud Functions, Google Cloud Storage (GCS) object events, Pub/Sub topics, a git-sync VM, and Workers KV before a file reached a user. None of those systems shared a correlation ID. GCP Logging held one half of the story, Cloudflare Logpush held the other, and the two had no common key to join on.
The problem wasn't outright failure, it was partial success. A version that processed cleanly, wrote to KV, and then silently failed to land in the GitHub repo would serve fine for weeks until someone noticed the two stores had diverged. There was no alert for that. There couldn't be, because nothing in the system knew the full pipeline state.
Split-brain storage Files lived in two places at once: Workers KV at the edge (with a bare-metal origin as fallback) and a GitHub repository as the source of truth. The ingestion pipeline wrote to both at the end of every run. Neither was authoritative, and when they drifted, there was no clean way to reconcile them.
Pipeline glued together with object events The ingestion pipeline was a chain of small GCP Cloud Functions, each doing one step and handing off to the next through shared storage. One function fetched the package's release archive from npm and dropped it in a bucket. The bucket firing a "new file" event triggered the next function, which unpacked it and wrote the results somewhere else, triggering the next, and so on. Storage was doing double duty as a message queue, with no dead-letter queue, no backlog visibility, and no clean replay when a step failed.
26 functions for 26 letters Just checking npm for updates required 26 Cloud Functions, one per letter of the alphabet. Each shard had its own deployment and its own logs, and the only way to know if the fleet was healthy was to check all 26.
The GitHub repo GitHub couldn't serve A separate VM ran git-sync, mirroring every processed file into cdnjs/cdnjs. Years of releases pushed it past 1.1TB of packed storage, large enough that GitHub's own archive service refused to generate tarballs or zip downloads for it. Forking became impractical, clones were slow, and the .gitignore had grown to 274 hand-curated entries blocking broken or weirdly-versioned releases. It was a documented graveyard of everything the pipeline couldn't reasonably reject upstream.
A genuine thank you to the GitHub team for hosting this giant for over a decade. They bore with us through years of storage growth, and the project wouldn't have survived without them.
A quieter benefit that came with the migration is having fewer moving parts to secure. Cloud Functions, a git-sync VM, container images, GCS buckets, service-account keys — every one of those was a thing to secure, patch, and audit. Retiring the pipeline closed all of the recently opened cdnjs vulnerabilities.
How we re-built it
The new cdnjs architecture runs entirely on Cloudflare’s Developer Platform.
R2 is the single source of truth for file content. It has no practical size limit, so the files that couldn't fit in KV before, like source maps, big bundles, and font packs, now live alongside everything else. As a bonus, the S3 API makes the entire cdnjs catalog accessible to any S3 client. Maintain a mirror? Open an issue on the cdnjs repository and we'll set you up with read-only credentials.
KV stores only metadata now: package info, version lists, SRI hashes. KV is built for high read volume with infrequent writes, which is exactly the shape of metadata access.
In front of the Worker sits Workers Cache, a tiered cache Cloudflare launched this year. Before, we relied on a separate internal caching layer between the edge and the Worker. That layer is gone now, replaced by one owned by the Developer Platform, the same platform that runs the rest of cdnjs. One less moving part!
The new architecture also extends a long-standing partnership. DigitalOcean has hosted the cdnjs website for years as a sponsor; now it hosts the storage too. Every file published to R2 is mirrored to DigitalOcean Spaces: architecturally a disaster-recovery copy, operationally also a live fallback. The serving worker reads through to it whenever R2 can’t return a file. The chain is cache → R2 → DigitalOcean, so R2 having a bad day doesn't take cdnjs down. A Cloudflare-hosted origin still sits in the chain during the transition, but it will retire once the GitHub backfill lands in R2.
The ingestion pipeline is built on Cloudflare Workflows. Every ten minutes, a cron job triggers PackageUpdatesWorkflow, which checks npm and GitHub for new versions. For each new version found, it spawns a DownloadPackageWorkflow that fetches the tarball into R2, then a ProcessingWorkflow per file that extracts, minifies, and compresses. Finally, PublishingWorkflow writes the results to R2 and KV and updates the Algolia search index.
Because Workflows provides durable execution, the state of each step is preserved. If anything fails — a network timeout, a compression error — the workflow resumes from the last successful step.
The trickier piece is how we glue Workflows to the external compression container. We pre-compress text-based files to streamline the delivery process. But compression is too CPU-intensive for a Worker, so we hand it off to Cloudflare Containers, wait for compression to complete, and then pick up where we left off.
The pipeline has two kinds of waiting:
Per file: Each ProcessingWorkflow writes the uncompressed file to an R2 bucket, sends a job to a Queue, and hibernates. A Rust compression service running in the container picks it up, compresses it, and writes the result to another bucket. An R2 event notification wakes the workflow up so it can continue.
Per package: The parent workflow needs to wait for all its file children before moving on to publish. A package with thousands of files means thousands of children running in parallel. We use a small Durable Object as a counter: a parent increments on each child it spawns, children decrement when they finish. The parent wakes up when the counter reaches zero.
An overview of the new architecture, with R2 as the source of truth and Workflows running the pipeline:
Pushing the limits
Designing the new architecture was one challenge. Migrating the existing catalog into it, without disturbing a single file already in the wild, was another.
We'd actually tried this once before and had to roll back. The plan, back then, was to re-process old packages and write the results directly to R2, but the regenerated files didn't byte-match what KV had been serving. Minifiers and compressors aren't fully deterministic across versions, so the new outputs were correct but had different SRI hashes. For a CDN where users pin those hashes in their HTML, that's a serving break. So we rolled back, and now, we migrated the existing content from KV to R2 as-is instead of regenerating it.
That decision shifted the problem from "re-process millions of files" to "copy millions of files between accounts, without missing any." And that's where we ran into the Workers subrequest limit, capped at 1,000 per invocation on paid plans. A package with thousands of files would burn through it in one go. Parallelizing didn't help, since every Worker hits the same ceiling. So we sharded the migration by package name and fanned the work out across many invocations via Queues, whose at-least-once delivery guarantee meant no package could silently fall out of the migration.
We hit two platform limits during the migration: 1,000 subrequests per Worker invocation and 1,024 steps per Workflow. Instead of just working around them, we asked the Workers and Workflows teams to raise them — which they did. Subrequests now go up to 10 million on paid plans; Workflows now default to 10,000 steps, configurable to 25,000.
The cdnjs pipeline runs on the same building blocks anyone can use: Workers, Workflows, R2, KV, Queues, Containers, and Durable Objects. The limits we hit are limits we lifted for everyone. If the Cloudflare Developer Platform can serve 9 billion requests a day and publish packages with hundreds of thousands of compressed, minified files, it can probably run whatever you're building.
What’s next
There's an obvious next question hiding in all of this: could cdnjs also serve modern, browser-native ES modules? The same packages, transformed on publish, ready to import without a bundler. The architecture doesn't rule it out. The Workflows-plus-Containers pattern that pre-compresses files today would work just as well for transforming them. We're not committing to it, but it's the kind of thing that's now possible to consider, which wasn't true a year ago.
git commit -m "with love" --author="cdnjs team"
We follow every open issue on GitHub and we want your feedback. Don't hesitate to contribute and help make the Internet better for everyone.
R2 gives developers object storage, without the egress fees. Before R2, cloud providers taught us to expect a data transfer tax every time we actually used the data we stored with them. Who stores data with the goal of never reading it? No one. Yet, every time you read data, the egress tax is applied. R2 gives developers the ability to access data freely, breaking the ecosystem lock-in that has long tied the hands of application builders.
In May 2022, we launched R2 into open beta. In just four short months we’ve been overwhelmed with over 12k developers (and rapidly growing) getting started with R2. Those developers came to us with a wide range of use cases from podcast applications to video platforms to ecommerce websites, and users like Vecteezy who was spending six figures in egress fees. We’ve learned quickly, gotten great feedback, and today we’re excited to announce R2 is now generally available.
We wouldn’t ask you to bet on tech we weren’t willing to bet on ourselves. While in open beta, we spent time moving our own products to R2. One such example, Cloudflare Images, proudly serving thousands of customers in production, is now powered by R2.
What can you expect from R2?
S3 Compatibility
R2 gives developers a familiar interface for object storage, the S3 API. With S3 Compatibility, you can easily migrate your applications and start taking advantage of what R2 has to offer right out of the gate.
Let’s take a look at some basic data operations in javascript. To try this out on your own, you’ll need to generate an Access Key.
Regardless of the language, the S3 API offers familiarity. We have examples in Go, Java, PHP, and Ruby.
Region: Automatic
We don’t want to live in a world where developers are spending time looking into a crystal ball and predicting where application traffic might come from. Choosing a region as the first step in application development forces optimization decisions long before the first users show up.
While S3 compatibility requires you to specify a region, the only region we support is ‘auto’. Today, R2 automatically selects a bucket location in the closest available region to the create bucket request. If I create a bucket from my home in Austin, that bucket will live in the closest available R2 region to Austin.
In the future, R2 will use data access patterns to automatically optimize where data is stored for the best user experience.
Cloudflare Workers Integration
The Workers platform offers developers powerful compute across Cloudflare’s network. When you deploy on Workers, your code is deployed to Cloudflare’s more than 275 locations across the globe, automatically. When paired with R2, Workers allows developers to add custom logic around their data without any performance overhead. Workers is built on isolates and not containers, and as a result you don’t have to deal with lengthy cold starts.
Let’s try creating a simple REST API for an R2 bucket! First, create your bucket and then add an R2 binding to your worker.
Through this Workers API, we can add all sorts of useful logic to the hot path of a R2 request.
Presigned URLs
Sometimes you’ll want to give your users permissions to specific objects in R2 without requiring them to jump through hoops. Through pre-signed URLs you can delegate your permissions to your users for any unique combination of object and action. Mint a pre-signed URL to let a user upload a file or share a file without giving access to the entire bucket.
Presigned URLs make it easy for developers to build applications that let end users safely access R2 directly.
Public buckets
Enabling public access for a R2 bucket allows you to expose that bucket to unauthenticated requests. While doing so on its own is of limited use, when those buckets are linked to a domain under your account on Cloudflare you can enable other Cloudflare features such as Access, Cache and bot management seamlessly on top of your data in R2.
Bottom line: public buckets help to bridge the gap between domain oriented Cloudflare features and the buckets you have in R2.
But before you’re ready to start paying for R2, we allow you to get up and running at absolutely no cost. The included usage is as follows:
10 GB-months of stored data
1,000,000 Class A operations, per month
10,000,000 Class B operations, per month
What’s next?
Making R2 generally available is just the beginning of our object storage journey. We’re excited to share what we plan to build next.
Object Lifecycles
In the future R2 will allow developers to set policies on objects. For example, setting a policy that deletes an object 60 days after it was last accessed. Object Lifecycles pushes object management down to the object store.
Jurisdictional Restrictions
While we don’t have plans to support regions explicitly, we know that data locality is important for a good deal of compliance use cases. Jurisdictional restrictions will allow developers to set a jurisdiction like the ‘EU’ that would prevent data from leaving the jurisdiction.
Live Migration without Downtime
For large datasets, migrations are live and ongoing, as it takes time to move data over. Cache reserve is an easy way to quickly migrate your assets into a managed R2 instance to reduce your egress costs at the touch of a button. In the future, we'll be extending this mechanism so that you can migrate any of your existing S3 object storage buckets to R2.
We invite everyone to sign up and get started with R2 today. Join the growing community of developers building on Cloudflare. If you have any feedback or questions, find us on our Discord server here! We can’t wait to see what you build.
Today we are launching Workers Cache: a tiered cache that sits in front of your Worker, configured by a single line of Wrangler config and the same Cache-Control headers you already know.
When Workers Cache is enabled, every cacheable 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.
And when content changes, your Worker purges its own cache:
await ctx.cache.purge({ tags: ["product:123"] });
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 workers.dev, behind a service binding, in a preview, in a Workers for Platforms tenant. One Worker, one cache, configured once.
That’s the surface area. There’s a lot underneath: tiered caching across our entire network, full support for stale-while-revalidate so stale responses never block a user, content negotiation via Vary, multi-tenant-safe cache keys via ctx.props, 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.
Workers Cache is available today to every Worker on any plan, enabled in Wrangler.
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.
Why server-rendered apps need a cache in front
When we introduced Workers in 2017, the pitch was that you could run code on Cloudflare’s network to transform requests on their way to your origin. The Worker sat in front of the cache and the origin:
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.
But the world changed. Workers stopped being a thing you bolted onto an origin and started being the origin. Frameworks like Astro, TanStack Start, Next.js, Remix, and SvelteKit all ship a Cloudflare adapter that builds your app as a Worker. There’s no origin behind them. The Worker is the server.
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 Workers runtime is fast enough that this works — 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.
Workers Cache flips the architecture. Cloudflare’s cache now sits in front of the Worker:
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.
This is what was missing for server-side rendering on Workers. You used to have to choose between two unsatisfying options:
Prerender everything at build time (“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.
Render every page on every request. Up-to-date content, but every page load pays the rendering cost and every visitor pays the latency.
Workers Cache gives you a third option: server-render on demand, cache the rendered response, refresh it on a time-to-live (TTL) 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 stale-while-revalidate, even that one doesn’t wait.
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.
stale-while-revalidate is the part that makes it feel instant
The stale-while-revalidate directive tells Cloudflare that when a cached response expires, it’s allowed to serve the stale copy immediately while it refreshes the response in the background. Cloudflare shipped full support for stale-while-revalidate earlier this year, and it’s the directive that turns “we cache your Worker” into “your Worker’s site feels static.”
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 Cf-Cache-Status: UPDATING 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.
In practice, this looks like:
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",
},
});
},
};
The mental model that makes this click:
Fresh window (max-age): Cloudflare serves the cached response. Your Worker doesn’t run.
Stale window (stale-while-revalidate): Cloudflare serves the cached response. Your Worker runs in the background to refresh it. No user waits.
Outside both windows: Cloudflare runs your Worker to generate a fresh response, and the user waits for that one render.
You pick the windows. For a product catalog that updates every few minutes, max-age=300, stale-while-revalidate=3600 means visitors basically never wait, and your Worker still runs often enough to keep content fresh. For a blog archive that almost never changes, max-age=86400, stale-while-revalidate=2592000 means your Worker runs once a day per page.
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.
One URL, many representations: Vary works
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.
Doing this without a cache is easy — your Worker just reads the request header and returns the right thing. Doing it with 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.
Workers Cache supports the standard HTTP Vary header, which is the right way to solve this. When your Worker returns a response with Vary: Accept-Encoding (or Accept, or Accept-Language, 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.
One URL, two cached variants. A browser that sends Accept: image/webp,*/* gets the WebP. A browser that sends Accept: image/jpeg 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.
This is the well-trodden HTTP standard for content negotiation, and Workers Cache implements it the way RFC 9110 and RFC 9111 describe. There’s no allowlist of what headers you can Vary on. You list whatever you need, and Cloudflare keys variants on the verbatim values. The docs go through the edge cases — 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 (Vary: *) that disables caching entirely.
This is your Worker’s cache, not your zone’s
Before we get to what becomes possible with all this, there’s a conceptual shift worth naming.
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.
Workers Cache is different. It’s your Worker’s cache — it belongs to the Worker, not to a zone. This has a bunch of consequences that turn out to matter:
There is no zone configuration to manage. Cache Rules, cache level settings, the file-extensions list, Page Rules — none of them apply to Workers Cache. The Worker’s Cache-Control headers are the configuration.
The cache follows the Worker, not the hostname. A Worker that’s bound to api.example.com, api.example.net, and invoked over a service binding shares one cache across all three. A request to /users/42 hits the same cached entry regardless of which way in it came.
The cache works on workers.dev. It works in preview URLs (each preview gets its own cache, so testing a change doesn’t poison production). It works in Workers for Platforms (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.
Purges are scoped to the Worker’s entrypoint. When you call ctx.cache.purge({ purgeEverything: true }), 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.
What you configure about caching, you configure in code: which paths get longer TTLs (branch on the path and set a different max-age), which requests bypass the cache (return Cache-Control: private), how the cache key is shaped (control what gets into ctx.props, normalize the URL in a gateway Worker before dispatching). The Worker you already wrote is the configuration surface.
Workers Cache is regionally tiered by default. There are two layers:
A lower tier in the Cloudflare data center closest to the user. Every data center that receives traffic for your Worker has its own lower-tier cache.
An upper tier that aggregates fills across the whole network. There are fewer of these, and every lower tier consults the upper tier on a miss.
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.
The reason this matters is that the first request anywhere in the world 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.
This is the same topology that powers Tiered Cache 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.
If your Worker uses Smart Placement, 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 planning to smooth out, in the docs.
Run your app near the user and near the data
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.
We’ve spent years chasing both. Our network puts us within ~50ms of about 95% of the world’s Internet users. Smart Placement and Placement Hints 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. We knew we could do better.
Workers Cache is the piece that closes the gap. Because the cache belongs to the Worker (not the zone), and because service bindings and ctx.exports 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.
The architecture looks like this:
Worker A runs near the user. It handles the cheap, latency-sensitive parts of every request: authentication, rate limiting, routing, header normalization, rendering the outer “shell” of an HTML page that doesn’t depend on data.
Worker 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.
Workers Cache sits in front of Worker 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.
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.
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 wrangler.jsonc file, and you’re done.
Multi-tenant by default, with ctx.props
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 automatic bypass for Authorization headers does exactly that. But “don’t cache anything” gives up the entire performance win.
Workers Cache solves this by making the caller’s ctx.props part of the cache key. When one Worker calls another over a service binding and passes ctx.props 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.
import { WorkerEntrypoint } from "cloudflare:workers";
interface Props { userId: string; }
export default class Backend extends WorkerEntrypoint<Env, Props> {
async fetch(request: Request): Promise<Response> {
// 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",
},
});
}
}
The typical pattern is to authenticate the request in a gateway Worker, strip the Authorization header, set the authenticated user’s ID into ctx.props, 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 Multi-tenant safety with ctx.props and the example in Per-user authenticated responses.
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.
A cache between every Worker entrypoint
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.”
Workers Cache sits in front of every Worker entrypoint — the default export, every named WorkerEntrypoint, and every call between entrypoints in the same Worker via ctx.exports. That last clause is the one that changes what you can build.
When one entrypoint calls another via ctx.exports, 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 ctx.props. The caller still runs on every request, but anything it hands off to the callee is memoized independently.
You decide, per entrypoint, which ones cache. In your Wrangler config, the exports map lets you turn caching on or off for each entrypoint by name ("default" is the default export). Opt an entrypoint in to cache the responses it produces; opt one out 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.
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 ctx.props you pass, what Cache-Control you set.
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.
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:
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<Env, Props> {
async fetch(request: Request): Promise<Response> {
// 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<void> {
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<Response> {
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<Env>;
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 exports 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.
The patterns this composes into are open-ended. The same shape works for:
Caching a Durable Object. Wrap the Durable Object behind an entrypoint, set Cache-Control 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.
Normalizing Accept-Encoding before Vary. The outer entrypoint restores the original encoding from request.cf.clientAcceptEncoding (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.
Stripping tracking parameters before caching. The outer entrypoint canonicalizes the URL — or sets a custom cache key with cf.cacheKey on the ctx.exports call — so the cached inner entrypoint sees only the canonical form, and ?utm_source=anything collapses to a single cache entry.
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 Examples page in the docs walks through several of these end-to-end.
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, ctx.props, ctx.exports — the patterns you can build are open-ended. We’ve barely scratched the surface in this post.
First-class support in your framework
If you’re building with Astro, the Cloudflare adapter wires up Workers Cache for you. Just add the cacheCloudflare provider to your configuration:
The adapter enables the cache, sets the right headers on the responses Astro generates, attaches Cache-Tag values for invalidation, and gives you a cache.invalidate() 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.
We’re working with the maintainers of other frameworks to ship the same integration. If you build a framework adapter for Cloudflare, the Workers Cache APIs are exactly what you’d want them to be — header-driven configuration, programmatic purges, no platform-specific concepts to model.
See your cache on the same dashboard as your Worker
Caching is only useful if you can see what it’s doing. The Workers Observability dashboard now surfaces cache hit information per invocation:
You can see, per Worker:
Cache hit ratio over time. The number you want trending up after you enable caching.
Hits, misses, updates, bypasses broken down. If your hit ratio is low, this is where you find out why — too many BYPASS responses (because something is setting a cookie?), too many MISS responses (because the cache key is partitioning more than you thought?), too many UPDATING responses (because max-age is shorter than your traffic interval?).
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.
Billing
Cache hits don’t run your Worker, and they don’t bill CPU time. They do count as a request at the standard Workers request rate, the same as any other invocation. Cache misses and bypasses bill normally — request + CPU time, exactly as they would without caching.
Outcome
Request charge
CPU time charge
Cache HIT (Worker does not run)
Standard rate
Not billed
Cache MISS (Worker runs)
Standard rate
Billed
Cache BYPASS (Worker runs)
Standard rate
Billed
Static asset request
Standard rate
Not billed
Worker-to-worker invocation
Standard rate
Billed if the Worker runs
There’s no separate Workers Cache SKU and no per-GB cache storage fee. Tiered caching, purges, stale-while-revalidate, 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.
One thing to watch: when caching is enabled, requests that are normally free — static asset requests and worker-to-worker invocations through service bindings or ctx.exports — are billed at the standard request rate, because each one now consults the cache in front of your Worker.
What’s next
Things we know we want to do next:
Smarter co-location with Smart Placement. 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.
Larger response size limits. At launch, all responses follow the Free plan’s cacheable size limit (512 MB), regardless of your account. That’s temporary — the standard per-plan cache limits will apply once we finish a few rollout steps.
An API to mark cached responses stale. ctx.cache.purge() removes matching responses from cache. We’re looking at a ctx.cache.invalidate() 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.
Try it
Workers Cache is available today to every Worker on any plan.
Cloudflare Workflows allows you to build durable, multi-step applications with built-in retries and state persistence across long-running processes. When a Workflow executes, each step can call external systems, retry failures, and persist state across restarts. But if one step fails, it may leave earlier work from completed steps in an inconsistent or partial state.
Today we’re shipping saga rollbacks for Workflows, allowing you to declare rollback logic within the step itself, in case of failure.
For example, consider a workflow for transferring funds between accounts at two different banks:
Debit from account at Bank A
Credit to account at Bank B
Send email confirmation to both account owners
What happens if Step 2, the credit to account at Bank B, fails? Once the debit succeeds at Bank A, the transaction is committed and the money has left its system. As the orchestrator of the transaction, you cannot simply “undo” the operation in Bank A’s system. Instead, the money must be credited back to the account at Bank A through a new operation that semantically reverses the first one.
This pairing of an operation and its compensation logic is called the saga pattern.
Before today, developers had to implement their own compensation logic to track what succeeded, what failed, and what actions should be taken upon failure, outside of the steps’ direct definitions. Now, you can define compensation logic for each step.do() as an argument within the steps themselves, maintaining your workflow’s durability for the rollback as well.
// track what completed so we know what to undo
let debitA;
let creditB;
try {
debitA = await step.do("debit-bank-a", () => bankA.debit(from, amount));
creditB = await step.do("credit-bank-b", () => bankB.credit(to, amount));
await step.do("notify", () => notifyBoth(from, to, amount));
} catch (error) {
// unwind in reverse. each undo is its own durable step,
// must be idempotent, and must keep going if one fails.
if (creditB) {
try {
await step.do("reverse-credit-b", () => bankB.debit(to, amount, creditB.id));
} catch (e) {
await alertOnCall("reverse-credit-b failed", e);
}
}
if (debitA) {
try {
await step.do("refund-debit-a", () => bankA.credit(from, amount, debitA.id));
} catch (e) {
await alertOnCall("refund-debit-a failed", e);
}
}
throw error;
}
Without rollbacks
// each step ships with its own undo. add a step,
// add its rollback right here. no growing catch
// block, no manual ordering, no replay logic.
await step.do("debit-bank-a", () => bankA.debit(from, amount), {
rollback: async ({ output }) => bankA.credit(from, amount, output.id),
});
await step.do("credit-bank-b", () => bankB.credit(to, amount), {
rollback: async ({ output }) => bankB.debit(to, amount, output.id),
});
await step.do("notify", () => notifyBoth(from, to, amount));
With rollbacks
Try it out
To use rollbacks, just pass an options object containing a rollback function as the last argument to step.do().
const debit = await step.do(
"debit-account-a",
async () => {
return await bankA.debit({
accountId: fromAccountId,
amount,
idempotencyKey: `${transferId}:debit-account-a`,
});
},
{
rollback: async () => {
await bankA.credit({
accountId: fromAccountId,
amount,
idempotencyKey: `${transferId}:rollback-debit-account-a`,
});
},
}
);
// The idempotency keys make both the forward operations and rollback operations safe to retry without duplicating the transfer
const credit = await step.do(
"credit-account-b",
async () => {
return await bankB.credit({
accountId: toAccountId,
amount,
idempotencyKey: `${transferId}:credit-account-b`,
});
},
{
rollback: async ({ output }) => {
if (output === undefined) {
return;
}
await bankB.debit({
accountId: toAccountId,
amount,
idempotencyKey: `${transferId}:rollback-credit-account-b`,
});
},
}
);
// If we fail here, we may want to revert all previous payments. Users should not have to wrap their code in complex try-catch logic just to revert two small payments (see below)
await step.do("send-confirmation", async () => {
await sendTransferConfirmation({ ... });
});
Rollback functions should be idempotent, just like regular Workflow steps. If you refund a charge, use the payment provider’s idempotency key. If you release inventory, make the release safe to call more than once.
If any step fails, the rollback handlers will execute in reverse step-start order. It sounds simple: run the undo steps when something fails. In practice, there are a few details that make the API and execution model important.
1. The failed step may still need rollback. A failed step.do() can still be rollback-eligible if it registered a rollback handler.
The rollback will not start if user code catches an error and the Workflow continues, but if a step error is caught and the Workflow later fails for another reason, rollback can still run for previously registered handlers, which execute in reverse step-start order.
Why? The step may have partially interacted with an external system before failing. For example, a payment provider may capture a charge, but the step may fail before returning the chargeId to Workflows. That is why rollback handlers receive output, but must handle output === undefined.
2. Rollback only starts when the Workflow fails. Adding a rollback handler does not mean every step error triggers rollback. If user code catches an error and continues, the Workflow continues. Rollback starts when the Workflow itself is about to fail terminally.
When rollback starts, Workflows finds eligible step.do() calls, runs their rollback handlers, then records the final Workflow failure.
3. Ordering has to be predictable. For sequential Workflows, rollback order feels obvious:
Reserve inventory.
Charge card.
Create shipment.
If shipment fails, refund the card and release the inventory.
Parallel steps make this more subtle. Completion order can differ from start order, so Workflows uses reverse step-start order instead of reverse completion order.
The practical rules are:
Any started or completed steps with rollback handlers are eligible.
The failing step.do() is also eligible if it registered a rollback handler.
Handlers run in reverse step-start order, not completion order.
How we designed the API
Once we had the expected behavior in mind, we had to add this new pattern into the Workflows API. Rollbacks went through a few iterations before we landed on rollback options.
Why not a fluent or builder API?
The first approach was a fluent form: step.do(...).rollback(...) It reads well. The forward action and the compensation sit next to each other, and the call site looks like ordinary JavaScript chaining.
The problem is that step.do() already has an important meaning: it starts a durable step and returns a Promise for the step output. In Workers, promise-like values are especially meaningful because Workers RPC supports promise pipelining, a pattern inherited from systems like Cap’n Proto.
Promise pipelining lets code call a method on a future value before that value has fully returned to the caller. For example:
const session = api.authenticate(apiKey);
const name = await session.whoami();
Here, session is not the real session object yet. It is more like a handle to the session that will exist soon. When you call session.whoami(), Workers can send that call to the remote side early and say: “once authentication creates the session, call whoami() on it.”
That saves a round trip. The caller does not need to wait for authenticate() to fully finish before asking for whoami().
To a reader, that can look like “call .rollback() on the result of charge-card.” But rollback is not part of the step’s output. It is part of the step.do() options, registered before the step starts, so Workflows knows how to compensate the step if a later step fails.
A fluent API also makes step timing harder to reason about. Today, step.do() starts the step when it is called, so developers can start a step, do other work, and await the first step later:
With today’s execution model, first starts immediately, before second. A fluent API would complicate that. Workflows would need to wait and see whether .rollback() gets attached before it knows the full step definition. That could delay when the step is sent to the engine.
In the earlier example, first could start at await first instead of at step.do("first", ...), after second has already completed.
That makes concurrent Workflows harder to reason about: step timing would depend on when the returned Promise is consumed, not just where step.do() is called.
A builder API avoids the Promise ambiguity. It also gives us an obvious place for future step-level options, and makes it clear that the forward action and rollback action belong to the same saga step.
But it adds ceremony. Every step needs a final .run(), forgetting .run() would be easy and hard to spot without tooling, and simple one-step cases start to look like configuration chains. It also introduces a new step.saga() builder, breaking from the existing step.<action> pattern. Most importantly, it makes step.do() feel like an older API rather than the primary Workflows primitive. The goal of rollback was to extend step.do(), not replace it.
Rollback as step metadata
step.do(..., { rollback })
Ultimately, we chose the explicit form where rollback is metadata on the step.
This way, each rollback is defined within the forward step itself. Each handler receives the error that caused the rollback to start, the step context, and the output, which is either the persisted value returned by the forward step (which can be undefined) or undefined if the step failed before persisting a value.
Rollbacks emit lifecycle events, so you can tell whether compensation started, which rollback handler failed, and whether rollback completed successfully.
Crucially, the original Workflow failure remains separate: rollback is what Workflows does after the failure, not the reason the Workflow failed.
Just as you can define custom retry and timeout behavior in thestep configuration via WorkflowStepConfig, you add rollback-specific values in rollbackConfig.
This matches the lifecycle-event mental model we wanted. A step.do() already describes a durable unit of work that Workflows records, retries, and later shows in logs. Rollback is another lifecycle behavior for that same unit of work. It should travel with the step definition, not live in a separate wrapper or builder.
The step still starts when step.do() normally starts.
The returned promise still represents the step output.
Concurrent Workflow code keeps the same execution model.
Retry and timeout options for rollback live next to the rollback handler.
Existing step.do() calls keep working exactly as they do today.
This shape is slightly more explicit than the fluent API, but that explicitness is useful. The operation and its compensation are still in one place, and the API does not introduce a new step builder or a new kind of promise. Developers who already understand step.do() only need to learn one additional options object.
This is less magical, but it is simpler to adopt, and clearer to understand.
How it works under the hood
Rollback feels like a small API addition, but it changes what Workflows needs to record about each step.
A regular step.do() already has a durable record. Workflows records that the step started, whether it completed, what it returned, and whether it should be skipped instead of repeated if the Workflow resumes later.
Rollbacks add one more thing to that record: whether the step registered compensation logic.
This means Workflows has two pieces of information to bring together if the Workflow fails.
The first is durable step history. The Workflow engine stores data to know what ran, what completed, what output was saved, and whether rollback was registered.
The second is the rollback handler itself, which is the function written to compensate for that step. Workflows does not save the text of that function as data. Instead, it keeps a callable reference to the handler while the Workflow is running.
In Workers RPC, this kind of callable reference is called a stub. A stub lets one part of the system call code that is running somewhere else. Stubs also have lifetimes such that they can be disposed when a call or execution context ends. If you need to keep a stub past that point, Workers RPC provides a dup() method, which creates another handle to the same target.
For rollback, that model is useful. The durable step history records what needs compensation. The rollback stub gives Workflows a way to invoke the compensation code. And because rollback handlers may need to outlive the immediate step.do() call that registered them, Workflows keeps its own callable reference to the handler for the rollback phase.
In the common case, when a Workflow enters rollback in the same engine lifetime, Workflows already has the rollback stubs it needs. It can use the durable step history to find eligible steps, then invoke the rollback stubs that were registered during forward execution.
This gets more subtle when Workflows has to recover after a restart.
If the engine is evicted, crashes, or restarts while rollback is needed, Workflows still has the durable step history, but it may no longer have the in-memory rollback stubs. To recover, Workflows uses replay: a recovery mode where it can re-run the Workflow code without re-executing completed forward step bodies.
When replay reaches a completed step.do(), Workflows reads the persisted result instead of running the step body again. For rollback recovery, Workflows only needs to rebuild handlers for steps that had rollback attached and are eligible for rollback. As those step.do() calls are encountered, their rollback options can register the callable stubs again
That lets Workflows recover the rollback handlers it needs without duplicating the original external side effects.
With those pieces in place, rollback can work whether the handler is still available in memory or has to be rebuilt during recovery.
When the workflow is about to fail, Workflows does not ask your application to reconstruct what happened. It already has the step history. It can look at the persisted record and answer the important questions:
Which steps started?
Which steps finished?
Which failed step may still need cleanup?
Which steps registered rollback handlers?
What output should each rollback handler receive?
What order should compensation run in?
Then Workflows invokes each rollback stub with a rollback context: the original error, the step context, and the step output, if one was persisted.
The ordering detail matters. In normal JavaScript, especially with Promise.all(), completion order is not always the same as start order. If step A starts first and step B starts second, step B might finish first. For rollback, Workflows uses the persisted start order as the stable source of truth, then unwinds it in reverse.
Rollback handlers also run through Workflows’ normal step machinery. That means compensation gets the same operational properties you expect from Workflows: retries, timeouts, lifecycle events, logs, and a final recorded outcome. If a rollback handler keeps failing after its configured retries, Workflows records the rollback outcome as failed, stops running the remaining rollback handlers, and the Workflow instance ultimately ends in the Errored state.
This is the main difference between saga rollbacks and a catch block. A catch block only knows what is still in memory at its exact point in your JavaScript execution. Workflows rollback uses persisted step history to decide what already happened, invokes the stubs it already has in the common case, and safely rebuilds missing stubs during recovery when it needs to.
That is also why the API puts rollback on step.do() itself. Rollback is not a separate global error handler — it is metadata attached to the durable unit of work Workflows already understands.
When a multi-step application fails halfway through, the hardest part is often not knowing that it failed. It is knowing what already happened, and what needs to happen next.
Saga rollbacks let you put that answer directly beside each step. If you are building multi-step applications with Workflows, try saga rollbacks and tell us what compensation patterns you want next. Get started with the Workflows documentation and share feedback in the Cloudflare Community.
Cloudflare provides services that help run 20% of the web, but we don’t do it alone. Developers on our platform use a myriad of tools and services from other companies too. Cloudflare provides a rich API for our platform that enables developers to create automations, CI/CD, and integrations that glue together the various parts of their infrastructure. Earlier this month, we announced self-managed OAuth, making it easier for customers to create and manage their own OAuth clients for delegated access to the Cloudflare API.
Cloudflare isn’t new to OAuth. If you’ve used Wrangler, or used integrations from partners like PlanetScale, then you’ve already used it. However, until now, third-party OAuth was only available through a small number of manually onboarded integrations, and was not available to developers more broadly. That meant developers building their own integrations had to rely on API tokens, which are harder to manage and a poor fit for many delegated application flows.
Over the last year, we onboarded a growing number of early partners while improving the consent, revocation, and security model behind Cloudflare OAuth. But as our Developer Platform grew and agentic tools drove demand for delegated access, it became clear that opening up OAuth to all customers was critical to the success of our platform.
With self-managed OAuth, developers can now offer a standard OAuth flow where customers grant scoped access directly, making it easier to build SaaS integrations, internal developer platforms, and agentic tools while giving users clearer consent, easier revocation, and more control over what an application can do.
Scaling the ecosystem securely
While our earlier OAuth solution was sufficient for a small number of carefully managed partners, we realized that our permissions model, our consent experience, and our ways of mitigating potential abuse vectors were not mature enough.
Earlier this year we updated our consent experience to make it clearer which application is requesting access, and what permissions it will receive. We also added revocation to the dashboard so developers can easily control which applications have access to their data, and made app ownership more visible to prevent OAuth phishing attacks.
Opening self-managed OAuth to all customers also required major upgrades to our underlying OAuth engine. This process required a large amount of planning to do with minimal user interruption, while also ensuring data stability and security.
Planning the upgrade to our OAuth engine
Years ago, we deployed Hydra, an open-source OAuth engine, to power Cloudflare OAuth under the hood. That deployment served us well when usage was limited, but as the developer platform grew and agentic workflows became more common, it became clear that we needed a major upgrade to unlock new capabilities and improve performance.
As we planned the upgrade, we decided to do two smaller sequential upgrades rather than doing one large upgrade. First, we would move to the latest 1.X release, evaluate any behavior or performance changes, and then proceed with the 2.X upgrade.
During our upgrade planning, it became clear that even the 1.X upgrade wouldstill impact customers because the Hydra database required extensive schema migrations that:
Created indexes in a manner that would claim an exclusive lock on critical tables, preventing active users from performing important OAuth operations
Added columns to critical tables, and moved other columns to new tables
There was also a quirk in the version of Hydra we were using in which the SDK would perform SELECT * operations, causing deserialization issues with the schema changes.
To prevent user impact, we rewrote the SQL migrations to use features such as CREATE INDEX CONCURRENTLY, and built a custom version of Hydra which selected explicit columns rather than SELECT *.
With the latest 1.X upgrade planned out, we now needed to create a plan for the even larger 2.X upgrade. We identified three potential options, and weighed the benefits and drawbacks of each one. Doing an in-place upgrade was not going to work for us, due to the sheer amount of schema changes the major version bump brought with it. We decided that a blue-green strategy would work, but there was more that needed to be done than simply flipping a switch to start using the new version. The upgrade and migration process would take multiple hours, and we needed the system to continue functioning correctly in that time window.
The first blue-green option would involve disabling writes to the database, preventing any new authorizations from occurring. This means they would not be lost in the transition, but it also meant that nobody would be able to use existing OAuth apps unless they already had a valid credential. It also presented another large problem: if users needed to revoke access from an application for any reason, it would not be possible while the upgrade was being performed.
To combat these issues, we came up with a way to leave writes to the database enabled, at the cost of losing some of them in the switch to the green version. The first thing to solve was minimizing the number of writes for new tokens. There was an operational lever we pulled: increasing the expiry time of tokens to multiple hours. This would allow apps that received new tokens before the upgrade to continue using them without needing to refresh.
With reducing writes solved, we needed to come up with a way to not lose any revocations our users performed during the upgrade window. To do this, we created a queue system (using Cloudflare Queues!) which, after a revocation event, would have a record written into the queue with information about that revocation. This would allow us to drain the queue with the database flipped to the green version, replaying all revocation events that took place in the time window in which they would have been lost. This was critical to get right, otherwise applications that users had revoked would inadvertently have their access restored.
Executing the upgrade
Upgrading to 1.X
From an operational point of view, our first upgrade to the last 1.X release went off without any hitches. Our custom database migrations ran faster than we expected, with no user impact. We had to do a hard cutover to the new version because the old version was unable to introspect tokens that were created by the newer version.
After the cutover, we saw an increase in refresh token errors that we had not seen before. This ended up being due to stricter refresh invalidation behaviors in the new version; if a refresh token was reused, Hydra would invalidate the whole access and refresh token chain. This is problematic for Wrangler and MCP clients. These clients both have a high request volume, and a single reused refresh token would invalidate the entire session.
We mitigated this by adding refresh token coalescing behavior to our Worker which routes OAuth traffic to the correct destination. This allowed us to briefly cache the refresh token request before it reached Hydra, so that if we detected a retry we could short-circuit the request and respond without invalidating the tokens. Fortunately, 2.X versions of Hydra have a configurable “refresh token grace period”, which resolves this by allowing a refresh token to be retried for a period of time without invalidating the whole chain.
Upgrading to 2.X
Since multiple hours of high user-facing impact would not be acceptable, we had our blue-green upgrade strategy set. At a high level, this sounds simple; the migrations would run on a copy of our production database, and then cut over along with the new Hydra version after they complete. In reality, there were a lot more moving parts:
Enable revocation replay capture queue
Copy and restore our database to the new target
Targeted data cleanup — existing data violated some new constraints introduced in the newer versions, which could prevent migrations from succeeding
Perform cutovers on the Hydra service along with two additional critical internal systems simultaneously to prevent any errors
Post-cutover monitoring and validation
We chose an upgrade window when Hydra had the lowest request volume per second to minimize lost token writes. Other than some timeout tuning, our production migrations ran well against the new database: the net runtime in production was approximately three hours. After the migrations completed, we carefully rolled out the new version of the Hydra service, along with two additional system configs to flip our systems to use the new SDK version.
Shortly after cutting traffic over, we observed that a data cleanup job in our authorization service (which relies on the Hydra consent session API) was being overeager in its purging of OAuth policy data. After investigation, we discovered that there was an issue in one of the Hydra migrations that corrupted the state of certain valid OAuth sessions, which resulted in the migration marking them as invalid. The valid sessions being corrupted caused a disagreement between Hydra and our authorization service, manifesting as an increase in 403s. To mitigate this, we did data restorations and began work on improvements for OAuth authorization behaviors to remove reliance on static policy data.
Beyond the data cleanup issue, there were some additional small fixes more driven by specific client behaviors which we landed quickly.
With the Hydra version upgrade complete, OAuth traffic has remained stable with improved system performance and reliability for our customers. It also brought production onto the same foundation our newer OAuth APIs had already been validated against in staging, clearing the way for our self-managed OAuth release on June 3.
Performance improvements
After completing a large upgrade like this, it is always rewarding and illuminating to look at some broad metrics about the impact. We gathered additional metrics during the database migrations, and observed considerable performance improvements after the upgrade was complete.
Database
Metric
Approx. Value
Rows updated
132.5M
Rows inserted
114.7M
Temp bytes
136.97GB
Transaction commits
22.2k
Hydra performance
Metric (avg)
Before
After
Change
API P95
185ms
101ms
-45%
RSS memory
888MB
763MB
-14%
Go heap alloc
449MB
271MB
-40%
Goroutines
4015
3076
-23%
CPU
1.07 cores
0.67 cores
-37%
Self-managed OAuth for all
Opening up OAuth to all customers is an important step toward a broader Cloudflare app ecosystem. Today, any Cloudflare customer can create their own OAuth applications and build integrations on top of Cloudflare. We’re extremely excited to launch Cloudflare self-managed OAuth for all.
To get started, take a look at our documentation or jump straight to the OAuth apps page in the dashboard and create your first OAuth app.
The Images service, built in Rust on Workers, runs on every machine in Cloudflare’s edge network. To handle client connections, we use hyper, an open-source HTTP library for Rust.
Last year, we introduced the Images binding to enable custom, programmatic workflows for processing remote images in Workers. At the end of 2025, we rearchitected the binding to provide a more direct, local connection between the Workers runtime and the Images service.
Shortly after rollout, we received reports that transformation requests from the binding were failing — but only intermittently and only for larger images. Even stranger, the responses for these requests returned a 200 status without any errors logged. The image data was simply cut short: A response that should have been two megabytes might arrive with a few hundred kilobytes instead.
We spent six weeks chasing a nearly invisible bug — a race condition that occurred only under specific conditions — in the hyper library that impacted how the Images binding returned processed image data back to the client. In the end, it took four lines of code to fix it.
Hops, handoffs, and hyper
When developers build on Cloudflare, they compose full-stack applications from a set of platform services that are accessible to Workers through bindings. Bindings provide direct APIs to resources on the Developer Platform like compute, storage, AI inference, and media processing.
The Images binding decouples image optimization from delivery; you can transcode, composite, or manipulate images without needing to return the output as an HTTP response. It also lets you apply optimization parameters in any order, rather than following the fixed sequence imposed by the URL interface. Here, a worker can pass image data directly to the Images API, chain operations together, and get the processed result back as a stream:
At a high level, this is how image data moves through our various services:
The pipe represents a socket connection between the intermediary and Images, where data is handed off from one process to the next through the kernel’s buffer.
The binding communicates with Images through a socket connection managed by the Workers runtime. A socket connection is a communication channel between two processes. Each end of the socket has buffers that are managed by the operating system’s kernel; these buffers are temporary holding areas where data sits after one side writes it but before the other side reads it.
Hyper manages the connection on the Images service’s side, reading incoming requests from the socket and writing responses back to it.
When a request uses the Images binding, the Images service reads the input, performs the requested optimization operations, and encodes the result. It then passes the entire encoded image to hyper as a single in-memory block.
Hyper writes this response data into its own internal buffer. At this point, hyper considers the encoding work as complete, since it has all the bytes that it needs to send. The next step is to flush its internal buffer to the socket’s outbound buffer, moving the data from the Images service to the intermediary on the other end.
If the reader on the other end is fast, then hyper can flush everything in one pass — the outbound buffer will have room because the reader is consuming data as quickly as it arrives. Once all data is sent, hyper issues a shutdown on the socket, signaling that the connection is finished and no more data will be written. But if the reader is slower (even by a few milliseconds), then the outbound buffer fills up, and hyper needs to wait until there’s room to continue writing.
Taking the local
All incoming traffic on Cloudflare’s network passes through FL, an internal intermediary service that runs security and performance features and routes requests to the appropriate backend. When we first launched the binding, image data flowed from the Workers runtime, through FL, to the Images service.
This path was a natural fit for our initial release and follows the same architecture as our URL interface. Over time, though, this coupling with FL became a constraint: Every change to the binding had to follow FL’s release cycle.
In December 2025, the Images team replaced FL with a new intermediary service, an internal worker binding that runs on the same machine. In the original architecture, data moved through FL over network sockets; this path carried the overhead of FL’s full processing pipeline, such as DNS lookups and routing.
The internal binding replaced these with Unix sockets to directly connect the services on the same machine, bypassing FL and the overhead of the network stack. This made the request path to Images faster and gave the team independent control over binding releases.
Within days of the rollout, we received our first customer report.
200 OK (not OK)
The first sign of trouble came from a customer with a non-standard setup: two layers of image processing, where one pipeline was nested inside another.
First, their worker used the Images binding to composite multiple large source images from R2 — a JPEG background plus PNG overlay layers — into a single combined JPEG. Second, they further compressed, transcoded, and resized the result through the URL interface.
The bug originated in the inner pipeline’s return path, where the response was truncated before reaching the outer pipeline.
The inner pipeline (transformation binding) handled compositing. The outer pipeline (transformation URL) handled delivery optimizations like scaling and format conversion. This layered approach meant that when the inner pipeline silently returned a truncated response, the only visible error appeared one level up:
error reading a body from connection: end of file before message length reached
The outer pipeline received HTTP 200 from the inner one, with a Content-Length header that promised several megabytes. The actual body was only a fraction of that: In one request, only ~200 KB arrived out of an expected 3.3 MB. The error surfaced in the outer pipeline, but the truncation could have originated in the binding, the intermediary service, the Images service, or somewhere in between.
When a browser receives a truncated image, the result is visible. Depending on the format, the image either renders partially (e.g., with the bottom half missing or gray) or fails to decode entirely, instead displaying a broken image.
Debugging in the dark
From here, we worked inward through the request path, testing each layer to isolate where the truncation was happening. Some of these efforts hit dead ends; others left breadcrumbs that narrowed the search:
Building a reproduction. We built a worker that mimicked the customer’s nested setup, then stripped away layers until we could trigger the bug with the binding alone. A small script let us fire requests in batches. In one early run, 19 out of 25 requests failed. The amount of data that did arrive — roughly 200 KB — was suspiciously close to the size of the socket buffer in production. This confirmed that the problem wasn’t tied to the customer’s configuration and gave us a reliable way to trigger the bug on demand.
Investigating timeouts. Early on, we suspected the truncation might be related to timeout behavior (i.e., the connection was being closed after a time limit). This theory didn’t hold, as the truncation wasn’t correlated with request duration.
Updating hyper version. When the bug was first reported, we were running 0.14.x, while the latest hyper version was around 1.8.x. We tested across hyper versions 0.14, 1.7, and 1.8, just in case the most obvious answer was the correct (and easiest) one. But the bug appeared in each version, which meant that there wasn’t an upstream fix.
Reproducing locally. We ran local integration tests on macOS and a Debian VM. Even under considerable load, our local requests never triggered any failure. Making direct curl requests to the binding socket and replaying captured requests always seemed to work. The bug only appeared on the full production path when there was real concurrency and a real Workers runtime client on the other end of the socket. This led us to suspect the runtime itself.
Ruling out the Workers runtime. We examined the HTTP client that the Workers runtime uses to communicate with Images through the binding socket. None of the traces from either side of the connection showed any syscalls that indicated an unexpected close or early termination. We observed that the client behaved correctly and multiple other services used the same client without issues.
Distributed tracing. By inspecting request traces end-to-end, we confirmed that the truncated body was already present before it reached the outer transformation layer in the customer’s setup. That narrowed the problem to the inner pipeline — the binding path through the Images service.
Instrumenting the intermediary service. We added instrumentation to the intermediary service to measure body sizes before forwarding the response data. The bodies were already truncated by the time they left the Images service, so the intermediary was ruled out.
Deeper tracing within the Images service. At the service level, the request was processed, the image was properly encoded, and the response was sent with HTTP 200.
The only consistent signal was that the bug was timing-dependent: It appeared only on the production path, with real concurrency, and only for larger images.
A kernel of truth
Tools for application-level debugging told only what the system thought it was doing. But according to the system, everything was fine: Tracing said the response was sent; logging reported no errors, and the Images service returned 200 on every request.
To see what the system was actually doing, we attached strace to the Images service. strace records the syscalls that a process makes to the kernel, which could show us exactly which bytes were written, when a shutdown was called, and whether the client sent any termination signal.
Setting up the trace was delicate. strace works by intercepting syscalls as they happen, which adds a small amount of timing overhead to each one. Filtering for a narrow set of syscalls kept that overhead minimal. Broadening the filter, however, slowed the process just enough to shift the timing between the flush and the shutdown check — and make the bug disappear entirely. That alone reinforced our theory that the issue was timing-sensitive.
Using a reproduction worker, we triggered the bug and compared the syscall output between successful and failing requests.
In a successful request, the response is written in chunks as the socket buffer allows, with shutdown called only after all the data is sent. For example, this may look like:
Here, there is only one write — just enough for the headers and a sliver of the body — before the shutdown is immediately called. Out of a 14.9 MB response, only about 219 KB was sent. The remaining ~14.8 MB of image data never left hyper’s internal buffer, nor was there any termination signal from the client between the write and the shutdown. Instead, the Images service prematurely shut down the connection on its own, genuinely believing it was finished.
The failing requests confirmed that the bug was a race condition that triggered intermittently. Whether a request succeeded or failed depended on whether the flush and shutdown operations overlapped, which changed from request to request. When the buffer was still full at the exact moment that hyper decided the connection was finished, data was lost.
When the reader consumes slower than hyper writes, the outbound buffer fills up. If hyper shuts down the connection before the buffer drains, then only a fraction of the response makes it to the intermediary; this incomplete data gets forwarded back to the Workers runtime and the client.
The December rearchitecture didn’t introduce this bug, which had been present in hyper for years across multiple major versions. But the new intermediary changed who was reading on the response side of the socket. Our working theory is that FL, the previous intermediary, consumed data fast enough that the socket buffer rarely filled during a response. The new reader read at a pace that occasionally let the buffer fill during larger responses.
These few milliseconds of backpressure, introduced by an improvement that made everything else faster, were all it took to surface a flaw that had been hiding in plain sight.
Inside the dispatch loop
Hyper’s HTTP/1 connection lifecycle is driven by a state machine in a file called dispatch.rs. It runs a loop that reads requests, writes responses, flushes the write buffer to the socket, and decides when to shut down. In simplified form:
fn poll_loop(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
loop {
let _ = self.poll_read(cx)?;
let _ = self.poll_write(cx)?;
let _ = self.poll_flush(cx)?;
if !self.conn.wants_read_again() {
return Poll::Ready(Ok(()));
}
}
}
More precisely, the let _ before poll_flush is where the bug lives.
In Rust, let _ = expr discards the expression’s result, including Poll::Pending, the signal that the flush isn’t done yet. The flush might still have megabytes sitting in its buffer, but the loop never finds out.
When a request fails, this is the exact sequence of events:
The Images service finishes encoding the image and hands the entire response to hyper as a single in-memory block.
Hyper writes the block into its internal buffer and marks its write state as Writing::Closed. From an encoding standpoint, the work is done — there is nothing left to encode.
Hyper calls poll_flush to move the buffered data to the socket. In our previous example, the socket accepted about 219 KB. The remaining ~14.8 MB stays in hyper’s buffer. The socket is full, so the kernel returns Poll::Pending.
poll_loop discards the Poll::Pending with let _.
It checks wants_read_again(). The full request was already received, so this returns false.
poll_loop returns Poll::Ready(Ok(())), signaling that the loop is finished, even though the flush is not.
poll_shutdown() fires. The SHUT_WR syscall is issued.
The client receives 219 KB and an EOF (end-of-file) indicating that the connection is closed, even though it expects 14.9 MB.
In the second step, hyper marks the write operation as complete as soon as the response body is buffered (i.e., when encoding is finished), rather than when it has actually been flushed. Most of the time, the flush completes in a single pass and this distinction is invisible. On the rare occasions when the socket buffer is full, the flush has to wait — even though hyper doesn’t. The bytes are still sitting in hyper’s buffer, waiting to be flushed to the socket. Hyper proceeds to shut down the connection with this data still in the buffer.
This also explains why curl never triggered the bug. Curl reads data as fast as it arrives: The socket buffer never fills, the flush always completes immediately, and the discarded return value is harmless. The production path, with a reader that occasionally paused for a few milliseconds, was the only configuration where the buffer filled at exactly the wrong moment.
Don’t forget to flush
After weeks of investigation, the fix itself was conceptually simple. Hyper needed to check whether the flush was actually done before moving on.
Our reproduction worker confirmed that the bug existed, but it couldn’t tell us why a given request failed. Before writing the fix, we needed a test that could trigger the exact socket conditions inside hyper.
We knew the conditions that triggered the bug: a socket that accepts one chunk of data and then blocks. To test with a controlled scenario, we built a custom wrapper around a TCP stream that simulated a full socket buffer. The wrapper accepted 8 KB on the first write, then returned Poll::Pending on every subsequent write, mimicking a reader that stopped draining the buffer.
The test sent a 500 KB response through this constrained socket and checked whether hyper called shutdown while 492 KB was still buffered. Without a fix, it did. With the fix, it waited.
Initially, we applied the fix in hyper’s dispatch loop. Instead of discarding the result of poll_flush, we checked to see whether the flush was actually done:
let flush_result = self.poll_flush(cx)?;
if flush_result.is_pending() {
return Poll::Pending;
}
if !self.conn.wants_read_again() {
return Poll::Ready(Ok(()));
}
If the flush hasn’t completed, then the loop returns Poll::Pending to the asynchronous runtime. The runtime waits for the socket to become writable, then wakes the task back up to continue the flush. The connection shuts down only after all data has been sent.
When we deployed this fix, we observed that every byte was written and the shutdown was called only after the buffer was actually empty. The customer who made the first report also confirmed that the issue disappeared.
While our initial solution worked, the dispatch loop wasn’t the right place for the fix. Returning Poll::Pending early could slow down other operations on the same connection by reducing how frequently reads are polled, causing unintended backpressure. It also doesn’t correctly handle keepalive connections, where a single connection handles multiple requests in sequence — these should remain reusable even while the previous response is still being flushed. Neither issue affected our particular service (where keepalive is disabled), but both could affect other hyper users if the fix were contributed upstream.
We traced through hyper’s connection lifecycle and found a more targeted approach. Rather than changing how the dispatch loop behaves, we applied the fix at the point where shutdown is actually called. Before shutting down the socket, hyper should first flush any remaining data in its buffer:
This leaves the dispatch loop unchanged. It adds a flush only at the exact point where data loss would otherwise occur — the moment before shutdown.
What stayed with us
None of the tools at the application level surfaced any errors, crashes, or log entries that provided useful clues. Application-level observability can have a blind spot for bugs that live below its awareness.
The failure occurred intermittently, scaled with response size, couldn’t be reproduced with simple tools like curl, and disappeared when we observed the system more closely. These signals pointed to a timing-dependent bug in the connection layer, not in the application logic.
Our breakthrough came from using kernel-level tooling with strace, the one layer that records what actually happened on the socket. The underlying bug lived in the few milliseconds between a partial flush and a premature shutdown — a window that opened only after we made the system faster.
We merged our fix and the deterministic test into hyperium/hyper via PR #4018. It will be available in a future hyper release, ensuring that any service using hyper’s HTTP/1 implementation won’t lose response data to the same race condition.
In the meantime, we’re running an internal fork with the patch applied. This fix stabilized the binding’s architecture, creating a reliable foundation to expand its functionality.
The Images binding initially covered only transformations of remote images. Earlier this month, we announced that the Images binding now supports operations for hosted images, giving developers a unified way to build media-rich applications on Cloudflare.
2026 is the year agent harnesses go to production. The software that controls the model’s access to the outside world — harnesses like Codex, Claude Code, OpenCode, Pi, and Project Think — has matured to the point where teams are deploying agents as real, load-bearing infrastructure, not just prototypes.
But building agents that survive production is hard.
We learned this firsthand building Project Think as our first-party agent harness. In working with our customers to run agents in production, we found a common set of distributed systems problems that every agent faces when running in the cloud. When an agent is interrupted, how can it automatically and gracefully resume from where it left off, without losing context or wasting tokens? How can agents run untrusted code securely? How can agents use the tools they were trained for?
A harness can’t solve these problems on its own. They’re tied to state, storage and compute — which means they’re dependent on the platform the agent runs on. That’s why we’re taking our learnings from hardening Project Think for production and bringing them to the Cloudflare Agents SDK as a base layer. Durable execution, dynamic code execution, a durable filesystem and dynamic workflows, now available to any harness building on Agents SDK.
At the same time, a new layer has emerged above the harness. Frameworks like Flue wrap a harness with the project structures, conventions, integrations and developer experience that make agents productive to build.
To solve these scaling challenges, there’s a new, three-layer stack that is emerging for building production-grade AI. Here is how the pieces fit together, moving from the user-facing developer experience down to the underlying platform primitives:
The framework (Flue) — the project structure, the conventions, the integrations, the CLI and the developer experience for building agents.
The harness(Pi, Project Think) — the agentic loop that calls tools, reads results, manages context and keeps going until the task is done.
The runtime/platform(the Cloudflare Agents SDK) — the compute, state, and storage primitives everything above depends on
The Agents SDK is that bottom layer: it makes primitives like durable execution available to any harness and any framework. Flue, our new open-source framework from the team behind Astro, is the first to build on it. Here’s how.
Flue
Flue shipped 1.0 Beta this week, built on the Pi harness, the same harness that OpenClaw is built on. What makes it different as an agent framework is the approach: you don’t script what your agent does, you describe what it knows. Define the context an agent needs — its model, skills, sandbox, and instructions — and it solves whatever task you give it, autonomously. There’s no orchestration loop to write.
This declarative model is what makes writing agents easy: here’s a triage agent that intercepts a bug report, reproduces it in a sandbox, and diagnoses the issue in under 25 lines.
The Flue developer experience
Flue’s power comes from the fact that agents don’t live in isolation. They are built to exist where your users already work, and integrate with your preferred tooling:
Anywhere agents: Drop your agents into Slack, GitHub, Linear, or Discord with pre-configured Channels that handle event verification and dispatch boilerplate automatically.
Headless, but UI-ready: Agents shouldn’t live in a black box. Flue agents can run completely headlessly for background tasks, but @flue/react provides native frontend hooks that stream an agent’s state, tool execution, and live messages straight into your frontend application, without you having to build custom real-time plumbing from scratch.
Ecosystem-ready: Flue makes it easy to add and upgrade integrations with commands like flue add channel slack, generating a Markdown blueprint that your own coding agent can read, modify, and cleanly integrate straight into your codebase.
Designed for production, not just prototypes
Moving an agent out of a local terminal and into a production ecosystem introduces traditional distributed systems failures. Host crashes, API timeouts from LLM providers, and unexpected restarts threaten to erase the short-term memory of a running agent turn.
Flue solves this via Durable Streams. Each event in the execution history is added to an append-only log. By processing every prompt, tool response and model choice as an unchangeable ledger, an agent’s state is never volatile. If a process dies, another simply picks up the log and continues from the exact step it left off.
Deploy anywhere, including Cloudflare
Flue is a multi-cloud framework. On Node.js, each agent runs as a long-lived process. You can deploy it to any VM or container, run it in GitHub Actions, or embed it on an existing server. But when you target Cloudflare, each agent becomes a Durable Object.
By running each Flue agent inside its own Durable Object, Cloudflare can automatically scale to as many agents as you need, each with their own isolated storage and compute. You don’t have to provision servers, manage sticky sessions, or worry about noisy neighbors. And when Flue agents are deployed to Cloudflare, they get durable execution using Agents SDK’s runFiber(), stash(), and onFiberRecovered() methods. Flue also uses @cloudflare/codemode and @cloudflare/shell for sandboxed code execution against a durable workspace.
What harnesses need out of an agentic platform
Flue’s Cloudflare target works so effectively because it maps cleanly to the core primitives we built into the Agents SDK. You can even dig into the Flue source code to understand how Pi, the underlying harness, is adapted to work on Cloudflare Agents SDK.
Here’s how Flue leverages the Agents SDK under the hood, and what it takes to run any modern agent harness reliably at scale.
Every agent harness needs durable execution
An agent turn is not a single request. The model streams tokens, calls tools, waits for results, maybe asks a human for approval, or delegates work to a subagent. That sequence can take seconds or minutes, and at any point the process can be interrupted or crash. When that happens, all of the agent state that was in memory is gone: the streaming connection, the pending tool calls, where the agent was in its turn. Sure, the conversation history is persisted on disk, but the user sees a spinner that never resolves. That’s a broken user experience.
Fibers solve this problem by providing a native checkpointing mechanism directly inside the Agent’s underlying Durable Object. runFiber() records the progress to the Durable Object’s SQLite storage before the work in the Agent turn starts and checkpoints with stash() as the turn advances. When a fresh agent instance boots after an interruption, onFiberRecovered() delivers the last checkpoint, so your agent knows a turn was interrupted, where it got to, and can decide how to continue.
Flue uses runFiber()on its Cloudflare target for exactly this. With the onFiberRecovered() hook, your harness can decide how to resume the execution of the turn, whether it attempts a full reconstruction model like Project Think that repairs turn state or whether it replays certain parts of the turn.
Executing code is better than overloading agents with tools
Agent harnesses give models access to the outside world through tools. But tool surfaces grow fast, and models get worse at selecting the right tool as the list gets longer and the context window fills up with tool definitions. A better pattern: give the model one tool that executes code. The model writes a TypeScript function that calls the APIs it needs, and the harness runs it. We wrote about this when we introduced Code Mode.
The question is where that code runs. To run LLM-generated code securely, you need a sandbox. But typical sandboxes would be slow, cost-prohibitive and inefficient to run each tool call. That’s why the Agents SDK provides @cloudflare/codemode, which wraps Dynamic Workers, to execute LLM-generated code in its own Worker isolate with only the bindings you provide.
Code Mode creates a fresh Dynamic Worker for each snippet, runs it, and discards it. Isolates start in under 10ms and $0.002 per load, resulting in drastically faster and cheaper cost of execution than booting a container every time your agent needs to execute a short piece of code. Flue uses @cloudflare/codemode on its Cloudflare target to power its code tool. The agent writes JavaScript against the workspace and runs it with Code Mode.
You don’t need a full container for most workspace tasks
Agent harnesses often need a filesystem, whether it’s to read files, write outputs, search through code and understand diffs. Coding agents in particular live in the filesystem. But if the harness is running in a serverless environment, how can it get a durable filesystem that persists across executions?
The usual answer is a container. That works, but it’s expensive for what agents mostly do. The majority of filesystem operations in an agent turn are text. Consider a review agent that reads files, greps through source code, or perhaps writes a patch. You don’t need a full Linux boot for that.
@cloudflare/shell gives your agent a durable virtual filesystem inside its Durable Object, backed by SQLite. It provides typed file operations — read, write, edit, search, grep, diff — that agent harnesses can use as tools.
Instead of calling individual tools, a Flue agent running on the Cloudflare target writes JavaScript against the workspace virtual file state API. By running more operations within the Durable Object, the agent benefits from the isolate model’s more efficient execution process, entirely avoiding container overhead:
async () => {
const files = await state.glob("src/**/*.ts");
const results = [];
for (const file of files) {
const content = await state.readFile(file);
const todos = content.match(/\/\/ TODO:.*/g);
if (todos) results.push({ file, todos });
}
return results;
}
This translates into a faster and more cost-efficient sandbox environment for agents that need to run shell and filesystem operations to get their work done. And for agents that need a full OS, to run npm install, git, or compilers, Cloudflare Containers provides that. We’re also building @cloudflare/workspace, to keep the virtual file system of a given Durable Object in sync with a container’s, allowing for seamless transition from lightweight Workers to a Linux environment only when it needs one.
Dynamic Workflows: let agents write their own workflows to repeat tasks consistently
But what happens when an agent needs to do more than read files or execute single code snippets? What happens when it needs to orchestrate a massive, multi-step pipeline that must repeat consistently over time, like a code review that successfully resolves bugs or a research workflow that produces good results? A harness can’t provide durable multi-step execution on its own. It needs the platform to persist each step, retry failures, and resume after interruptions.
This pattern is gaining traction. Claude Code recently shipped dynamic workflows, where Claude writes a JavaScript script at runtime to hand off work to dozens of subagents, and the runtime executes it durably. @cloudflare/dynamic-workflows provides this for any harness running on the Agents SDK. Your agent generates a workflow at runtime, and the Workflows engine persists each step, retries failures, and can sleep for hours or wait for external events like human approval.
From the Agent class, runWorkflow() connects your agent to the Workflows engine. The agent kicks off the workflow and can go to sleep. The workflow calls back into the agent via RPC to report progress, update state, or request approval. When the workflow finishes, the agent wakes up with the result.
Direct access to the Cloudflare ecosystem
Beyond compute and storage, agent harnesses need access to external capabilities: web browsing, email, memory, search, inference. A harness shouldn’t have to integrate each of these separately, manage API keys for each, or worry about credentials leaking through agent-generated code.
The Agent class gives your harness access to the rest of Cloudflare through bindings: AI Gateway for per-agent spend tracking and limits, Browser Run for web automation, Email Service for inbox workflows, Agent Memory for persistent recall, AI Search for retrieval, Containers for workloads that need a full OS, and inference across 14+ model providers. Bindings grant capabilities without exposing credentials: your agent uses them, but the keys never enter agent-generated code.
Bring your agents to the agentic cloud
We know this approach works because it is the exact architectural foundation we used to build Project Think, our first-party agent harness. While Project Think remains our highly optimized, out-of-the-box solution for native Cloudflare agent experiences, the Agents SDK ensures that the broader open-source ecosystem can leverage those exact same battle-tested primitives, including Flue.
If you’re building agents today with Flue, you can deploy in just a few clicks to Cloudflare. And if you’re building your own agent harness or you’re building an agent framework, target the Agents SDK and get the platform integration for free.
There isn’t a CIO on the planet not worried about AI spend right now. CFOs are increasingly nervous, too.
For fear of falling behind, many companies have pushed their employees to use AI as aggressively as possible. The edict was clear: “Move fast, we’ll figure out the bill later.” And for the most part, it worked: AI has been genuinely transformational for the teams that leaned in.
But the costs are real: we’ve heard countless horror stories of huge bills and painful overages on token spend.
Today, we’re announcing spend controls in Cloudflare AI Gateway, and a closed beta for identity-driven budgets and routing using Cloudflare Access and your existing identity provider.
As we’ve spoken with hundreds of companies about their AI strategy, we’ve seen a common story: The company gives every engineer access to frontier models through a shared API key. Usage takes off. At the end of the month, finance pulls the invoice and nobody can explain where the money went. Was it the machine learning team training a new pipeline? Was it an intern running Claude Opus on email triage? Was it a runaway continuous integration job that burned through 50 million tokens in a weekend? Nobody knows, because the API key doesn’t tell you who used it.
Without guidelines, staff will generally reach for the biggest model available. And why wouldn’t they? If there’s no budget, no visibility, and no routing logic, the rational move is to use the most powerful model for everything. The problem is that most tasks don’t need a frontier model. A code review summary doesn’t need the same model as a complex architecture refactor. A log parser doesn’t need the same model as a customer-facing content generator. It should be easy to select the right tool for the job, rather than defaulting to the most powerful and expensive one. And it should be simple to see where the spend is going.
You can’t calculate ROI on your AI spend without visibility on what you’re spending, and you can’t protect that ROI without controls. Every other line item in a business has a budget and per-team attribution and AI spend should be no different.
What AI Gateway is
AI Gateway sits between your applications and AI providers. Instead of calling OpenAI, Anthropic, Google, or any other provider directly, your requests route through AI Gateway first.
This immediately gives you several useful tools:
Unified billing to easily switch between different providers and models
Logging across all providers — every request, token count, and cost in one place
However, AI Gateway didn’t have an easy way to answer who is spending what or how you might set limits on AI spend.
You could see aggregate usage across your account. But you couldn’t see that Jane from engineering burned through \$2,000 on Claude this month while the entire data science team only used \$400. You couldn’t set a budget that said “engineering gets \$5,000/month on frontier models, interns get \$200/month on Kimi K2.6.”
That changes today.
Spend limits: budgets for AI usage
AI Gateway now supports spend limits as a core feature. These are true cost control measures in the form of budgets set in dollars, not tokens, that track cumulative spend across all requests, operating independently of traditional rate limiting.
You can scope limits to any combination of dimensions: model, provider, or admin-defined custom attributes like user, team, or application. Windows can be fixed (resets on the first of the month, Monday, or midnight) or rolling, and set to daily, weekly, or monthly.
AI Gateway calculates cost per request based on the model’s pricing, and tracks cumulative spend against your limit in real time. You can easily track your model spend on our analytics dashboard and filter by model, provider, or any custom attribute.
You have options for what happens when the budget limit is reached. AI Gateway will block further requests by default. Or you can set up rules through Dynamic Routes to route requests to a fallback model after you’ve hit a spend limit, so that a hard spending cap won’t kill your engineers’ workflow. We’re working to add the capability for you to also send alerts when a limit is reached.
Spend limits are available in open beta today for all AI Gateway users across all plans. Configure them in your gateway settings in the dashboard or via the API.
We use this ourselves
We’re tracking token costs inside Cloudflare already. Every Cloudflare employee uses AI tools daily, routing millions of requests and billions of tokens per month through AI Gateway. We faced the same question every company faces at this scale: who’s using what, and how do we budget for it?
We solved this by enabling AI Gateway to add identity to every request. When an employee authenticates via Cloudflare Access, we extract their identity from the JSON Web Token (JWT) and attach it as metadata on the AI Gateway request. This makes per-user token consumption, team-level usage breakdowns, and cost attribution across the organization all visible in one place.
Identity-driven budgets and policies (closed beta)
In addition to spend limits, today we’re also announcing identity-driven budgets and policies as a closed beta.
Spend limits in AI Gateway let you set budgets by model, provider, or custom attributes. But your application has to pass that metadata, and AI Gateway trusts whatever it receives. For verified, automatic attribution, you need identity.
When combined with Cloudflare Access, AI Gateway can see who is making each request — not just which account, but which employee, which identity provider (IdP) group, which service, etc.
Here’s what that looks like in practice.
You can set per-user budgets, say \$500/month for individual contributors and \$2,000 for senior engineers. When a user hits their limit, requests can be downgraded to a cheaper model or blocked.
You can set per-team model policies. For instance, your ML team gets Claude Opus and GPT-4o. The brand design team can access generative image and video models. Interns use open-source models on Workers AI. These policies map directly to your existing IdP groups, the same identity provider groups you already manage.
For CI/CD pipelines and autonomous agents, Access service tokens allow you to give each agent a named identity. You can see that your code review bot used 5 million tokens this week while your documentation generator used 500,000. If one agent is running out of control, apply a budget policy without affecting any others.
Every AI Gateway log entry will include the authenticated identity: email, IdP group, service token name. Export these to your analytics platform, and you’ve got a cost-by-user-by-team breakdown without building anything custom.
Under the hood, you create a Cloudflare Access application for your AI Gateway endpoint and configure policies based on your IdP groups. When a developer or agent makes a request, they authenticate via OAuth, using the typical CLI device-code flow. AI Gateway validates the token and extracts the identity. You don’t need to write a custom Worker, parse JWTs yourself, or rely on honor-system metadata headers.
If you would like access to the closed beta, sign up here.
What’s next: from cost control to cost optimization
Setting a budget is necessary. But once you’ve got a budget, how do you make the most of it?
The reality is that not every request needs a frontier model: a summarization task can run on a smaller, cheaper model without meaningful quality loss, while a large-scale code refactor might require the bleeding edge. But without controls, people will almost always opt for the most advanced model.
A solution for that is coming next: We’re building intelligent, task-based routing in AI Gateway. For each request, we can analyze and automatically route it to the model that will give you the best result at the lowest cost. This is in active development, so follow our developer docs and changelog.
Get started
It’s free to get started with AI Gateway. Spend limits are available now for all users.
If you haven’t already, create a gateway and point your applications at it. From there, set up spend limits in the dashboard or via API. Start with a high limit in monitoring mode to understand your current usage patterns before you start enforcing.
If you want per-user attribution and team-based policies, sign up for the identity-driven budgets closed beta, and we’ll get you set up with the Access integration.
We want to hear how you’re managing AI costs today. Join the conversation on Cloudflare Community or reach out to discuss your broader AI security strategy.
VoidZero, the company behind Vite, Vitest, Rolldown, Oxc, and Vite+, is joining Cloudflare. As part of this change, all team members of VoidZero are joining Cloudflare, too.
Before saying anything else, we want to make the most important thing clear: Vite, Vitest, Rolldown, Oxc, and Vite+ will stay open source, vendor-agnostic, and community-driven. Nothing about that changes.
Cloudflare’s mission is to help build a better Internet. And a better Internet is an open Internet. Developers need choice, frameworks need a neutral foundation, and applications need to be portable. It is not reasonable to expect the entire web ecosystem to build around a single vendor. The most important tools and frameworks are portable by design.
Vite is one of the few foundational tools that the whole JavaScript ecosystem agrees on. It earned that position by being fast, excellent, portable, and vendor-neutral. One of the best ways Cloudflare can help build a better Internet is by investing in that foundational open source toolchain. A toolchain that makes the Internet better for everyone, not just people who use Cloudflare or choose to host with us.
Over the last few years we’ve invested heavily in making Cloudflare the best place to build and run websites, applications, and agents on our developer platform. But ultimately that choice will always be yours. Run your Vite application anywhere you want.
What this means for Vite
Today’s news gives Vite more resources to keep growing, while the things that make Vite what it is remain the same:
Vite remains MIT-licensed and open source.
Vite remains vendor-agnostic. Applications built with Vite run anywhere and will continue to do so.
Vite’s roadmap continues to be driven by the broader Vite team and community, and continues to be developed in the open.
Evan and the rest of the VoidZero team continue to lead Vite, Vitest, Rolldown, Oxc, and Vite+.
Cloudflare is committing engineering and resources to those projects, not redirecting them.
We made the same kind of commitment when Astro joined Cloudflare earlier this year. Astro is still open source, and still deploys anywhere. The team is still shipping the roadmap they were already shipping.
This commitment matters even more with Vite, because Vite is not one framework. Vite is the foundation underlying so many: Vue, SvelteKit, Nuxt, Astro, Solid, Qwik, Angular, React Router, TanStack Start. Even Next.js now has a Vite-based implementation in vinext. Vite has become a shared substrate for the JavaScript ecosystem.
Our number one goal is to maintain the trust that has earned Vite so much adoption. Not with our words here, but by proving it every day in how we support and develop these projects.
We also want to put our money where our mouth is when it comes to our support for open source and shared ecosystem foundations. As part of this announcement, Cloudflare is committing $1 million to a Vite ecosystem fund to support maintainers and contributors, administered by the Vite core team. Vite is bigger than VoidZero or Cloudflare, and the people who have helped build it should be part of what comes next.
Vite as the foundation
The Vite and Cloudflare teams have been collaborating well before this announcement, starting in 2024 with the Vite Environment API. The Environment API lets Vite run server code in something other than Node.js during development. We worked closely with the Vite team on its design, and then built the Cloudflare Vite plugin on top of it.
When you run vite dev with the Cloudflare plugin, your server code runs inside workerd, the same open-source runtime that powers Workers in production. Durable Objects, D1, KV, R2, Workflows, Workers AI, Agents, Service Bindings, Workers RPC – all of it runs locally inside the same runtime model as production.
For a long time, the cost of developing on a non-Node runtime was that local dev felt like a worse version of production. The Environment API removed that cost without forcing anyone to adopt a Cloudflare-specific dev server. Any runtime that wants to plug into Vite can do the same thing. That kind of design – a generic mechanism in Vite with provider-specific implementations – has proven to work well and is one we want to keep building on.
We knew we were on to something when we saw adoption of the Cloudflare Vite plugin take off:
Vite’s adoption curve is one of the more remarkable things to watch in the ecosystem right now. As of this writing, Vite is at roughly 129M weekly downloads. The Cloudflare Vite plugin (@cloudflare/vite-plugin) is at almost 14M weekly downloads.
If you had told us a year ago that a Cloudflare Vite plugin would reach downloads equivalent to more than 10% of Vite itself, we wouldn’t have believed you. What happened? AI happened. More software is being created than ever before, and a lot of it starts with AI-generated code. Those applications need a default stack and a place to run. Agent-coded applications are choosing Vite, and increasingly they are choosing Vite running on Cloudflare.
AI is changing how we write software
Developers used to be the only users of dev servers, bundlers, linters, formatters, and CLIs. That is no longer true: agents are using them too, constantly. They scaffold projects, run dev servers, read errors, write tests, lint and format code, deploy previews, and iterate.
A lot of AI-generated applications already start as Vite apps, because Vite is fast, well understood, and broadly compatible with what agents have seen in their training data. Fast feedback loops have always been important. They become even more critical when writing software with agents:
Fast builds, because they iterate more than humans do.
Fast tests, because they re-run the suite constantly to verify their own work.
Fast linting and formatting, because those tools become guardrails.
Clear, structured errors, because the agent has to read and act on them.
Consistent CLIs, because small inconsistencies cause big detours.
The entire VoidZero toolchain is built for this kind of loop. Vitest, Rolldown, Oxc, Oxlint, and Oxfmt are each among the fastest tools in their respective categories, and they work well when they are run over and over by an agent. Vite+ brings those pieces together into one toolchain, with one CLI, one configuration model, and fewer moving parts. That makes the development loop easier for people to understand, and easier for agents to drive reliably.
We are dogfooding this ourselves. The Cloudflare dashboard is built on Vite. Oxlint is already saving days of engineering time in Cloudflare codebases. Flue, the agent harness framework from the Astro team, is also moving onto Vite as its foundation. Flue can run agents on Node.js, Cloudflare Workers, GitHub Actions, GitLab CI/CD, and more, and the Cloudflare target now uses the official Cloudflare Vite plugin and workerd integration. Vite is becoming the default application foundation inside Cloudflare too.
Vite is becoming full-stack
A few years ago, the job of a build tool was straightforward: take source files, produce a bundle, hand it off. That is not enough for modern applications, especially in a world where some of those applications are agents themselves.
A modern application is server-rendered routes, APIs, background jobs, queues, databases, object storage, real-time, auth, plus a growing list of agents and AI capabilities. The “build” is no longer the end of the story. It is the start of a deployment that has to understand all of those pieces.
That means Vite has to become more than a build tool. It needs to understand more of the application, while staying true to what made Vite work in the first place: speed, simplicity, and portability.
Void, a deployment platform designed for Vite, has been another testbed for these ideas. It helped explore what a modern application framework should own, what deployment should feel like, and how much of the full application lifecycle can be unified around one toolchain. We have learned a lot from that work.
Now the work is putting those lessons in the right place. Some belong in Vite itself as provider-agnostic primitives: first-class abstractions and hooks for backends, APIs, agents, and deployment that any provider can implement. Other lessons belong inside Cloudflare. Cloudflare will provide a first-class implementation of those hooks on Workers and the rest of our Developer Platform.
Even though some Vite maintainers are joining Cloudflare, changes to Vite itself will continue to go through the same open contribution process as any other Vite contribution. Features added to Vite itself should not be Cloudflare-specific. They will work anywhere Vite works.
Moving Cloudflare toward Vite
The same principle shaped how we think about the future of Cloudflare’s own tooling. We are not moving Vite in the direction of Cloudflare. We are doing the opposite: moving Cloudflare’s application tooling onto Vite, so it is built on top of the same workflows developers already know.
We recently shipped a technical preview of cf, a new unified CLI for the whole Cloudflare platform. Vite is going to be the foundation of our CLI experience for applications. The end goal is one consistent CLI for all of Cloudflare, with the same ergonomics whether you are working on Workers, R2, D1, Agents, or anything else.
If we do this right, the Cloudflare CLI should feel like Vite, not like a separate thing bolted on next to Vite.
cf dev should be a superset of vite dev. Same speed, same hot module replacement, same plugin model, plus the Cloudflare runtime and bindings when you want them.
cf build should understand Vite projects natively, without an adapter dance.
cf deploy should make deploying a Vite app to Cloudflare simple.
If you are running Vite today, the path to Cloudflare will feel like swapping in a superset of the commands you already know. Same project shape. Same Vite workflows. The entire Cloudflare developer platform available when you want it.
What happens next
In the short term, nothing changes for Vite users or the frameworks building on top of Vite:
Vite, Vitest, Rolldown, Oxc, and Vite+ keep shipping. The VoidZero team keeps contributing and leading them.
The Cloudflare Vite plugin keeps improving.
The Environment API and the broader story of “run your server code in the right runtime locally” keeps getting better, including for non-Cloudflare runtimes.
Longer term:
We start the work on moving the Cloudflare CLI toward an experience built directly on top of Vite.
Vite will get new, clean, provider-agnostic primitives for full-stack apps and agents that work for everyone on any platform.
Over time, we intend to open-source the Void platform, so others can learn from it and build their own platforms on top of Vite and Cloudflare.
We will do all of this in public and with the community. The same way Vite has always been built.
Welcome VoidZero
Vite, Vitest, Rolldown, Oxc, and Vite+ exist because a deep ecosystem of open source contributors put years of work into them. These projects are already foundational to how the web is built, and we are grateful to everyone who helped get them here. Thank you to everyone who has contributed code, reviews, issues, docs, plugins, integrations, and support along the way.
We are excited to welcome the VoidZero team to Cloudflare, and excited to put more resources behind these projects. Our job now is to help them grow, stay open, and power the JavaScript ecosystem for everyone.
Today, we are extending Cloudflare’s cloud access security broker (CASB) to support the Claude Compliance API. Security and compliance teams can now monitor Claude usage directly in the Cloudflare dashboard. No endpoint agents required.
Enterprise security teams have long struggled to see how users interact with sanctioned and unsanctioned applications. The rapid adoption of AI applications has made this harder. Employees spend significant time in these new surface areas, and their interactions differ from traditional SaaS: users upload files, share freeform prompts, and providers generate content that may contain sensitive data.
Cloudflare CASB helps solve this problem. One API integration gives you out-of-band visibility and control over the applications your organization uses. This integration builds on our existing support for AI governance, extending coverage over the most common tools security teams now manage.
The fast path to safe AI adoption
AI adoption has outpaced security governance. While IT and security teams raced to enable AI tools for productivity, the controls lagged behind. Most organizations today operate with partial visibility: they may block unauthorized AI tools at the network layer, but they cannot see what happens inside sanctioned ones.
This matters because AI tools are not like traditional SaaS applications. They are conversational, persistent, and deeply integrated into workflows through APIs and agent frameworks. An employee might paste customer data into a prompt. A developer might accidentally share an API key and leave it unrotated for months. An AI application might generate content which contains company secrets. Each of these actions creates compliance risks that conventional security tools cannot detect.
Organizations are moving fast to adopt AI, but these tools require a different security model. They do not just read data; they generate it, act on it, and connect to multiple systems of record in a single workflow. Security needs to cover the full lifecycle: from how an application calls an API, to what data it handles, to where that data lives at rest. Cloudflare gives organizations the tools to do this at every point of the workflow:
Cloudflare AI Gateway sits between your applications and AI providers like Anthropic, giving you observability into requests, token spend, and model performance. This allows administrators to enforce rate limits, cache responses, and make fine-grained routing decisions.
Cloudflare Gateway and Data Loss Prevention inspect AI traffic for sensitive data, blocking prompts that contain customer personally identifiable information or confidential material before they reach the model.
Cloudflare Access with MCP server portals centralizes agent connections to corporate tools behind a single protected endpoint. Administrators control which users and agents can reach which systems, and every request is logged for audit.
Cloudflare CASB now extends this same unified approach to data at rest inside Claude, scanning for misconfigurations and sensitive data without endpoint agents.
These capabilities run side by side, on the same metal, making each service both composable and programmable. More importantly, that means traffic never hairpins through multiple vendors or clouds to be secured.
Better insight and control with Cloudflare CASB
Cloudflare CASB helps organizations connect to, scan, and monitor third-party SaaS applications for misconfigurations, improper data sharing, and other security risks through lightweight API integrations. Organizations can regain visibility and control over their growing investments in SaaS apps.
As enterprises deploy Claude at scale, security and compliance teams need the same visibility into Claude usage that they have for every other enterprise application in their stack. Anthropic recognized this gap and built the Claude Compliance API to give enterprises programmatic access to security-relevant data about their Claude organizations, workspaces, and usage.
Cloudflare CASB now consumes this endpoint to surface actionable security findings without requiring inline traffic inspection or endpoint agents.
What the Claude Compliance API surfaces
With this integration, Cloudflare One customers can monitor Claude Enterprise activity using the detection and remediation workflows they already rely on. Cloudflare CASB connects to Claude via the Compliance API and scans for security findings.
Starting today, Cloudflare supports security findings for the following assets:
Projects: Detect projects shared across the organization or a subset of users and groups
Project attachments: Files and documents added to projects that violate DLP policies
Chat files: User-uploaded and provider-generated files that violate DLP policies
Chat messages: User prompts and provider responses that violate DLP policies
Artifacts: Provider-generated documents and files that violate DLP policies
These findings appear directly in the Cloudflare dashboard alongside posture and content findings from your other SaaS applications. Findings are grouped by category and ordered by severity level. Security teams can triage, assign, and remediate Claude-specific risks using the same workflows they use for Microsoft 365, Google Workspace, or Salesforce.
Supporting Claude Enterprise and Claude Platform
For Claude Enterprise, CASB surfaces compliance data such as organizations, projects, chats, and roles. It also retrieves conversation content, including messages and uploaded files through dedicated read-only endpoints to prevent data loss.
For Claude Platform, CASB will continue to surface member and workspace changes, API key creation, and file create or download events. In the near future, we will add support for the Activity Feed.
CASB turns findings into action. A detected security finding in Claude, such as a user uploading files containing sensitive data, can become a Gateway policy in minutes. You can use Gateway to block uploads to Claude for specific users, restrict access to the application entirely, or limit functionality until the issue is resolved. This moves security teams from visibility to action by combining CASB findings with Cloudflare’s existing in-line policy engine.
Getting started
To enable the Claude Compliance API integration:
Ensure you have a Claude Enterprise account.
Request Compliance API access from Claude for your organization.
In the Cloudflare dashboard, go to Zero Trust > Integrations > Cloud & SaaS.
Select Add Integration > Anthropic and enter your Compliance API key.
Configure DLP profiles if you want to scan uploaded files for sensitive data.
The integration begins scanning immediately and surfaces findings in the dashboard within minutes.
For new Cloudflare customers, you can sign up and start with your first two integrations for free. Existing customers can enable the integration directly in the dashboard.
What’s next
We are continuing to expand CASB coverage for AI tools as providers release new enterprise security APIs. We are also deepening integrations within CASB to allow customers to create custom findings and build workflows which automatically remediate security findings.
The shift to agentic AI is here, and we believe the best way to help organizations safely adopt it is by providing a unified platform to build, deploy, and govern agents. To stay up to date, check our developer documentation or subscribe to get updated automatically.
We’ve enabled higher usage limits, faster performance, and better reliability for Browser Run by rebuilding on top of Cloudflare’s Containers.
You can now spin up 60 browsers per minute via the Workers binding and run up to 120 concurrently — 4x the previous limit. Also, Quick Action response times dropped more than 50%. You don’t need to change anything: these improvements are live today. On top of that, we’re shipping fixes and new features faster than before. Read on to learn how we did it and see the data.
Remind me: what is Browser Run?
Browser Run enables developers to programmatically control and interact with headless browser instances running on Cloudflare’s global network. That’s useful for end-to-end testing of web applications, securely investigating suspicious URLs, and leveraging how browsers can easily render PDF documents, amongst other quick actions like capturing screenshots and extracting content. More recently, it’s become a critical enabler of AI agents to interact with the web. We’re building Browser Run to be the go-to platform to responsibly utilize automated browsers securely at massive scale.
Outgrowing our bunk bed
Before adopting Cloudflare Containers, we shared infrastructure with Browser Isolation (BISO). While technically similar, BISO’s larger container images slowed startup and development. Crucially, BISO browsers lacked optimal global distribution, compromising resiliency and latency. Additionally, typical BISO users’ long, steady sessions clashed with Browser Run’s short, spiky usage, creating scaling bottlenecks and availability delays.
Thankfully, after much internal development, Cloudflare released Durable Object (DO)-enabled Containers open beta last year, meaning we were ready for a tentative adoption that ultimately benefited both product platforms. Like most successful product platforms, we’re committed to building on our own platform wherever feasible so that we can feel and fix any pain points ahead of any external customers.
The migration: Containers
We started a gradual migration by inserting a Worker in our incoming request paths to provide some Container-powered browsers to a handful of users alongside those from BISO. This dual support during development was key: it allowed us to compare performance, isolate implementation bugs and ultimately gain confidence in the benefits of the Container-driven approach.
Ramping up adoption, we first used the Container browsers for all of our Quick Actions endpoints, then for connections via the Workers browser binding on free accounts, followed by pay-as-you-go accounts in order to validate stability before we rolled it out to all remaining contract customers, ensuring a transition that required no action or existing worker redeployments from our customers.
Challenges: performance and scale bottlenecks
On our end, though, we faced a fresh set of challenges getting familiar with a novel, unstable early-stage Containers platform interface that was light on documentation, light on observability, and light on colleagues in an overlapping timezone. However, our feedback to our own teams as Customer Zero meant that we could provide a tight feedback loop leading to substantial upgrades that benefit our external customers too. Nevertheless, there was a lot of friction to overcome initially, most of which were to be expected for a closed beta in active development. Other hurdles to overcome were intrinsic to the new technical environment.
For example, once our browsers could run globally, our architecture had to adapt. DO-enabled Containers create a Durable Object as close to the incoming request as possible, but the connected Container may spin up on the other side of the world. This works fine for one-shot messages like “start my app,” but when you’re establishing a WebSocket between them and exchanging dozens of messages for a screenshot request, those extra milliseconds crossing the globe start adding up.
Our solution? Create regional pools of pre-warmed DO-backed browser containers to constrain the max distance (and hence max latency) between DOs and containers. When a request comes in, we pick a DO-container pair closest to the user within that region. This keeps latency low on both hops: user to DO, and DO to container. It adds a few more moving parts to our overall architecture, but we figured that was worthwhile so long as we had observability into the global state of each browser so that we could allocate and re-allocate capacity according to changing demand. A perfect use case for Workers KV…to a point.
Demand for our headless browsers has been ramping up since the beginning of last year. In short, AI agent builders discovered Browser Run and quickly brought request volumes outpacing our existing capacity. We quickly hit the limits of how quickly we could adjust our pool capacity to serve this new demand with a scalable approach. KV’s eventual consistency of around 30 seconds was becoming a bottleneck on our critical request path. You might check KV, see a container as “available,” but by the time you route to it (30 seconds later), it’s already claimed. That lag creates race conditions and overallocation of browsers, severely limiting how fast we could scale to meet demand spikes.
Migrating from KV to D1 + Queues
We previously stored each container state in KV. This meant that we could keep getting a minute old state due to cache TTL (recently KV changed the minimum cache TTL to 30 seconds, but even so that value is still too high).
We decided to migrate the container state into D1 instances instead. D1’s transactional nature is a good fit here. Once we assign a browser to a user, it’s exclusively theirs. Browsers are not shared resources. SQLite transactions ensure atomic assignment and prevent race conditions where two requests might claim the same browser simultaneously.
Here’s a simplified version of our browser acquisition query:
WITH candidate_pool AS (
-- candidate pool logic to pick based on latency and other rules
)
UPDATE containers
SET status = 'picked'
WHERE sessionId IN (
SELECT sessionId
FROM candidate_pool
ORDER BY RANDOM()
LIMIT ?5
)
RETURNING data
We keep D1 shards per location and given that we may have several thousand containers running, and that each container needs to update its state every 5 seconds, we kept running into a problem: we would overload the database. For instance, if each write takes 1ms we can only write at most 1,000 times, which at one row per write would mean that we could only have 5,000 containers before overloading the database.
However, if we batch those writes, we can get much higher values, because batch writes are not significantly longer than individual ones, so we can increase the throughput in orders of magnitude. In our case, we use 100 row batches, which means we can now update a maximum of 500,000 containers per location. This headroom means capacity planning is no longer a bottleneck.
Currently, our P95 for batch write is 0.1ms!
To batch writes, we use Queues: every 5 seconds, each container computes its own state and adds it to its location queue. We then configure a worker consumer with 100 batch size and 1 second batch timeout:
With this configuration, we achieve acceptable lag times well below 2 seconds. That said, queue backlogs can still cause stale state. When this happens, each region falls back to a designated backup region until the primary queue catches up.
Additional perks for quick actions
With dedicated infrastructure, we could now make upgrades to the browser container image without unwanted side effects or bloat for other products like BISO. This opened the door to optimize quick actions like screenshots and content extraction. Previously, our workers established a WebSocket to the remote browser and sent instructions one at a time: open a page, navigate to the URL, wait for it to load and take the screenshot. Each step had to be completed before the next could begin.
However, now we send all parameters in a single HTTP request directly to the container, and the entire flow executes internally without any back-and-forth between the worker and browser.
Results: massive performance boost and increased limits
We’ve seen a sharp decrease in average quick-action response time, as users are able to get what they need from a browser session in less time: less time waiting for browsers to be ready and faster processing of their DevTools Protocol messages.
Overcoming our real-time state management at this new scale meant we could spend more time in the playground, discovering and cooking up new features such as our recently launched /crawl endpoint.
Better browser flexibility
We also benefitted from another important perk by leaving behind shared Browser Isolation containers: faster upgrades.
When our browsers ran on shared product infrastructure, upgrading Chrome meant coordinating across multiple teams and products, each with their own roadmap and priorities. However, now that we run our own container image, we can upgrade at a faster tempo. For example, WebGL, a much-requested feature, is now available for browser-based rendering along with WebMCP (Model Context Protocol for the web) which enables new agentic interaction patterns. Both are made possible because we can control the browser version and flags without unwanted side effects in other Cloudflare products.
In a nutshell, we’re just getting started with unleashing the power of browsers at scale, especially for agentic development. We hope you’re diving in too — check out our docs.
Get started
Browser Run is available on all Workers plans. Start with the quick start guide, explore the Quick Actions, or try the /crawl endpoint to deeply extract data from any webpage, following links across the site.
Building AI agents? Check out our Agents SDK with built-in Browser Run support.
The collective thoughts of the interwebz
Manage Consent
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional
Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes.The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.