Skip to content

Changelog

New updates and improvements at Cloudflare.

Block emails by content with blocked content rules

Cloudflare Email security now lets administrators write their own content-based blocking rules. A new Blocked content area under Policies & rules lets you define a plaintext string or a regular expression, choose whether to scan the message subject, body, or both, and automatically block any message that matches.

  • Create rules using either plaintext matches or regular expressions — useful for blocking targeted phishing campaigns, known-bad phrases, or content patterns unique to your organization.
  • Choose the search location for each rule: subject, body, or subject and body.
  • Use the built-in regular expression checker to validate your pattern against sample text before saving, so you can confirm the rule matches what you expect and avoid false positives.
  • Matching messages are marked with a malicious disposition and prevented from reaching users' inboxes.

Blocked content rules currently only support the block action.

This feature is available for the following Email security packages:

  • Enterprise
  • Enterprise + PhishGuard

To get started, refer to Blocked content.

Independent MFA supports FIDO2 for infrastructure applications

Infrastructure applications support independent multi-factor authentication (MFA) with FIDO2 keys. You can allow ssh_fido2_key, piv_key, or both in application-level and policy-level MFA settings.

Users enroll FIDO2 keys through the App Launcher and connect with the generated SSH identity. FIDO2 keys for SSH are separate from browser-based WebAuthn security keys and Personal Identity Verification (PIV) keys.

For setup instructions, refer to Enroll a FIDO2 key for infrastructure apps and Configure MFA for infrastructure applications.

MCP protocol detection and AI Security dashboard

Cloudflare Gateway now automatically detects Model Context Protocol (MCP) ↗︎ traffic flowing through your network. MCP is the standard protocol used by AI agents to connect to external tools and data sources. Gateway identifies MCP requests by inspecting protocol-specific headers and payload characteristics.

MCP policy selector

A new Is MCP selector (experimental.is_mcp) is available in HTTP policies. Use this selector to build Gateway rules that allow, block, or isolate MCP traffic.

This selector is currently in beta and may change before general availability.

For example, the following policy blocks MCP traffic that does not arrive through an approved MCP portal:

Selector Operator Value Logic Action
Is MCP is True And Block
Traffic Source is not MCP portal
Example Gateway policy that blocks MCP traffic not arriving through an MCP portal

AI security report

A new AI security report dashboard under Insights & Logs > Dashboards provides visibility into MCP usage across your organization. The dashboard includes:

  • Total MCP request volume, unique users, and unique MCP servers
  • A timeseries chart of unique MCP servers observed over time
  • A summary of Gateway policies that target MCP traffic
AI security report dashboard showing MCP detection data including total MCP requests, users, servers, and Gateway policies for MCP

For more information, refer to HTTP policies.

Traffic Source selector in Gateway policies

Gateway HTTP and Network policies now include a Traffic Source selector that identifies how traffic reaches Cloudflare. This allows administrators to write policies that target specific on-ramp methods - for example, applying different rules to traffic arriving via the Cloudflare One Client compared to traffic routed through an MCP portal or a proxy endpoint.

Available traffic source values

UI name API value Description
Device client device_client Traffic from the Cloudflare One Client (WARP)
Mesh mesh Traffic from a Cloudflare Mesh connector
Cloudflare WAN cloudflare_wan Traffic from Cloudflare WAN (Magic WAN)
Clientless RDP clientless_rdp Traffic from a clientless RDP session
Proxy endpoint proxy_endpoint Traffic from a proxy endpoint (PAC file)
Clientless Browser Isolation agentless_biso Traffic from clientless Browser Isolation
MCP portal mcp_portal Traffic from an MCP portal

The selector uses the net.onramp.type API field in both HTTP and Network policies.

UI name API example
Traffic Source net.onramp.type == "device_client"

Browser Isolation selector

A Browser Isolation selector is also available in Network and HTTP policies. This selector identifies whether the current session is running inside Remote Browser Isolation, allowing administrators to apply different policy behavior to isolated traffic.

UI name API example
Browser Isolation net.is_isolated == true

For more information, refer to HTTP policies and Network policies.

Hostname routing is now generally available, with a new public IP range for initial resolved IPs

Hostname routing ↗︎ is now generally available. Instead of managing static IP lists and routes, you can route traffic by hostname across multiple Cloudflare One connectors:

  • Cloudflare Tunnel: route a private hostname (for example, wiki.internal.local) to a private application behind your tunnel, or a public hostname (for example, bank.example.com) to egress through a specific tunnel and anchor traffic to a dedicated exit node.
  • Cloudflare Mesh: attract a private or public hostname's traffic to a Mesh node.

Alongside GA, the default IPv4 range used for initial resolved IPs (also called token IPs) is changing from a Carrier-Grade NAT (CGNAT) range to a public Cloudflare-owned range:

  • IPv4: 172.64.128.0/20
  • IPv6: 2606:4700:0cf1:4000::/64

This is the default range. You can configure a custom initial resolved IP range for IPv4 if it conflicts with your existing network.

Why this is changing: Starting with Chrome 142 ↗︎, Local Network Access (LNA) restrictions block background requests to CGNAT addresses (100.64.0.0/10), which included the previous initial resolved IP default (100.80.0.0/16). LNA is implemented at the Chromium engine level, so it affects all Chromium-based browsers (for example, Microsoft Edge, Brave, and Opera), not only Google Chrome. This could silently break hostname-based Gateway features for users of these browsers, and required Chrome Enterprise policy workarounds. The new default range is public Cloudflare address space, so it is not affected by this restriction.

What is affected: Initial resolved IPs are used by several features that associate a DNS query with the network connection that follows it:

You can check your account's current range, or configure a custom range, at any time from Networking > IP addresses > Address space > Custom IPs, or using the Initial Resolved IP Subnet API.

Go to Custom IPs ↗

For full instructions, refer to Configure initial resolved IPs. The IPv6 range (2606:4700:0cf1:4000::/64) is unchanged and is not affected by this restriction.

The default IPv4 range, and all Cloudflare One IPv6 ranges, are automatically routed through the Cloudflare One Client and do not require any Split Tunnel configuration. Refer to Automatically managed ranges for details.

If you were relying on a Chrome Enterprise policy workaround (such as LocalNetworkAccessRestrictionsTemporaryOptOut) while your account was still on the legacy CGNAT-based range, refer to Google Chrome restricts access to private hostnames for next steps.

Stream live logs from Cloudflare Tunnel in the dashboard

Real-time Tunnel log streaming is now available in the Cloudflare dashboard under Networking > Tunnels. This brings the same live debugging capability previously only available in the Cloudflare One dashboard, including multi-connector aggregated streaming for high-availability deployments.

Stream live logs from a tunnel in the Cloudflare dashboard

In the tunnel detail view, a new Live logs tab lets you:

  • Stream logs from single or multiple connectors — In highly available deployments with multiple cloudflared replicas, logs from all connectors are merged into a single stream grouped by hostname, making it easy to identify which host machine produced each log entry.
  • Filter by log level, event type, and HTTP method — Narrow the stream to only the events you care about (HTTP, TCP, UDP, or cloudflared internal), at any log level.
Go to Tunnels ↗

For more information, refer to Tunnel observability and Tunnel log streams.

Load Balancing health notifications now resolve automatically

Load Balancing health notifications are now stateful. When a pool or endpoint becomes unhealthy, the notification opens an incident in your alerting tool as before. When that same pool or endpoint recovers, the follow-up notification is matched to the original alert and resolves that incident automatically, so you no longer have to close it by hand.

As part of this change, Load Balancing also sends a notification when a pool or endpoint returns to a healthy state, not only when it becomes unhealthy. Expect to see recovery notifications alongside the failure notifications you already receive.

This applies to your existing Load Balancing health alerts with no configuration change, and it matches the behavior already used by Health Checks notifications.

Two things to keep in mind:

  • A recovery notification is matched to the earlier unhealthy notification for the same pool or endpoint. Renaming an endpoint while an incident is open prevents the match, so that incident stays open until you close it.
  • If a health change cannot be classified as either healthy or unhealthy, the notification is still delivered, but without the state needed to open or resolve an incident.

Refer to Integrate with PagerDuty to learn more about routing Load Balancing health notifications to an incident management tool.

Container image for Cloudflare Mesh

Cloudflare Mesh nodes can now run as Docker containers. The cloudflare/mesh ↗︎ image is available on Docker Hub for Docker Compose, Kubernetes, and any OCI-compatible runtime — no host-level package installation required.

The image supports amd64 and arm64 architectures and includes built-in source NAT so return traffic routes correctly without VPC route table changes.

Deployment patterns

  • Docker Compose — add a cloudflare-mesh service to your compose.yaml and connect your entire stack to a private network.
  • Kubernetes StatefulSet — deploy a standalone Mesh node with persistent registration state.
  • Kubernetes sidecar — add the Mesh image as a sidecar container in a Pod to connect an application to Cloudflare without application changes.
  • CI/CD — pull the image in a pipeline step, join the Mesh, run integration tests against private infrastructure, and tear down. The node disappears when the container exits.

For high availability, run multiple replicas with the same Mesh node token. Cloudflare operates replicas in active-passive mode with automatic failover.

Go to Mesh ↗

For setup steps, runtime configuration, and deployment examples, refer to Run Mesh in Docker / Kubernetes.

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-11",
  "compatibility_flags": [
    "no_nodejs_compat",
    "no_nodejs_compat_v2"
  ]
}
# Set this to today's date
compatibility_date = "2026-10-11"
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.

Control authorization cookies for multi-domain Access applications

Cloudflare Access administrators can now control whether a self-hosted application preemptively sets authorization cookies across its public hostnames.

Previously, Access automatically used eager redirects for applications with five or fewer hostnames. Applications with more than five hostnames received cookies as users visited each hostname. Administrators can now choose either behavior, regardless of the number of hostnames.

The new Eager redirect cookie setting is turned on by default for new applications. After a user signs in, Access redirects the browser through each hostname and sets a CF_Authorization cookie. This supports applications that need to make requests across hostnames before the user visits each one.

For applications with many hostnames, the redirect chain can cause sign-in loops in some browsers. Turn off the setting to issue the cookie only when a user visits each hostname.

To configure the setting, refer to Authorization cookie.

See fallback pool traffic separately in load balancing analytics

Load balancing analytics now shows traffic served by your fallback pool separately from traffic routed to the same pool by normal steering.

Previously, requests were grouped by pool name alone. If the pool acting as your fallback also received traffic through your steering policy, both appeared as a single series, so it was not obvious from the graph whether Cloudflare was still making health-based routing decisions or had fallen back to the pool of last resort. Because the fallback pool ignores health, that distinction matters when you are diagnosing an outage or reviewing how much traffic was shed.

Fallback traffic is now labeled with the pool name followed by (Fallback). A pool named eu-west, for example, is shown as eu-west (Fallback). This label appears as its own entry in:

  • Requests over time, as a separate series in the chart.
  • Pool distribution, as a separate segment.
  • Top endpoints, as a separate card for the pool.

The Latency view and the health event Logs are unchanged.

To see this, go to Traffic > Load Balancing Analytics for a zone. The same breakdown appears in the analytics view for an individual load balancer under Load Balancing at the account level.

Refer to load balancing analytics to learn more.

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.

Cloudflare One Client for Windows (version 2026.7.1210.1)

A new Beta release for the Windows Cloudflare One Client is now available on the beta releases downloads page.

This beta release includes the following changes and improvements:

  • Improved connection reliability: the client now swaps protocol order after repeated connectivity-check failures, which helps when HTTP/3 is blocked after the QUIC handshake.
  • Fixed issue where a certificate error could be incorrectly displayed right after the connection is established.
  • A DNS search domain parsing failure no longer prevents connection.
  • Fixed a MASQUE issue where the tunnel could stall while uploading at a high rate.
  • Fixed being unable to switch organizations when the client was stuck in the "Device not in organization" state.
  • Fixed the Home Screen dropdown popup not anchoring correctly.
  • Fixed a crash during dialog dismissal.
  • Increased tolerance for configurations with a large number of local domain fallback resolver IPs, so DNS resolution behaves correctly even when more fallback resolvers are configured than recommended.
  • Fixed a networking issue where IPv6 multicast routes were being assigned to the WARP tunnel interface.
  • Fixed fatal errors on UI load on Windows 10.
  • Fixed a crash during Windows notification initialization.
  • Made the Windows domain-joined posture check more reliable.
  • Fixed orphaned credentials left behind on multi-user uninstall.
  • A successful re-authentication will cause the device profile to be re-evaluated.
  • Improved dashboard-managed client updates by running the updater only when needed.

Cloudflare One Client for macOS (version 2026.7.1210.1)

A new Beta release for the macOS Cloudflare One Client is now available on the beta releases downloads page.

This beta release includes the following changes and improvements:

  • Improved connection reliability: the client now swaps protocol order after repeated connectivity-check failures, which helps when HTTP/3 is blocked after the QUIC handshake.
  • Fixed issue where a certificate error could be incorrectly displayed right after the connection is established.
  • A DNS search domain parsing failure no longer prevents connection.
  • Fixed a MASQUE issue where the tunnel could stall while uploading at a high rate.
  • Fixed being unable to switch organizations when the client was stuck in the "Device not in organization" state.
  • Fixed the Home Screen dropdown popup not anchoring correctly.
  • Fixed a crash during dialog dismissal.
  • Increased tolerance for configurations with a large number of local domain fallback resolver IPs, so DNS resolution behaves correctly even when more fallback resolvers are configured than recommended.
  • Fixed the WARP client stealing window focus (for example, during reauth).
  • Fixed a client crash when connecting to a captive portal over Wi-Fi.
  • Fixed the system tray icon showing "disconnected" while the UI showed "connected".
  • A successful re-authentication will cause the device profile to be re-evaluated.
  • Improved dashboard-managed client updates by running the updater only when needed.

Static OAuth client credentials for MCP server portals

MCP server portals can now connect to upstream MCP servers that require a pre-registered OAuth client. This supports OAuth providers that do not offer Dynamic Client Registration or have disabled it. This unlocks portal connections to major SaaS providers such as Slack and GitHub, whose MCP servers do not yet support DCR.

When adding an MCP server, administrators can enter the client ID and client secret from an OAuth application registered with the upstream provider. The configuration also supports custom OAuth endpoints, scopes, and the client_secret_post and client_secret_basic token endpoint authentication methods.

Cloudflare stores the client secret encrypted. Users still authenticate to the upstream server with their own accounts when they connect through a portal.

For setup instructions, refer to Configure manual OAuth credentials.

Rotate Stream broadcast keys for live inputs

You can now rotate the broadcast credentials for a Stream live input without changing the live input identifier.

Use key rotation when live input credentials may have been shared with the wrong audience, exposed in client code or a screenshare, or need to be refreshed as part of your security process. Rotating keys revokes the old credentials, disconnects broadcasts using stale credentials, and returns refreshed credentials in the API response.

To rotate keys for a live input, make a POST request to the rotate_keys endpoint:

curl --request POST \
https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/live_inputs/{live_input_identifier}/rotate_keys \
--header "Authorization: Bearer <API_TOKEN>"

Live input responses now also include keysRotatedAt, which indicates when the live input keys were last rotated. This field is omitted for live inputs whose keys have never been rotated.

For endpoint details, refer to Rotate keys for a live input. For usage guidance, refer to Manage live inputs.

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.

Admins can turn on Code Mode by default for MCP portal users

MCP server portals now support four Code Mode policies: Off, Opt-in, On by default, and Enforced. Admins can choose whether Code Mode is unavailable, optional, enabled by default, or required for every session.

Existing portals retain their current behavior. Portals that previously allowed Code Mode use Opt-in, while portals that did not allow Code Mode use Off. New portals also use Opt-in by default.

Clients turn on Code Mode for an Opt-in portal with ?codemode=search_and_execute. The On by default policy lets clients opt out with ?codemode=off, which avoids nested code execution when a client runs its own Code Mode implementation. The Off and Enforced policies ignore client overrides.

The Cloudflare API exposes these policies through the code_mode field:

{
	"code_mode": "default_on"
}

The supported values are off, opt_in, default_on, and enforced. The previous allow_code_mode boolean is deprecated.

For configuration details and client behavior, refer to Code Mode policies.

Control Cloudflare Gateway DNS caching with a maximum TTL setting

You can now set a maximum time-to-live (TTL) for DNS responses returned by Gateway. When an upstream DNS record has a TTL that exceeds the configured maximum, Gateway caps it to your specified value. This ensures that DNS policy changes - such as blocking a newly identified malicious domain - take effect faster across all clients.

The maximum DNS TTL setting in Traffic policies > Traffic settings, showing a numeric input field that accepts values between 60 and 36,000 seconds

The setting is available at two levels:

  • Account level - In Traffic Policies > Traffic Settings, under Proxy and inspection. This sets the default cap for all DNS locations.
  • Per-location - Each DNS location can inherit the account setting, disable the cap, or override it with a custom value.

Two new fields are also available in DNS logs: upstream_record_ttls (the original TTL from the upstream response) and applied_max_ttl (the cap Gateway applied). These appear in the DNS logs column picker and in Logpush datasets.

For more information, refer to Maximum DNS TTL.

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.

Run integration tests against your Worker's production build

Wrangler now provides createTestHarness(), an API for running integration tests against Workers built with Wrangler or the Cloudflare Vite plugin from any Node.js test runner.

The test harness starts a local Worker server with helpers for dispatching requests, resetting storage, and inspecting runtime logs.

This is useful for tests that need to:

For example, this test starts two Workers and mocks an upstream API:

tests/vitest.test.jsjs
import { afterAll, afterEach, beforeAll, test } from "vitest";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { createTestHarness } from "wrangler";

const network = setupServer();
const server = createTestHarness({
	workers: [
		/** Includes `"routes": ["example.com/*"]` */
		{ configPath: "./workers/web/wrangler.jsonc" },
		/** Includes `"routes": ["api.example.com/v1/*"]` */
		{ configPath: "./workers/api/wrangler.jsonc" },
	],
});

beforeAll(async () => {
	network.listen({ onUnhandledRequest: "error" });
	await server.listen();
});

afterEach(async () => {
	network.resetHandlers();
	await server.reset();
});

afterAll(async () => {
	network.close();
	await server.close();
});

test("routes requests to each Worker", async ({ expect }) => {
	// Mock the outbound fetch used to load user profiles.
	network.use(
		http.get("http://identity.example.com/profile/123", ({ params }) => {
			return HttpResponse.json({ id: 123, name: "Ada" });
		}),
	);

	const apiWorkerResponse = await server.fetch(
		"http://api.example.com/v1/users/123",
	);
	expect(await apiWorkerResponse.json()).toEqual({
		id: 123,
		name: "Ada",
	});

	const webWorkerResponse = await server.fetch("http://example.com/users/123");
	expect(await webWorkerResponse.text()).toBe("Profile: Ada");
});
tests/vitest.test.tsts
import { afterAll, afterEach, beforeAll, test } from "vitest";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { createTestHarness } from "wrangler";

const network = setupServer();
const server = createTestHarness({
	workers: [
		/** Includes `"routes": ["example.com/*"]` */
		{ configPath: "./workers/web/wrangler.jsonc" },
		/** Includes `"routes": ["api.example.com/v1/*"]` */
		{ configPath: "./workers/api/wrangler.jsonc" },
	],
});

beforeAll(async () => {
	network.listen({ onUnhandledRequest: "error" });
	await server.listen();
});

afterEach(async () => {
	network.resetHandlers();
	await server.reset();
});

afterAll(async () => {
	network.close();
	await server.close();
});

test("routes requests to each Worker", async ({ expect }) => {
	// Mock the outbound fetch used to load user profiles.
	network.use(
		http.get("http://identity.example.com/profile/123", ({ params }) => {
			return HttpResponse.json({ id: 123, name: "Ada" });
		}),
	);

	const apiWorkerResponse = await server.fetch(
		"http://api.example.com/v1/users/123",
	);
	expect(await apiWorkerResponse.json()).toEqual({
		id: 123,
		name: "Ada",
	});

	const webWorkerResponse = await server.fetch("http://example.com/users/123");
	expect(await webWorkerResponse.text()).toBe("Profile: Ada");
});

Cloudflare now recommends createTestHarness() for integration tests instead of unstable_startWorker() or unstable_dev(). To start a development server programmatically, use the Vite createServer() ↗︎ API with the Cloudflare Vite plugin.

For more information about createTestHarness(), refer to the Integration test harness guide.