Skip to content

Changelog

New updates and improvements at Cloudflare.

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.

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.

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.

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.

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.

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.

Filter Durable Object logs and traces by instance ID

Workers Logs and OpenTelemetry spans for Durable Object requests include the Durable Object instance ID.

Use $workers.durableObjectId to filter logs for a specific instance. Root and child spans include the same ID in cloudflare.durable_object.id.

Query Builder filtering traces by Durable Object instance ID

Use these fields to isolate a specific instance and correlate its logs and traces.

For more information, refer to Durable Objects metrics and analytics and Workers tracing spans and attributes.

Budget alerts now on by default for Pay-as-you-go accounts

We are turning on budget alerts by default for eligible Pay-as-you-go accounts. If your account does not already have a budget alert, Cloudflare will create one for you with a $10 account-level threshold. Your default alert will enable at the turn of your next billing cycle, so it will not fire based on usage you have already incurred.

We are rolling this out in cohorts over the coming weeks, so eligible accounts may see their default alert appear at different times.

The default alert behaves exactly like an alert you would create yourself. When your cumulative usage-based spend this cycle reaches the threshold, you receive an email notification. The alert is informational only. It does not cap your usage or impact your account in any way.

Usage is processed once per day for the prior day's activity, so budget alerts fire the day after the threshold is reached rather than in real time.

Budget alerts only consider spend on usage-based products. Recurring subscription fees, such as the Workers Paid plan fee or other monthly plan charges, are not included in the threshold calculation.

You can change the threshold, add additional alerts, or remove the default alert entirely from Manage Account > Billing > Billable Usage, or from your Notifications settings. If you already configured your own budget alert, nothing changes.

Enterprise contract accounts are not in scope.

For more information, refer to the Budget alerts documentation.

Platforms can now create Temporary Accounts via the Cloudflare API

Platforms can now create temporary preview accounts through the Cloudflare REST API. This lets your platform deploy a live Worker before the user signs in to Cloudflare.

With the Temporary Accounts API, coding agents, AI app builders, and other platforms can build a similar flow for generated Workers and supported resources.

Your platform can keep users in its onboarding flow while they generate, deploy, and test an application. Users do not need an existing Cloudflare account, and your platform does not need write access to one.

Diagram showing an AI agent deploying, verifying, and redeploying a Worker in a temporary account, then a user authenticating and claiming the account to keep its resources

The API returns a claim URL that lets the user make the temporary account and its resources permanent.

Cloudflare Drop ↗︎ demonstrates this preview-and-claim pattern for static sites. Someone can upload a site, test and share it for one hour, then sign in or create an account only when they want to keep it.

This API expands the flow first introduced with wrangler deploy --temporary. Your backend now controls the provisioning and deployment experience directly:

  1. Show Cloudflare's Terms of Service and Privacy Policy in your product, and require the user to accept them.
  2. Request and solve a proof-of-work challenge.
  3. Create a temporary preview account.
  4. Deploy with the returned temporary account ID and API token.
  5. Show the deployed Worker URL and claim URL to the user.
curl "https://api.cloudflare.com/client/v4/provisioning/previews/challenge" \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{}'

curl "https://api.cloudflare.com/client/v4/provisioning/previews" \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{
    "termsOfService": "https://www.cloudflare.com/terms/",
    "privacyPolicy": "https://www.cloudflare.com/privacypolicy/",
    "acceptTermsOfService": "yes",
    "challengeToken": "<CHALLENGE_TOKEN>",
    "solution": {
      "checkpoints": "<BASE64_CHECKPOINTS>"
    }
  }'

For the complete API flow, proof-of-work requirements, supported products, and limits, refer to Claim deployments (temporary accounts). For the background and design goals behind this flow, refer to Temporary Cloudflare Accounts for AI agents ↗︎.

Zero Trust Networks route endpoints and Cloudflare Tunnel connections field retiring on October 5, 2026

On October 5, 2026, two changes take effect across the Zero Trust Networks API and Cloudflare Tunnel API: the CIDR-encoded route endpoints are removed, and tunnel list and get responses no longer include the connections field. If you manage private network routes or read tunnel connection details through the API, cloudflared, Terraform, or another integration, review the changes in the following sections and migrate before the removal date.

Route endpoints

The CIDR-encoded route endpoints are deprecated in favor of the standard, route_id-based endpoints that already exist today. Both sets of endpoints route a private network through Cloudflare Tunnel or Cloudflare Mesh (the API still refers to Mesh nodes as warp_connector) — only the request shape changes.

Deprecated endpoints (removed October 5, 2026):

Replacement endpoints:

What is changing

Deprecated (CIDR-encoded path) Replacement
Route identifier URL-encoded CIDR in the path (/network/{ip_network_encoded}) route_id in the path (network moves to the request body on create)
Create POST .../teamnet/routes/network/{ip_network_encoded} POST .../teamnet/routes with network and tunnel_id in the body
Update PATCH .../teamnet/routes/network/{ip_network_encoded} PATCH .../teamnet/routes/{route_id}
Delete DELETE .../teamnet/routes/network/{ip_network_encoded} DELETE .../teamnet/routes/{route_id}

Action required

  1. Capture each route's route_id by calling List tunnel routes, or read it from the response the first time you create a route with the replacement endpoint.
  2. Update any scripts, backend services, or CI/CD pipelines that call the CIDR-encoded endpoints directly.
  3. If you manage routes with the cloudflared tunnel route ip add | delete commands, upgrade cloudflared to the latest version ↗︎.
  4. If you manage routes with Terraform, make sure you are on a current version of the cloudflare_zero_trust_tunnel_cloudflared_route ↗︎ resource and the Cloudflare Terraform provider ↗︎.
# Before: create a route by URL-encoding the CIDR into the path
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/teamnet/routes/network/172.16.0.0%2F16 \
     -H 'Content-Type: application/json' \
     -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
     -d '{"tunnel_id": "'$TUNNEL_ID'", "comment": "Example comment for this route."}'

# After: create a route with the network in the request body
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/teamnet/routes \
     -H 'Content-Type: application/json' \
     -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
     -d '{"network": "172.16.0.0/16", "tunnel_id": "'$TUNNEL_ID'", "comment": "Example comment for this route."}'

# After: update or delete a route using its route_id
curl -X PATCH https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/teamnet/routes/$ROUTE_ID \
     -H 'Content-Type: application/json' \
     -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
     -d '{"comment": "Updated comment for this route."}'

curl -X DELETE https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/teamnet/routes/$ROUTE_ID \
     -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"

Cloudflare Tunnel and Cloudflare Mesh connections

Starting the same day, the connections array is removed from list and get responses for Cloudflare Tunnel and Cloudflare Mesh nodes (the cfd_tunnel and warp_connector API resources). Query the dedicated connections endpoint instead of reading the field off the tunnel or node object.

This affects:

Action required

Fetch connection details from the tunnel-specific connections endpoint instead of parsing it off the list or get response. For Cloudflare Tunnel, call GET /accounts/{account_id}/cfd_tunnel/{tunnel_id}/connections. For Cloudflare Mesh, call GET /accounts/{account_id}/warp_connector/{tunnel_id}/connections.

# Before: read connections off the tunnel object
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID \
     -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"

# After: query connections directly
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNNEL_ID/connections \
     -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"

Update any dashboards, monitoring scripts, or automation that parses connections from the tunnel list or get response. cloudflared and the Cloudflare Terraform provider do not read this field, so no changes are required on their side for this part of the update.

Why we are making these changes

  • Smaller, faster responses. Cloudflare Tunnel and Cloudflare Mesh nodes with many connections no longer inflate every list and get call — connection detail is only fetched when you need it.
  • A single way to identify a route. Consolidating on route_id removes the need to URL-encode CIDR ranges into the path and matches how every other resource in the Zero Trust Networks API is addressed.
  • Consistency across the API. Both changes align these endpoints with Cloudflare's standard REST conventions for resource identifiers and nested detail endpoints.

To learn more, refer to the Zero Trust Networks API, the Cloudflare Tunnel API, and Routes documentation.

Send npm package dependency metadata with Worker uploads

Wrangler now collects npm package dependency information from your project's package.json during wrangler deploy and wrangler versions upload, and includes it in the upload metadata sent to the Cloudflare API. This data, each dependency's name, declared version range, and exact installed version, enables dependency analytics and future supply chain security features such as vulnerability alerting.

To opt out, set dependencies_instrumentation.enabled to false in your Wrangler configuration file:

{
	"dependencies_instrumentation": {
		"enabled": false
	}
}
[dependencies_instrumentation]
enabled = false

For more details, refer to Wrangler configuration.

Cloudflare Drop

Cloudflare Drop ↗︎ lets you deploy a static site to Cloudflare without requiring a Cloudflare account to get started.

Cloudflare Drag and Drop upload screen for browsing folders or ZIP files

Upload a folder or zip file of static assets (static HTML, CSS, JavaScript, images, and fonts) and get a temporary live preview that stays live for 1 hour. During that window, you can test the site, share the preview URL, or claim the deployment to keep it.

Cloudflare Drag and Drop temporary live preview screen with claim and copy claim link actions

When you are ready to make the deployment permanent, click Claim to sign in or create a Cloudflare account. You can claim the site into an existing Cloudflare account or create a new account for the deployment.

Cloudflare Drag and Drop claim account screen with a countdown before the claim link expires

After claiming the site, you can:

  • Add a domain: Connect an existing domain or purchase a new one for your site.
  • Enable observability: Monitor your site's performance and usage.
  • Enable Markdown for Agents: Allow AI agents to access your site's content in Markdown.
  • Control access: Make your site private and choose who can view it.
Claimed Cloudflare Drag and Drop site setup screen showing options to add a domain, control access, enable observability, and enable Markdown for agents

Simpler runtime types with @cloudflare/workers-types v5

We have released version 5 of @cloudflare/workers-types ↗︎. This release simplifies the package to expose only the latest runtime types.

We still recommend that you generate types for your Worker using wrangler types, but if you want to use the package directly, you can install it with your package manager of choice:

npm i -D @cloudflare/workers-types@latest

The package now exposes two entrypoints:

  • @cloudflare/workers-types reflects the latest compatibility date, using the latest stable compatibility flags.
  • @cloudflare/workers-types/experimental reflects APIs behind experimental compatibility flags.

The dated entrypoints, such as @cloudflare/workers-types/2022-11-30 and @cloudflare/workers-types/2023-03-01, are removed. With runtime type generation in Wrangler v4, you can generate these with the wrangler types command to create types locked to your Worker's compatibility date.

For more information, refer to TypeScript language support.

Work across multiple accounts with Wrangler auth profiles

Wrangler CLI now supports auth profiles: named logins that you scope to specific Cloudflare accounts and switch between automatically, based on the directory you are working in.

A profile is a named OAuth login bound to a directory. Commands run in that directory, and its subdirectories, use the matching account — so you can move between accounts without re-running wrangler login.

Use profiles to keep a separate login for each client when working at an agency, or to separate staging and production into different accounts. Pair a profile with an account_id in your Wrangler configuration file so a command cannot reach the wrong account.

# Create a profile for each account, choosing which accounts it can reach
wrangler auth create client-a
wrangler auth activate client-a ~/clients/client-a

wrangler auth create client-b
wrangler auth activate client-b ~/clients/client-b

Use the --profile flag to run a single command with a specific profile:

wrangler deploy --profile personal

In CI and other automated environments, CLOUDFLARE_API_TOKEN still takes precedence over all profiles.

For setup, the resolution order, and the full command reference, refer to Authentication profiles.

Images binding is now billed per unique transformation

The Images binding is now billed per unique transformation, matching the model already used for URL-based transformations. Repeat requests for the same combination of source image and parameters within the same calendar month are counted only once.

Previously, every call to the binding counted as a separate transformation regardless of whether the image or parameters were unique. With this change, you can call the binding on hot paths without paying for each individual request.

Calls to .info() are no longer billed.

For more information, refer to Images pricing and the Images binding documentation.

Track memory usage for Workers and Durable Objects in the dashboard

You can now monitor how much memory your Workers and Durable Objects consume across invocations with the new Memory Usage chart in the Workers Metrics tab, broken down by P50, P90, P99, and P999 percentiles.

Memory usage chart showing P50, P90, P99, and P999 percentiles with deployment markers

Memory usage measures the V8 isolate memory at the time of each invocation, subject to the 128 MB per-isolate limit — a single isolate can handle many concurrent requests and shares memory across them.

Use the Memory Usage chart to:

  • Track memory trends — Spot gradual increases that may indicate a memory leak before they cause Exceeded Memory errors.
  • Correlate with deployments — Deployment markers on the chart help you identify whether a new version introduced a memory regression.
  • Right-size your Worker — Understand your baseline memory footprint and how much headroom you have before hitting the 128 MB limit.

For Durable Objects, memory usage reflects the in-memory state an object holds (class properties, caches, active WebSocket connections), which persists across invocations until the object is hibernated or evicted. This state is not preserved across eviction, hibernation, or a crash, so persist anything important to storage.

To view memory usage, open the Metrics tab for your Worker ↗︎ or Durable Object namespace ↗︎. For Durable Objects, you can filter by DO ID or name to drill down into memory usage for a specific object. You can also query memory usage programmatically via the GraphQL Analytics API using the workersInvocationsAdaptive dataset — the quantiles.memoryUsageBytesP50 through quantiles.memoryUsageBytesP999 fields return percentile values in bytes.

For local memory debugging, you can also profile memory with DevTools to take heap snapshots and identify specific objects causing high memory usage.

Workers fetch requests now support cf.vary

Workers fetch() requests now support the cf.vary request option. Use cf.vary to control how Cloudflare caches origin responses with a Vary header for a single subrequest.

src/index.jsjs
export default {
	async fetch(request) {
		return fetch(request, {
			cf: {
				vary: {
					default: { action: "bypass" },
					headers: {
						accept: {
							action: "normalize",
							media_types: ["text/html", "application/json"],
						},
						"accept-language": {
							action: "normalize",
							languages: ["en", "fr", "de"],
						},
					},
				},
			},
		});
	},
};
src/index.tsts
export default {
	async fetch(request): Promise<Response> {
		return fetch(request, {
			cf: {
				vary: {
					default: { action: "bypass" },
					headers: {
						accept: {
							action: "normalize",
							media_types: ["text/html", "application/json"],
						},
						"accept-language": {
							action: "normalize",
							languages: ["en", "fr", "de"],
						},
					},
				},
			},
		});
	},
} satisfies ExportedHandler;

For more information, refer to cf.vary.

Test Durable Object eviction with new cloudflare:test helpers

The @cloudflare/vitest-pool-workers package now includes evictDurableObject and evictAllDurableObjects test helpers, exported from cloudflare:test.

These helpers let you test how a Durable Object behaves across evictions, simulating the production lifecycle where an idle Durable Object can be evicted from memory.

For more context, refer to Lifecycle of a Durable Object.

import { evictDurableObject, evictAllDurableObjects } from "cloudflare:test";
import { env } from "cloudflare:workers";

const id = env.COUNTER.idFromName("my-counter");
const stub = env.COUNTER.get(id);

// Evict the Durable Object instance pointed to by a specific stub
await evictDurableObject(stub);

// Close WebSockets instead of hibernating them
await evictDurableObject(stub, { webSockets: "close" });

// Evict all currently-running Durable Objects in evictable namespaces
await evictAllDurableObjects();

These helpers are available in @cloudflare/vitest-pool-workers@0.16.20 and later.

Learn more in the Test APIs reference and the Testing Durable Objects guide.