Skip to content

Changelog

New updates and improvements at Cloudflare.

Grant teammates and agents access to specific Workers

You can now grant access to specific Workers and choose from four roles to control the level of access you give teammates, agents, and CI/CD workflows.

Choose from four roles to control the level of access:

  • Metadata Read-Only: View settings, metrics, logs, and traces without access to Worker code or the ability to make changes.
  • Content Read-Only: Read Worker code, settings, and observability data without the ability to modify or deploy changes.
  • Editor: Update and deploy a Worker without the ability to delete it.
  • Admin: Everything in Editor, plus the ability to delete the Worker.
Permission policy form showing four roles scoped to an individual Worker

Worker-level access controls are available today for all customers. You can configure them in the Cloudflare dashboard, through the API, or with Terraform.

Roles designed for how teams build

Give Metadata Read-Only to a debugging agent so it can inspect settings and observability data without seeing Worker code. Give Content Read-Only to a code review agent so it can read code without changing it. Give Editor to a CI/CD workflow so it can deploy without deleting the Worker or accessing other Workers. Admin gives a teammate or agent full control over the Worker, including the ability to delete it.

Apply these roles across all Developer Platform products, across all Workers, or to an individual Worker.

Durable Objects

You can use granular permissions to control access to Durable Objects. Durable Objects do not have their own roles or scopes. Instead, they inherit the permissions assigned to the Worker that implements them.

Learn more about granular permissions in the Durable Objects documentation.

Grant access to members and User Groups

In the Cloudflare dashboard, go to Manage Account > Members and select a member. Create a permission policy, set the scope to Individual Workers, select the Workers they need, and choose a role to grant the right level of access.

If several people on the same team or project need the same access, assign the permission policy to a User Group instead of each member individually. Everyone added to the group automatically inherits the policy.

Create a scoped API token

For an agent or CI/CD workflow, go to Manage Account > Account API Tokens and create an account-owned API token. Set the scope to Specified Workers, select the Workers the token can access, and choose a role to grant the right level of access.

Account API token policy with Metadata Read-Only access scoped to a specific Worker

For more information, refer to the Workers roles and permissions documentation.

Miniflare v5 prepares local development for the cf CLI

Miniflare v5 prepares Cloudflare local development tooling for the upcoming cf CLI.

Miniflare powers local Workers development behind wrangler dev, the Cloudflare Vite plugin, and @cloudflare/vitest-plugin. Most projects should use those tools instead of depending on Miniflare directly, and Miniflare v5 will not require any action.

The most significant change is a new configuration shape which aligns Miniflare with cloudflare.config.ts, the programmatic Cloudflare configuration format now available for testing.

Other breaking changes include:

  • Removed deprecated APIs and options, such as legacy alpha D1 bindings.
  • Removed now-unused, internal APIs like wrappedBindings
  • Removed Miniflare's built-in module discovery; higher-level tools like Wrangler and the Vite plugin should be providing the module graph.
  • Moved local-only /cdn-cgi routes under /cdn-cgi/local.
  • Replaced per-resource persistence options with shared persistence root options.

For a more comprehensive list, refer to Miniflare's changelog ↗︎

This work sets up a cleaner foundation for the next generation of local development tooling, including the new cf CLI.

Python 3.14 for Python Workers

Python workers now use Python 3.14 by default.

This change applies to all new Python workers using compatibility date 2026-09-08 or later.

Internally, this change updates the Pyodide runtime to 314.0.6.

Manage Email Routing rules with Wrangler

You can now manage Email Routing rules that route emails to Workers from your Wrangler configuration. Add literal addresses or a catch-all address to the top-level addresses field:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "invoice-handler",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-10-16",
  "addresses": [
    "invoice@yourdomain.com"
  ]
}
name = "invoice-handler"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-10-16"
addresses = ["invoice@yourdomain.com"]

When you run wrangler deploy, Wrangler creates rules for new addresses, updates existing rules managed by the Worker, and removes managed rules that are no longer in the configuration. Wrangler shows the planned changes and asks for confirmation before applying potentially destructive changes.

Email Routing rules created by Wrangler also appear in the dashboard but with an icon that identifies them.

Email Routing rule created by Wrangler in dashboard

Refer to Configure rules with Wrangler for more information.

Enterprise customers can self-serve CDN upload limits up to 5 GB

Enterprise customers can now configure a zone's CDN Maximum Upload Size up to 5 GB directly from the Network page in the Cloudflare dashboard. This removes the need to contact your account team or Cloudflare Support when applications need to accept request bodies larger than 500 MB and no greater than 5 GB.

The default maximum upload size remains 500 MB. Upload limits above 5 GB still require additional configuration through your account team or Cloudflare Support.

Very large uploads may reach connection or read timeouts before reaching the configured size limit. Make sure clients and origins allow enough time to complete the transfer when increasing this setting.

Refer to Cache upload limits and Workers request body size limits for details.

Deploy larger Workers — up to 64 MiB for both free and paid plans

You can now deploy Workers with larger dependencies, heavier frameworks, and more code without hitting size limits.

When you deploy a Worker, Wrangler bundles your code and compresses it before uploading. Previously, Cloudflare checked that compressed size and rejected deploys over 3 MB (Free) or 10 MB (Paid). That limit has been removed. Cloudflare now only checks the uncompressed size of your bundle, which is 64 MiB across all plans.

To check your Worker's bundle size before deploying:

wrangler deploy --outdir bundled/ --dry-run
Total Upload: 259.61 KiB / gzip: 47.23 KiB

The Total Upload value is your uncompressed bundle size. This is what counts against the 64 MiB limit. The gzip value is shown for reference but is no longer a limit.

For more information, refer to the Worker size limits documentation.

Python Workers now support WSGI web frameworks like Django and Flask

Python web frameworks following the Web Server Gateway Interface (WSGI) ↗︎ or Asynchronous Server Gateway Interface (ASGI) ↗︎ specification can now be used in Python Workers.

Using web frameworks with Python Workers

Based on the web framework you are using, you can use either wsgi or asgi from the workers module.

WSGI frameworks

For WSGI frameworks like Django or Flask:

from workers import wsgi

from django.core.wsgi import get_wsgi_application

app = get_wsgi_application()
Default = wsgi.entrypoint(app)

The wsgi.entrypoint is equivalent to creating a WorkerEntrypoint class and using the wsgi.fetch method. If you want more control over the WorkerEntrypoint class, you can do so:

from workers import wsgi, WorkerEntrypoint

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        return await wsgi.fetch(app, request, self.env)

ASGI frameworks

For ASGI frameworks like FastAPI or Starlette:

from workers import asgi

from fastapi import FastAPI

app = FastAPI()
Default = asgi.entrypoint(app)

For more information about using individual web frameworks, refer to the packages documentation in Python Workers.

Durable Objects can use up to ten Dynamic Workers concurrently

Durable Objects can have up to ten distinct Dynamic Workers with in-flight requests, increased from four. This limit applies across all concurrent requests to the same Durable Object because they share an input/output (I/O) context. Other Workers can have up to four distinct Dynamic Workers with in-flight requests per request.

Multiple in-flight requests to the same Dynamic Worker count as one toward this limit.

For more information, refer to Dynamic Workers limits.

Preserve exception details in console logs

Console methods now preserve exception details in your Worker's logs. When your Worker logs an exception, the corresponding log entry includes the exception name, message, and stack.

For example, your Worker can catch and log an exception:

try {
	throw new Error("Deliberately created exception");
} catch (error) {
	console.error("caught exception:", error);
}
try {
	throw new Error("Deliberately created exception");
} catch (error) {
	console.error("caught exception:", error);
}

If you use Workers Observability, your log is automatically enriched with structured error information. The following example shows how the enriched log appears in the Cloudflare dashboard:

Workers Observability log entry showing a caught exception and its stack trace

The exception's stack trace appears directly in the log message.

If you send telemetry to a Tail Worker, the Tail Worker now receives a log entry with an errorInfo array:

{
	"message": ["Request failed:", "RangeError: Value out of range"],
	"errorInfo": [
		null,
		{
			"name": "RangeError",
			"message": "Value out of range",
			"stack": "RangeError: Value out of range\n    at ..."
		}
	],
	"level": "error",
	"timestamp": 1784851200000
}

Each errorInfo item corresponds to the console argument at the same index in message. Arguments that are not exceptions have a null entry.

Choose OAuth scopes for Wrangler and the Cloudflare API MCP server

Wrangler and the Cloudflare API MCP server now use optional OAuth scopes. During authorization, you can choose which optional scopes to grant instead of approving every scope requested by each client.

The consent dialog now includes the option to edit the permissions you grant to Wrangler or the Cloudflare API MCP server:

OAuth consent dialog with an Edit Permissions button

You can then choose which specific permissions to grant:

OAuth permission editor with controls for individual scopes

Required scopes remain selected. Choosing fewer optional scopes limits each tool's access to the permissions needed for your workflow.

If a command or tool call needs a scope that you declined, reauthorize the client and grant that scope.

For more information, refer to wrangler login and Edit optional permissions.

View deployments for Durable Objects in the dashboard

Durable Object namespaces now have a Deployments tab in the Cloudflare dashboard, showing the versions of the backing Worker that are currently live and the traffic split between them.

The Deployments tab for a Durable Object namespace, showing two versions with their traffic %, requests/sec, error rate, and median wall time Go to Durable Objects ↗

A Durable Object namespace is backed by a Worker script, so its deployments are the same as that Worker's deployments. Previously, checking on a gradual deployment in progress for a Durable Object meant navigating to the backing Worker. The new tab surfaces that information directly on the namespace, alongside the metrics that matter for it: requests, error rate, and wall time per version.

The tab is read-only — promoting, rolling back, or splitting traffic on a deployment is still managed from the backing Worker's Deployments tab.

Actual vs. configured traffic split

The Traffic % column, for both Workers and Durable Objects, now shows the actual, observed traffic share for each version next to the percentage you configured. Previously, this column only showed the configured percentage. If you moved a deployment from 50/50 to 100% on a new version, the configured number updated immediately, but requests take time to catch up, and there was no way to tell how far along that shift was without checking metrics elsewhere.

The configured split assigns Worker versions to individual Durable Objects, not to individual requests. Because each Durable Object is pinned to the version it started on until you create a new deployment and some objects naturally receive more traffic than others, the observed split can differ from the configured one for as long as multiple versions are active.

Actual traffic share is calculated from the same GraphQL Analytics API data that powers other Workers and Durable Objects metrics, so standard ingestion delay and sampling apply. Durable Objects analytics can lag Workers analytics by several minutes, so a version's actual share may take a little longer to catch up after a change.

To view this, go to Workers & Pages > Durable Objects, select a namespace, then select the Deployments tab. For more on how gradual deployments work, refer to Gradual deployments.

@cloudflare/vitest-pool-workers is now @cloudflare/vitest-plugin

Version 1 of the Workers Vitest integration is published as @cloudflare/vitest-plugin ↗︎. The package was formerly named @cloudflare/vitest-pool-workers.

The Vitest configuration API is unchanged. Existing projects must update the dependency name, package imports, and TypeScript types entries.

To migrate automatically, run:

npx @cloudflare/codemods vitest:pool-workers-to-vitest-plugin

The codemod updates your dependency, imports, and test TypeScript configuration. For manual migration steps, refer to Migrate to Vitest plugin.

For outbound request mocks in Workers tests, use the @msw/cloudflare ↗︎ integration. Refer to Mock outbound requests.

Connect handler no longer requires an experimental flag

Workers can now export a connect() handler without enabling any compatibility flag. Previously, using a connect() handler required the experimental compatibility flag, which cannot be enabled in production.

A connect() handler lets a Worker accept a TCP connection forwarded from another Worker over a service binding, and read and write raw bytes on it using a Socket object:

backend-worker.jsjs
export default {
	async connect(socket, env, ctx) {
		const writer = socket.writable.getWriter();
		await writer.write(new TextEncoder().encode("hello"));
		await writer.close();
	},
};

Another Worker forwards a connection to it by calling connect() on the service binding:

frontend-worker.jsjs
export default {
	async fetch(request, env) {
		const socket = env.BACKEND.connect("example.com:1234");
		return new Response(socket.readable);
	},
};

On the connect() handler side, socket.opened resolves with localAddress set to the address the caller passed to connect().

Direct inbound TCP connections remain unsupported at present.

For more information, refer to the connect handler documentation.

You can now enable Access on a Worker or all Workers at once

You now have two new ways to protect your Workers with Cloudflare Access.

Protect an application across all its domains at once

Until now, if a Worker was reachable on a route, a Custom Domain, and a workers.dev URL, you had to manually add each one to an Access application and keep the list in sync whenever routes or domains changed.

Now, Access attaches the policy to the Worker itself, so every associated domain and preview URL stays protected even when its routes or domains change.

Access setting for protecting a single Worker

Protect all new and existing Workers by default

Make all Workers private by default, so every existing and newly created Worker requires sign-in before anyone can reach it.

Account-wide Access setting that protects all Workers

If a specific Worker should remain publicly accessible, add a Worker-level bypass to exempt it.

Make a Worker public when all Workers are protected

Whether you protect a single application or all Workers at once, you can choose whether to protect preview deployments only or both previews and production, and control who can sign in by Cloudflare account membership, email address, or email domain.

For more advanced policy options, edit the policy in Zero Trust ↗︎.

Access policy configuration for controlling who can sign in

View all of your Worker Access policies

You can view and manage all of your Access policies in the Access tab of the Workers & Pages section in the dashboard.

Access tab showing all configured Access policies

See who is accessing your Worker

When Access is enabled on your Worker, every authenticated request includes ctx.access. Call ctx.access.getIdentity() to get the user's email, name, and groups — no manual JWT validation required.

export default {
  async fetch(request, env, ctx) {
    if (!ctx.access) {
      return new Response("Access did not run", { status: 401 });
    }

    const identity = await ctx.access.getIdentity();
    return Response.json({ aud: ctx.access.aud, email: identity?.email });
  },
};

Test Access locally

You can now test Cloudflare Access locally with wrangler dev. Add a dev block to your wrangler.jsonc:

{
  "access": {
    "dev": {
      "aud": "my-app",
      "identity": { "email": "admin@example.com" }
    }
  }
}

Your Worker will receive this identity through ctx.access and ctx.access.getIdentity(), letting you test authenticated and unauthenticated flows without deploying. Remove the dev block to simulate unauthenticated requests.

API and programmatic access

You can also set up these policies through the Workers API instead of the dashboard.

Cloudflare Agents: trace every model call, tool run, and decision

Cloudflare Agents ↗︎ now has a dedicated home in the Cloudflare dashboard that lists your traced agents and subagents.

Select an agent to inspect its traces in two ways:

Replay a session assembles the full conversation for a turn: system instructions, user messages, model reasoning, tool calls with arguments and results, and the final response.

Session replay showing messages, reasoning, and subagent tool calls

View a trace shows the execution timeline, connecting agent operations to Workers infrastructure so you can see where time was spent.

Trace waterfall showing nested agent, model, tool, and D1 spans

Enable tracing in your Wrangler configuration:

{
	"observability": {
		"enabled": true,
		"traces": {
			"enabled": true
		}
	}
}
[observability]
enabled = true

  [observability.traces]
  enabled = true

Framework support

Think and Flue agents are instrumented automatically. For direct AI SDK calls, wrap the namespace with wrapAISDK(). Custom harnesses can use the Workers custom spans API with OpenTelemetry Generative AI semantic conventions ↗︎.

Get Started

Check out the full blog post: Introducing: Cloudflare Agents ↗︎.

For setup details, refer to the Agent tracing documentation.

AI agents can debug Workers with local tracing

wrangler dev and vite dev automatically capture structured OpenTelemetry traces and correlated console logs during local Worker invocations.

Debug with AI agents

When the tooling detects an AI agent session, it prints a terminal hint pointing to the Local Explorer API at /cdn-cgi/local/explorer/api. The API serves an OpenAPI schema and exposes a read-only observability query endpoint for discovering telemetry, querying traces and logs, and inspecting binding state.

The agent can identify the exact failing operation, fix the code, rerun the request, and verify the result. This debug loop requires no deployment or temporary logs.

Inspect traces in Local Explorer

Humans can inspect the same traces and correlated console logs in the Local Explorer browser UI. Each trace shows spans, timing, attributes, and errors.

Local Explorer showing a failed Worker trace with spans, timing, and errors

Automatic spans cover handler calls, outbound fetch() calls, and binding calls. Custom spans appear alongside these automatic spans.

For more details, refer to the Local Explorer documentation.

Node.js compatibility is now enabled by default

Workers now enable the nodejs_compat and nodejs_compat_v2 compatibility flags by default for compatibility dates of 2026-08-04 or later. These flags are not used for these compatibility dates because the compatibility date enables the same behavior.

This means all Node.js built-in APIs supported by the Workers runtime are available by default, including node:crypto, node:buffer, node:stream, node:net, node:dns, node:fs, node:http, and more. npm packages that depend on these APIs will work without additional configuration.

Workers using an earlier compatibility date are not affected. They can still opt in by adding nodejs_compat to compatibility_flags.

New projects do not need to add either flag. Existing projects can update their compatibility date without removing them. Wrangler, Miniflare, the Cloudflare Vite plugin, and Vitest Pool Workers ignore these redundant flags when starting the runtime.

To turn off Node.js compatibility completely, remove any nodejs_compat and nodejs_compat_v2 flags. Then add both of the following flags:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  // Set this to today's date
  "compatibility_date": "2026-10-16",
  "compatibility_flags": [
    "no_nodejs_compat",
    "no_nodejs_compat_v2"
  ]
}
# Set this to today's date
compatibility_date = "2026-10-16"
compatibility_flags = ["no_nodejs_compat", "no_nodejs_compat_v2"]

For more information, refer to the Node.js compatibility documentation.

Log in to Wrangler without a local callback server

wrangler login now supports the OAuth 2.0 Device Authorization Grant ↗︎. Pass --device to authenticate without starting a temporary callback server on localhost:8976:

npx wrangler login --device

Wrangler prints a verification URL and a short user code, opens the URL in your default browser with the code already filled in, and polls Cloudflare for an access token while you approve the request:

 ⛅️ wrangler 4.119.0
────────────────────
Attempting to login via OAuth Device Authorization Grant...
To authorize Wrangler, please visit:

  https://dash.cloudflare.com/oauth2/device

and enter the code:

  jPqK6Qvs

You have 5 minutes to approve this request.

Opening a link in your default browser: https://dash.cloudflare.com/oauth2/device?user_code=jPqK6Qvs
Successfully logged in.

The default login flow needs your browser to reach localhost:8976, which is not always possible from containers, remote SSH sessions, or GitHub Codespaces. Previously these environments required forwarding ports or fetching the callback URL with curl from a second terminal session. Because --device has no callback server, those workarounds are no longer necessary.

Since the plain verification URL and user code are both printed to the terminal, you can also approve the request from a phone or another machine. Pass --browser=false to stop Wrangler from opening a browser at all.

Available in Wrangler version 4.119.0 or later. For more information, refer to wrangler login.

Preview: @cloudflare/computer agent runtime

We're releasing an early preview of @cloudflare/computer ↗︎, an open-source agent runtime that gives every agent its own computer. The runtime dynamically orchestrates between fast, efficient isolates and full Linux containers, so the agent always runs on the right compute primitive for the task at hand.

@cloudflare/computer provides a virtual filesystem backed by SQLite, which you can populate from cloud storage, source control, or any files you choose. Agents can read, write, and edit files, run shell commands, and interact with Git repositories. All operations are gated, audited, and observed.

Install the package via npm:

npm install @cloudflare/computer

Instantiate a Workspace inside any Durable Object to give your agent a filesystem and execution runtime:

import { Workspace } from "@cloudflare/computer";

export class Agent {
	workspace = new Workspace({
		storage: this.ctx.storage,
	});
}

Several execution backends are included or you can write your own:

  • Isolate runtime — fast, horizontally scalable execution via just-bash and Dynamic Workers, ideal for file manipulation and data processing.
  • Container runtime — full Linux environment via Cloudflare Containers, mounted through FUSE, for tasks that need native binaries, package managers, or a complete userland.

The AI SDK-compatible toolkit provides common agent tools (read, write, edit, ls, exec) and guides the model to choose the appropriate backend for each task.

For more examples, including a step-by-step tutorial, visit the @cloudflare/computer repository ↗︎.

Read the announcement blog post for more details: Your agent needs a computer, not a container ↗︎.

Python and JavaScript Workers can now call each other via RPC

You can now call methods between Python and JavaScript Workers using Workers RPC. This works through Service bindings without extra dependencies, schema definitions, or serialization code.

Cross-language RPC calls behave like ordinary function calls. Exceptions propagate to the call site. You can pass structured cloneable types ↗︎ as parameters or return values, and Pyodide Foreign Function Interface (FFI) automatically converts types between languages.

Call a TypeScript Worker from Python

Define a method in a TypeScript Worker:

index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

export class RpcService extends WorkerEntrypoint {
	async add(a, b) {
		return a + b;
	}
}
index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";

export class RpcService extends WorkerEntrypoint {
	async add(a: number, b: number): Promise<number> {
		return a + b;
	}
}

Call it from a Python Worker through a Service binding:

from workers import Response, WorkerEntrypoint

class Default(WorkerEntrypoint):
	async def fetch(self, request):
		rpc = self.env.RPC
		result = await rpc.add(42, 144)
		return Response.json({"result": result})

Configure the Service binding in the Python Worker's Wrangler configuration:

{
	"services": [
		{
			"binding": "RPC",
			"service": "ts-rpc-server",
			"entrypoint": "RpcService"
		}
	]
}
[[services]]
binding = "RPC"
service = "ts-rpc-server"
entrypoint = "RpcService"

Call a Python Worker from JavaScript

Define a method in a Python Worker:

from workers import WorkerEntrypoint

class Default(WorkerEntrypoint):
	async def highlight_code(self, code: str, language: str) -> dict:
		from pygments.formatters import HtmlFormatter
		from pygments import highlight
		from pygments.lexers import get_lexer_by_name

		lexer = get_lexer_by_name(language, stripall=True)
		formatter = HtmlFormatter(linenos=True, cssclass="highlight", style="monokai")
		highlighted_html = highlight(code, lexer, formatter)
		css = formatter.get_style_defs(".highlight")

		return {
			"html": highlighted_html,
			"css": css
		}

Call it from a JavaScript Worker through a Service binding:

index.jsjs
export default {
	async fetch(request, env) {
		const rpc = env.PYTHON_RPC;
		const result = await rpc.highlight_code("print(42)", "python");
		return Response.json(result);
	},
};
index.tsts
export default {
	async fetch(request, env) {
		const rpc = env.PYTHON_RPC;
		const result = await rpc.highlight_code("print(42)", "python");
		return Response.json(result);
	},
};

Configure the Service binding in the JavaScript Worker's Wrangler configuration:

{
	"services": [
		{
			"binding": "PYTHON_RPC",
			"service": "py-rpc-server"
		}
	]
}
[[services]]
binding = "PYTHON_RPC"
service = "py-rpc-server"

For more details on the announcement, read the blog post ↗︎.

For more information, refer to the Workers RPC documentation and the Python Workers overview.

Inspect Worker startup performance with Wrangler

wrangler check startup now reports your Worker's raw and compressed bundle sizes. It also summarizes local CPU activity during startup directly in your terminal.

Large bundles and costly startup work can introduce cold-start latency, so use this command to find code and large dependencies that slow your Worker before it handles requests.

The summary includes sampled, active, garbage collection, and idle time. Wrangler continues to save a .cpuprofile file for detailed flamegraph analysis in Chrome DevTools or VS Code.

⛅️ wrangler 4.116.0
───────────────────────────────────────────────
├ Building your Worker
│ Worker Built! 🎉
│
├ Analysing
│ Startup phase analysed
│
│ Bundle: 7171.25 KiB / gzip: 2197.00 KiB
│
│ Local startup profile:
│   Profile window: 70.3 ms
│   Sampled time: 70.3 ms
│   Active: 38.5 ms (including 3.7 ms garbage collection)
│   Idle: 31.8 ms
│   Samples: 36
│
│ CPU Profile has been written to worker-startup.cpuprofile. Load it into the Chrome DevTools profiler (or directly in VSCode) to view a flamegraph.
│
│ Note that the CPU Profile was measured on your Worker running locally on your machine, which has a different CPU than when your Worker runs on Cloudflare.
│
│ As such, CPU Profile can be used to understand where time is spent at startup, but the overall startup time in the profile should not be expected to exactly match what your Worker's startup time will be when deploying to Cloudflare.

The profile runs locally, so its duration will differ from startup time on Cloudflare. For authoritative startup time, deploy your Worker or upload a version.

Available in Wrangler version 4.116.0 or later. For more information, refer to wrangler check startup.

Cloudflare MCP servers support the new MCP 2026-07-28 Specification

Cloudflare's product-specific MCP servers now support the new MCP 2026-07-28 Specification. Each request runs on a fresh stateless server without an MCP protocol session or protocol-specific Durable Object.

The /mcp endpoint also accepts stateless requests from 2025 Streamable HTTP clients. Most clients can reconnect without configuration changes.

Use /mcp for new connections. Historical /sse URLs continue to work as aliases for the same Streamable HTTP handler, but they no longer serve the deprecated HTTP+SSE transport. If a client forces SSE transport, change it to Streamable HTTP or automatic transport detection.

Workers tracing — write custom spans with new startActiveSpan() and span.end() runtime APIs

The Workers runtime now provides built-in tracing.startActiveSpan() and span.end() APIs, allowing you to write custom spans for operations that last beyond a single callback — for example, instrumenting a stream pipeline where the span should stay open until the stream is fully consumed.

This augments the existing API for writing custom spans, tracing.enterSpan(), which automatically ends a span when its callback is returned. With startActiveSpan(), the span remains open after the callback returns, and you call span.end() when the work is complete:

src/index.jsjs
import { tracing } from "cloudflare:workers";

const encoder = new TextEncoder();

export default {
	fetch() {
		return tracing.startActiveSpan("stream-response", (span) => {
			let timer;

			const body = new ReadableStream({
				start(controller) {
					controller.enqueue(encoder.encode("Starting...\n"));

					timer = setTimeout(() => {
						controller.enqueue(encoder.encode("Complete.\n"));
						controller.close();

						span.setAttribute("stream.status", "complete");
						span.end();
					}, 1000);
				},

				cancel() {
					if (timer !== undefined) clearTimeout(timer);

					span.setAttribute("stream.status", "cancelled");
					span.end();
				},
			});

			return new Response(body, {
				headers: { "content-type": "text/plain" },
			});
		});
	},
};
src/index.tsts
import { tracing } from "cloudflare:workers";

const encoder = new TextEncoder();

export default {
	fetch(): Response {
		return tracing.startActiveSpan("stream-response", (span) => {
			let timer: ReturnType<typeof setTimeout> | undefined;

			const body = new ReadableStream<Uint8Array>({
				start(controller) {
					controller.enqueue(encoder.encode("Starting...\n"));

					timer = setTimeout(() => {
						controller.enqueue(encoder.encode("Complete.\n"));
						controller.close();

						span.setAttribute("stream.status", "complete");
						span.end();
					}, 1000);
				},

				cancel() {
					if (timer !== undefined) clearTimeout(timer);

					span.setAttribute("stream.status", "cancelled");
					span.end();
				},
			});

			return new Response(body, {
				headers: { "content-type": "text/plain" },
			});
		});
	},
};

For more details, refer to the custom spans documentation.

Agents SDK adds MCP Specification 2026-07-28 support

Agents SDK v0.20.0 adds client and server support for the MCP 2026-07-28 release candidate ↗︎. Workers can serve tools, prompts, resources, and elicitation without an MCP transport session or Durable Object. Agents can connect to both MCP 2026-07-28 servers and existing legacy servers.

Client support

The MCP client manager now uses @modelcontextprotocol/client. For each connection, it probes for MCP 2026-07-28 support with server/discover. If the server does not support the stateless protocol, the client continues with the legacy initialize handshake on the same connection. Existing addMcpServer calls do not need a protocol-version setting or separate clients for each protocol generation.

For stateless requests, elicitation uses input_required through multi-round-trip requests (MRTR). The legacy path uses the same form and URL handlers for pushed requests. The SDK collects input, retries the original operation, and resolves the original callTool, getPrompt, or readResource promise with its final result.

OAuth callbacks now validate issuer metadata through the v2 SDK. Discovery state and issuer-bound credentials persist across browser redirects and Durable Object hibernation.

Run stateless servers

createMcpHandler now accepts a factory that returns a server from @modelcontextprotocol/server. The factory creates an isolated server for each request.

import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";

function createServer() {
	return new McpServer({ name: "example", version: "1.0.0" });
}

export default {
	fetch(request, env, ctx) {
		return createMcpHandler(createServer)(request, env, ctx);
	},
};
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";

function createServer() {
	return new McpServer({ name: "example", version: "1.0.0" });
}

export default {
	fetch(request, env, ctx) {
		return createMcpHandler(createServer)(request, env, ctx);
	},
} satisfies ExportedHandler;

The isolated agents/mcp/server entry keeps McpAgent, WorkerTransport, MCP client transports, and SDK v1 modules out of stateless server bundles.

The Workers wrapper validates present browser Origins, supports explicit delegation to trusted Origin middleware, and exposes request handling plus typed change notifications.

Backward compatibility

The same createMcpHandler(createServer)(request, env, ctx) route serves MCP 2026-07-28 clients and legacy clients that use stateless requests. You do not need separate routes or tool definitions for ordinary tools, prompts, and resources.

McpAgent is deprecated and feature-frozen. Migrate existing McpAgent servers to the stateless handler at your earliest convenience. If a server depends on protocol sessions, RPC, pushed server-to-client requests, standalone streams, or replay, use the migration guide to design stateless equivalents and run both routes while clients transition.

Migrate existing SDK v1 servers

Upgrade the Agents SDK:

npm i agents@latest

Move ordinary SDK v1 server definitions into an SDK v2 factory and serve them with createMcpHandler. The handler's default legacy compatibility means most stateless deployments need only one route.

If an existing McpAgent server still needs sessionful features, add the stateless path beside it. Use isLegacyRequest() to send only legacy traffic to the existing route:

import { isLegacyRequest } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { MyMcpAgent } from "./legacy-server";
import { createServer } from "./server";

const stateless = createMcpHandler(createServer, {
	route: "/mcp",
	legacy: "reject",
});
const legacy = MyMcpAgent.serve("/mcp");

export default {
	async fetch(request, env, ctx) {
		if (await isLegacyRequest(request)) {
			return legacy.fetch(request, env, ctx);
		}
		return stateless(request, env, ctx);
	},
};
import { isLegacyRequest } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { MyMcpAgent } from "./legacy-server";
import { createServer } from "./server";

const stateless = createMcpHandler(createServer, {
	route: "/mcp",
	legacy: "reject",
});
const legacy = MyMcpAgent.serve("/mcp");

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext) {
		if (await isLegacyRequest(request)) {
			return legacy.fetch(request, env, ctx);
		}
		return stateless(request, env, ctx);
	},
} satisfies ExportedHandler<Env>;

Migrate the remaining sessionful features, allow existing sessions to drain, then remove the legacy route. Refer to Migrate to MCP SDK v2 for package changes, compatibility limits, and rollout steps.

Deprecations in v0.20.0

This release deprecates the following Agents SDK APIs:

Deprecated API Replacement Status
McpAgent Use an SDK v2 factory with createMcpHandler for stateless servers. Use the migration guide to replace stateful features before removing a legacy route. Feature-frozen. No removal version is announced.
createMcpHandler(v1Server, options) Move the server to an SDK v2 factory and call createMcpHandler(factory, options). Use createLegacyMcpHandler only as a temporary bridge for sessionful features. Scheduled for removal in the next major version.
MCPClientManager.callTool(params, resultSchema, options) and the equivalent withX402Client overload Use callTool(params, options) or callTool(confirm, params, options). Compatibility overload. No removal version is announced.

The MCP 2026-07-28 draft separately deprecates Roots, Sampling, Logging, the old HTTP+SSE transport, and Dynamic Client Registration.