--- description: Build and deploy serverless applications across Cloudflare's global network with Workers. title: Cloudflare Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cloudflare Workers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) A serverless platform for building, deploying, and scaling apps across [Cloudflare's global network ↗](https://www.cloudflare.com/network/) with a single command — no infrastructure to manage, no complex configuration With Cloudflare Workers, you can expect to: * Deliver fast performance with high reliability anywhere in the world * Build full-stack apps with your framework of choice, including [React](https://developers.cloudflare.com/workers/framework-guides/web-apps/react/), [Vue](https://developers.cloudflare.com/workers/framework-guides/web-apps/vue/), [Svelte](https://developers.cloudflare.com/workers/framework-guides/web-apps/sveltekit/), [Next](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/), [Astro](https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/), [React Router](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/), [and more](https://developers.cloudflare.com/workers/framework-guides/) * Use your preferred language, including [JavaScript](https://developers.cloudflare.com/workers/languages/javascript/), [TypeScript](https://developers.cloudflare.com/workers/languages/typescript/), [Python](https://developers.cloudflare.com/workers/languages/python/), [Rust](https://developers.cloudflare.com/workers/languages/rust/), [and more](https://developers.cloudflare.com/workers/runtime-apis/webassembly/) * Gain deep visibility and insight with built-in [observability](https://developers.cloudflare.com/workers/observability/logs/) * Get started for free and grow with flexible [pricing](https://developers.cloudflare.com/workers/platform/pricing/), affordable at any scale Get started with your first project: [Deploy a template](https://dash.cloudflare.com/?to=/:account/workers-and-pages/templates)[Deploy with Wrangler CLI](https://developers.cloudflare.com/workers/get-started/guide/) --- ## Build with Workers #### Front-end applications Deploy [static assets](https://developers.cloudflare.com/workers/static-assets/) to Cloudflare's [CDN & cache](https://developers.cloudflare.com/cache/) for fast rendering #### Back-end applications Build APIs and connect to data stores with [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) to optimize latency #### Serverless AI inference Run LLMs, generate images, and more with [Workers AI](https://developers.cloudflare.com/workers-ai/) #### Background jobs Schedule [cron jobs](https://developers.cloudflare.com/workers/configuration/cron-triggers/), run durable [Workflows](https://developers.cloudflare.com/workflows/), and integrate with [Queues](https://developers.cloudflare.com/queues/) #### Observability & monitoring Monitor performance, debug issues, and analyze traffic with [real-time logs](https://developers.cloudflare.com/workers/observability/logs/) and [analytics](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/) --- ## Integrate with Workers Connect to external services like databases, APIs, and storage via [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), enabling functionality with just a few lines of code: **Storage** [Durable Objects](https://developers.cloudflare.com/durable-objects/) Scalable stateful storage for real-time coordination. [D1](https://developers.cloudflare.com/d1/) Serverless SQL database built for fast, global queries. [KV](https://developers.cloudflare.com/kv/) Low-latency key-value storage for fast, edge-cached reads. [Queues](https://developers.cloudflare.com/queues/) Guaranteed delivery with no charges for egress bandwidth. [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) Connect to your external database with accelerated queries, cached at the edge. **Compute** [Workers AI](https://developers.cloudflare.com/workers-ai/) Machine learning models powered by serverless GPUs. [Workflows](https://developers.cloudflare.com/workflows/) Durable, long-running operations with automatic retries. [Vectorize](https://developers.cloudflare.com/vectorize/) Vector database for AI-powered semantic search. [R2](https://developers.cloudflare.com/r2/) Zero-egress object storage for cost-efficient data access. [Browser Run](https://developers.cloudflare.com/browser-run/) Programmatic serverless browser instances. **Media** [Cache / CDN](https://developers.cloudflare.com/cache/) Global caching for high-performance, low-latency delivery. [Images](https://developers.cloudflare.com/images/) Streamlined image infrastructure from a single API. --- Want to connect with the Workers community? [Join our Discord ↗](https://discord.cloudflare.com) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/#page","headline":"Overview · Cloudflare Workers docs","description":"Build and deploy serverless applications across Cloudflare's global network with Workers.","url":"https://developers.cloudflare.com/workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Browse code examples and starter templates for Cloudflare Workers. title: Examples image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Examples Last updated May 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Explore the following examples for Workers. [**Single Page App (SPA) shell with bootstrap data**Use HTMLRewriter to inject prefetched bootstrap data into an SPA shell, eliminating client-side data fetching on initial load. Works with Workers Static Assets or an externally hosted SPA.](https://developers.cloudflare.com/workers/examples/spa-shell/) [**Write to Analytics Engine**Write custom analytics events to Workers Analytics Engine for high-cardinality, time-series data.](https://developers.cloudflare.com/workers/examples/analytics-engine/) [**Stream large JSON**Parse and transform large JSON request and response bodies using streaming.](https://developers.cloudflare.com/workers/examples/streaming-json/) [**HTTP Basic Authentication**Shows how to restrict access using the HTTP Basic schema.](https://developers.cloudflare.com/workers/examples/basic-auth/) [**Fetch HTML**Send a request to a remote server, read HTML from the response, and serve that HTML.](https://developers.cloudflare.com/workers/examples/fetch-html/) [**Return small HTML page**Deliver an HTML page from an HTML string directly inside the Worker script.](https://developers.cloudflare.com/workers/examples/return-html/) [**Return JSON**Return JSON directly from a Worker script, useful for building APIs and middleware.](https://developers.cloudflare.com/workers/examples/return-json/) [**Sign requests**Verify a signed request using the HMAC and SHA-256 algorithms or return a 403.](https://developers.cloudflare.com/workers/examples/signing-requests/) [**Stream OpenAI API Responses**Use the OpenAI v4 SDK to stream responses from OpenAI.](https://developers.cloudflare.com/workers/examples/openai-sdk-streaming/) [**Using timingSafeEqual**Protect against timing attacks by safely comparing values using timingSafeEqual.](https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/) [**Turnstile with Workers**Inject Turnstile implicitly into HTML elements using the HTMLRewriter runtime API.](https://developers.cloudflare.com/workers/examples/turnstile-html-rewriter/) [**Custom Domain with Images**Set up custom domain for Images using a Worker or serve images using a prefix path and Cloudflare registered domain.](https://developers.cloudflare.com/workers/examples/images-workers/) [**103 Early Hints**Allow a client to request static assets while waiting for the HTML response.](https://developers.cloudflare.com/workers/examples/103-early-hints/) [**Cache Tags using Workers**Send Additional Cache Tags using Workers](https://developers.cloudflare.com/workers/examples/cache-tags/) [**Accessing the Cloudflare Object**Access custom Cloudflare properties and control how Cloudflare features are applied to every request.](https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object/) [**Aggregate requests**Send two GET request to two urls and aggregates the responses into one response.](https://developers.cloudflare.com/workers/examples/aggregate-requests/) [**Block on TLS**Inspects the incoming request's TLS version and blocks if under TLSv1.2.](https://developers.cloudflare.com/workers/examples/block-on-tls/) [**Bulk redirects**Redirect requests to certain URLs based on a mapped object to the request's URL.](https://developers.cloudflare.com/workers/examples/bulk-redirects/) [**Cache POST requests**Cache POST requests using the Cache API.](https://developers.cloudflare.com/workers/examples/cache-post-request/) [**Conditional response**Return a response based on the incoming request's URL, HTTP method, User Agent, IP address, ASN or device type.](https://developers.cloudflare.com/workers/examples/conditional-response/) [**Cookie parsing**Given the cookie name, get the value of a cookie. You can also use cookies for A/B testing.](https://developers.cloudflare.com/workers/examples/extract-cookie-value/) [**Fetch JSON**Send a GET request and read in JSON from the response. Use to fetch external data.](https://developers.cloudflare.com/workers/examples/fetch-json/) [**Geolocation: Custom Styling**Personalize website styling based on localized user time.](https://developers.cloudflare.com/workers/examples/geolocation-custom-styling/) [**Geolocation: Hello World**Get all geolocation data fields and display them in HTML.](https://developers.cloudflare.com/workers/examples/geolocation-hello-world/) [**Post JSON**Send a POST request with JSON data. Use to share data with external servers.](https://developers.cloudflare.com/workers/examples/post-json/) [**Redirect**Redirect requests from one URL to another or from one set of URLs to another set.](https://developers.cloudflare.com/workers/examples/redirect/) [**Rewrite links**Rewrite URL links in HTML using the HTMLRewriter. This is useful for JAMstack websites.](https://developers.cloudflare.com/workers/examples/rewrite-links/) [**Set security headers**Set common security headers (X-XSS-Protection, X-Frame-Options, X-Content-Type-Options, Permissions-Policy, Referrer-Policy, Strict-Transport-Security, Content-Security-Policy).](https://developers.cloudflare.com/workers/examples/security-headers/) [**Multiple Cron Triggers**Set multiple Cron Triggers on three different schedules.](https://developers.cloudflare.com/workers/examples/multiple-cron-triggers/) [**Setting Cron Triggers**Set a Cron Trigger for your Worker.](https://developers.cloudflare.com/workers/examples/cron-trigger/) [**Using the WebSockets API**Use the WebSockets API to communicate in real time with your Cloudflare Workers.](https://developers.cloudflare.com/workers/examples/websockets/) [**Geolocation: Weather application**Fetch weather data from an API using the user's geolocation data.](https://developers.cloudflare.com/workers/examples/geolocation-app-weather/) [**A/B testing with same-URL direct access**Set up an A/B test by controlling what response is served based on cookies. This version supports passing the request through to test and control on the origin, bypassing random assignment.](https://developers.cloudflare.com/workers/examples/ab-testing/) [**Alter headers**Example of how to add, change, or delete headers sent in a request or returned in a response.](https://developers.cloudflare.com/workers/examples/alter-headers/) [**Auth with headers**Allow or deny a request based on a known pre-shared key in a header. This is not meant to replace the WebCrypto API.](https://developers.cloudflare.com/workers/examples/auth-with-headers/) [**Bulk origin override**Resolve requests to your domain to a set of proxy third-party origin URLs.](https://developers.cloudflare.com/workers/examples/bulk-origin-proxy/) [**Using the Cache API**Use the Cache API to store responses in Cloudflare's cache.](https://developers.cloudflare.com/workers/examples/cache-api/) [**Cache using fetch**Determine how to cache a resource by setting TTLs, custom cache keys, and cache headers in a fetch request.](https://developers.cloudflare.com/workers/examples/cache-using-fetch/) [**CORS header proxy**Add the necessary CORS headers to a third party API response.](https://developers.cloudflare.com/workers/examples/cors-header-proxy/) [**Country code redirect**Redirect a response based on the country code in the header of a visitor.](https://developers.cloudflare.com/workers/examples/country-code-redirect/) [**Data loss prevention**Protect sensitive data to prevent data loss, and send alerts to a webhooks server in the event of a data breach.](https://developers.cloudflare.com/workers/examples/data-loss-prevention/) [**Debugging logs**Send debugging information in an errored response to a logging service.](https://developers.cloudflare.com/workers/examples/debugging-logs/) [**Hot-link protection**Block other websites from linking to your content. This is useful for protecting images.](https://developers.cloudflare.com/workers/examples/hot-link-protection/) [**Logging headers to console**Examine the contents of a Headers object by logging to console with a Map.](https://developers.cloudflare.com/workers/examples/logging-headers/) [**Modify request property**Create a modified request with edited properties based off of an incoming request.](https://developers.cloudflare.com/workers/examples/modify-request-property/) [**Modify response**Fetch and modify response properties which are immutable by creating a copy first.](https://developers.cloudflare.com/workers/examples/modify-response/) [**Read POST**Serve an HTML form, then read POST requests. Use also to read JSON or POST data from an incoming request.](https://developers.cloudflare.com/workers/examples/read-post/) [**Respond with another site**Respond to the Worker request with the response from another website (example.com in this example).](https://developers.cloudflare.com/workers/examples/respond-with-another-site/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/examples/#page","headline":"Examples · Cloudflare Workers docs","description":"Browse code examples and starter templates for Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/examples/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Allow a client to request static assets while waiting for the HTML response. title: 103 Early Hints image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # 103 Early Hints Allow a client to request static assets while waiting for the HTML response. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/103-early-hints/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/103-early-hints) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. `103` Early Hints is an HTTP status code designed to speed up content delivery. When enabled, Cloudflare can cache the `Link` headers marked with preload and/or preconnect from HTML pages and serve them in a `103` Early Hints response before reaching the origin server. Browsers can use these hints to fetch linked assets while waiting for the origin’s final response, dramatically improving page load speeds. To ensure Early Hints are enabled on your zone: 1. In the Cloudflare dashboard, go to the **Speed settings** page. [Go to **Settings** ↗](https://dash.cloudflare.com/?to=/:account/:zone/speed/optimization) 2. Go to **Content Optimization**. 3. Enable the **Early Hints** toggle to on. You can return `Link` headers from a Worker running on your zone to speed up your page load times. ```js const CSS = "body { color: red; }"; const HTML = ` Early Hints test

Early Hints test page

`; export default { async fetch(req) { // If request is for test.css, serve the raw CSS if (/test\.css$/.test(req.url)) { return new Response(CSS, { headers: { "content-type": "text/css", }, }); } else { // Serve raw HTML using Early Hints for the CSS file return new Response(HTML, { headers: { "content-type": "text/html", link: "; rel=preload; as=style", }, }); } }, }; ``` ```js const CSS = "body { color: red; }"; const HTML = ` Early Hints test

Early Hints test page

`; export default { async fetch(req): Promise { // If request is for test.css, serve the raw CSS if (/test\.css$/.test(req.url)) { return new Response(CSS, { headers: { "content-type": "text/css", }, }); } else { // Serve raw HTML using Early Hints for the CSS file return new Response(HTML, { headers: { "content-type": "text/html", link: "; rel=preload; as=style", }, }); } }, } satisfies ExportedHandler; ``` ```py import re from workers import Response, WorkerEntrypoint CSS = "body { color: red; }" HTML = """ Early Hints test

Early Hints test page

""" class Default(WorkerEntrypoint): async def fetch(self, request): if re.search("test.css", request.url): headers = {"content-type": "text/css"} return Response(CSS, headers=headers) else: headers = {"content-type": "text/html","link": "; rel=preload; as=style"} return Response(HTML, headers=headers) ``` ```ts import { Hono } from "hono"; const app = new Hono(); const CSS = "body { color: red; }"; const HTML = ` Early Hints test

Early Hints test page

`; // Serve CSS file app.get("/test.css", (c) => { return c.body(CSS, { headers: { "content-type": "text/css", }, }); }); // Serve HTML with early hints app.get("*", (c) => { return c.html(HTML, { headers: { link: "; rel=preload; as=style", }, }); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/103-early-hints/#page","headline":"103 Early Hints · Cloudflare Workers docs","description":"Allow a client to request static assets while waiting for the HTML response.","url":"https://developers.cloudflare.com/workers/examples/103-early-hints/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Headers","JavaScript","TypeScript","Python"]} ``` --- --- description: Set up an A/B test by controlling what response is served based on cookies. This version supports passing the request through to test and control on the origin, bypassing random assignment. title: A/B testing with same-URL direct access image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # A/B testing with same-URL direct access Set up an A/B test by controlling what response is served based on cookies. This version supports passing the request through to test and control on the origin, bypassing random assignment. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/ab-testing/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ```js const NAME = "myExampleWorkersABTest"; export default { async fetch(req) { const url = new URL(req.url); // Enable Passthrough to allow direct access to control and test routes. if (url.pathname.startsWith("/control") || url.pathname.startsWith("/test")) return fetch(req); // Determine which group this requester is in. const cookie = req.headers.get("cookie"); if (cookie && cookie.includes(`${NAME}=control`)) { url.pathname = "/control" + url.pathname; } else if (cookie && cookie.includes(`${NAME}=test`)) { url.pathname = "/test" + url.pathname; } else { // If there is no cookie, this is a new client. Choose a group and set the cookie. const group = Math.random() < 0.5 ? "test" : "control"; // 50/50 split if (group === "control") { url.pathname = "/control" + url.pathname; } else { url.pathname = "/test" + url.pathname; } // Reconstruct response to avoid immutability let res = await fetch(url); res = new Response(res.body, res); // Set cookie to enable persistent A/B sessions. res.headers.append("Set-Cookie", `${NAME}=${group}; path=/`); return res; } return fetch(url); }, }; ``` ```ts const NAME = "myExampleWorkersABTest"; export default { async fetch(req): Promise { const url = new URL(req.url); // Enable Passthrough to allow direct access to control and test routes. if (url.pathname.startsWith("/control") || url.pathname.startsWith("/test")) return fetch(req); // Determine which group this requester is in. const cookie = req.headers.get("cookie"); if (cookie && cookie.includes(`${NAME}=control`)) { url.pathname = "/control" + url.pathname; } else if (cookie && cookie.includes(`${NAME}=test`)) { url.pathname = "/test" + url.pathname; } else { // If there is no cookie, this is a new client. Choose a group and set the cookie. const group = Math.random() < 0.5 ? "test" : "control"; // 50/50 split if (group === "control") { url.pathname = "/control" + url.pathname; } else { url.pathname = "/test" + url.pathname; } // Reconstruct response to avoid immutability let res = await fetch(url); res = new Response(res.body, res); // Set cookie to enable persistent A/B sessions. res.headers.append("Set-Cookie", `${NAME}=${group}; path=/`); return res; } return fetch(url); }, } satisfies ExportedHandler; ``` ```py import random from urllib.parse import urlparse, urlunparse from workers import Response, fetch, WorkerEntrypoint NAME = "myExampleWorkersABTest" class Default(WorkerEntrypoint): async def fetch(self, request): url = urlparse(request.url) # Uncomment below when testing locally # url = url._replace(netloc="example.com") if "localhost" in url.netloc else url # Enable Passthrough to allow direct access to control and test routes. if url.path.startswith("/control") or url.path.startswith("/test"): return fetch(urlunparse(url)) # Determine which group this requester is in. cookie = request.headers.get("cookie") if cookie and f'{NAME}=control' in cookie: url = url._replace(path="/control" + url.path) elif cookie and f'{NAME}=test' in cookie: url = url._replace(path="/test" + url.path) else: # If there is no cookie, this is a new client. Choose a group and set the cookie. group = "test" if random.random() < 0.5 else "control" if group == "control": url = url._replace(path="/control" + url.path) else: url = url._replace(path="/test" + url.path) # Reconstruct response to avoid immutability res = await fetch(urlunparse(url)) headers = dict(res.headers) headers["Set-Cookie"] = f'{NAME}={group}; path=/' return Response(res.body, headers=headers) return fetch(urlunparse(url)) ``` ```ts import { Hono } from "hono"; import { getCookie, setCookie } from "hono/cookie"; const app = new Hono(); const NAME = "myExampleWorkersABTest"; // Enable passthrough to allow direct access to control and test routes app.all("/control/*", (c) => fetch(c.req.raw)); app.all("/test/*", (c) => fetch(c.req.raw)); // Middleware to handle A/B testing logic app.use("*", async (c) => { const url = new URL(c.req.url); // Determine which group this requester is in const abTestCookie = getCookie(c, NAME); if (abTestCookie === "control") { // User is in control group url.pathname = "/control" + c.req.path; } else if (abTestCookie === "test") { // User is in test group url.pathname = "/test" + c.req.path; } else { // If there is no cookie, this is a new client // Choose a group and set the cookie (50/50 split) const group = Math.random() < 0.5 ? "test" : "control"; // Update URL path based on assigned group if (group === "control") { url.pathname = "/control" + c.req.path; } else { url.pathname = "/test" + c.req.path; } // Set cookie to enable persistent A/B sessions setCookie(c, NAME, group, { path: "/", }); } const res = await fetch(url); return c.body(res.body, res); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/ab-testing/#page","headline":"A/B testing with same-URL direct access · Cloudflare Workers docs","description":"Set up an A/B test by controlling what response is served based on cookies. This version supports passing the request through to test and control on the origin, bypassing random assignment.","url":"https://developers.cloudflare.com/workers/examples/ab-testing/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Access custom Cloudflare properties and control how Cloudflare features are applied to every request. title: Accessing the Cloudflare Object image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Accessing the Cloudflare Object Access custom Cloudflare properties and control how Cloudflare features are applied to every request. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/accessing-the-cloudflare-object) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(req) { const data = req.cf !== undefined ? req.cf : { error: "The `cf` object is not available inside the preview." }; return new Response(JSON.stringify(data, null, 2), { headers: { "content-type": "application/json;charset=UTF-8", }, }); }, }; ``` ```ts export default { async fetch(req): Promise { const data = req.cf !== undefined ? req.cf : { error: "The `cf` object is not available inside the preview." }; return new Response(JSON.stringify(data, null, 2), { headers: { "content-type": "application/json;charset=UTF-8", }, }); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.get("*", async (c) => { // Access the raw request to get the cf object const req = c.req.raw; // Check if the cf object is available const data = req.cf !== undefined ? req.cf : { error: "The `cf` object is not available inside the preview." }; // Return the data formatted with 2-space indentation return c.json(data); }); export default app; ``` ```py import json from workers import Response, WorkerEntrypoint from js import JSON class Default(WorkerEntrypoint): async def fetch(self, request): error = json.dumps({ "error": "The `cf` object is not available inside the preview." }) data = request.cf if request.cf is not None else error headers = {"content-type":"application/json"} return Response(JSON.stringify(data, None, 2), headers=headers) ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object/#page","headline":"Accessing the Cloudflare Object · Cloudflare Workers docs","description":"Access custom Cloudflare properties and control how Cloudflare features are applied to every request.","url":"https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Send two GET request to two urls and aggregates the responses into one response. title: Aggregate requests image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Aggregate requests Send two GET request to two urls and aggregates the responses into one response. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/aggregate-requests/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/aggregate-requests) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { // someHost is set up to return JSON responses const someHost = "https://jsonplaceholder.typicode.com"; const url1 = someHost + "/todos/1"; const url2 = someHost + "/todos/2"; const responses = await Promise.all([fetch(url1), fetch(url2)]); const results = await Promise.all(responses.map((r) => r.json())); const options = { headers: { "content-type": "application/json;charset=UTF-8" }, }; return new Response(JSON.stringify(results), options); }, }; ``` ```ts export default { async fetch(request) { // someHost is set up to return JSON responses const someHost = "https://jsonplaceholder.typicode.com"; const url1 = someHost + "/todos/1"; const url2 = someHost + "/todos/2"; const responses = await Promise.all([fetch(url1), fetch(url2)]); const results = await Promise.all(responses.map((r) => r.json())); const options = { headers: { "content-type": "application/json;charset=UTF-8" }, }; return new Response(JSON.stringify(results), options); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.get("*", async (c) => { // someHost is set up to return JSON responses const someHost = "https://jsonplaceholder.typicode.com"; const url1 = someHost + "/todos/1"; const url2 = someHost + "/todos/2"; // Fetch both URLs concurrently const responses = await Promise.all([fetch(url1), fetch(url2)]); // Parse JSON responses concurrently const results = await Promise.all(responses.map((r) => r.json())); // Return aggregated results return c.json(results); }); export default app; ``` ```py from workers import Response, fetch, WorkerEntrypoint import asyncio import json class Default(WorkerEntrypoint): async def fetch(self, request): # some_host is set up to return JSON responses some_host = "https://jsonplaceholder.typicode.com" url1 = some_host + "/todos/1" url2 = some_host + "/todos/2" responses = await asyncio.gather(fetch(url1), fetch(url2)) results = await asyncio.gather(*(r.json() for r in responses)) headers = {"content-type": "application/json;charset=UTF-8"} return Response.json(results, headers=headers) ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/aggregate-requests/#page","headline":"Aggregate requests · Cloudflare Workers docs","description":"Send two GET request to two urls and aggregates the responses into one response.","url":"https://developers.cloudflare.com/workers/examples/aggregate-requests/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Example of how to add, change, or delete headers sent in a request or returned in a response. title: Alter headers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Alter headers Example of how to add, change, or delete headers sent in a request or returned in a response. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/alter-headers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/alter-headers) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const response = await fetch("https://example.com"); // Clone the response so that it's no longer immutable const newResponse = new Response(response.body, response); // Add a custom header with a value newResponse.headers.append( "x-workers-hello", "Hello from Cloudflare Workers", ); // Delete headers newResponse.headers.delete("x-header-to-delete"); newResponse.headers.delete("x-header2-to-delete"); // Adjust the value for an existing header newResponse.headers.set("x-header-to-change", "NewValue"); return newResponse; }, }; ``` ```ts export default { async fetch(request): Promise { const response = await fetch(request); // Clone the response so that it's no longer immutable const newResponse = new Response(response.body, response); // Add a custom header with a value newResponse.headers.append( "x-workers-hello", "Hello from Cloudflare Workers", ); // Delete headers newResponse.headers.delete("x-header-to-delete"); newResponse.headers.delete("x-header2-to-delete"); // Adjust the value for an existing header newResponse.headers.set("x-header-to-change", "NewValue"); return newResponse; }, } satisfies ExportedHandler; ``` ```py from workers import Response, fetch, WorkerEntrypoint class Default(WorkerEntrypoint): async def fetch(self, request): response = await fetch("https://example.com") # Grab the response headers so they can be modified new_headers = response.headers # Add a custom header with a value new_headers["x-workers-hello"] = "Hello from Cloudflare Workers" # Delete headers if "x-header-to-delete" in new_headers: del new_headers["x-header-to-delete"] if "x-header2-to-delete" in new_headers: del new_headers["x-header2-to-delete"] # Adjust the value for an existing header new_headers["x-header-to-change"] = "NewValue" return Response(response.body, headers=new_headers) ``` ```ts import { Hono } from 'hono'; const app = new Hono(); app.use('*', async (c, next) => { // Process the request with the next middleware/handler await next(); // After the response is generated, we can modify its headers // Add a custom header with a value c.res.headers.append( "x-workers-hello", "Hello from Cloudflare Workers with Hono" ); // Delete headers c.res.headers.delete("x-header-to-delete"); c.res.headers.delete("x-header2-to-delete"); // Adjust the value for an existing header c.res.headers.set("x-header-to-change", "NewValue"); }); app.get('*', async (c) => { // Fetch content from example.com const response = await fetch("https://example.com"); // Return the response body with original headers // (our middleware will modify the headers before sending) return new Response(response.body, { headers: response.headers }); }); export default app; ``` You can also use the [custom-headers-example template ↗](https://github.com/kristianfreeman/custom-headers-example) to deploy this code to your custom domain. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/alter-headers/#page","headline":"Alter headers · Cloudflare Workers docs","description":"Example of how to add, change, or delete headers sent in a request or returned in a response.","url":"https://developers.cloudflare.com/workers/examples/alter-headers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Headers","Middleware","JavaScript","TypeScript","Python"]} ``` --- --- description: Write custom analytics events to Workers Analytics Engine for high-cardinality, time-series data. title: Write to Analytics Engine image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Write to Analytics Engine Write custom analytics events to Workers Analytics Engine. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/analytics-engine/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) provides time-series analytics at scale. Use it to track custom metrics, build usage-based billing, or understand service health on a per-customer basis. Unlike logs, Analytics Engine is designed for aggregated queries over high-cardinality data. Writes are non-blocking and do not impact request latency. ## Configure the binding Add an Analytics Engine dataset binding to your Wrangler configuration file. The dataset is created automatically when you first write to it. ```jsonc { "analytics_engine_datasets": [ { "binding": "ANALYTICS", "dataset": "my_dataset", }, ], } ``` ```toml [[analytics_engine_datasets]] binding = "ANALYTICS" dataset = "my_dataset" ``` ## Write data points ```js export default { async fetch(request, env) { const url = new URL(request.url); // Write a page view event env.ANALYTICS.writeDataPoint({ blobs: [ url.pathname, request.headers.get("cf-connecting-country") ?? "unknown", ], doubles: [1], // Count indexes: [url.hostname], // Sampling key }); // Write a response timing event const start = Date.now(); const response = await fetch(request); const duration = Date.now() - start; env.ANALYTICS.writeDataPoint({ blobs: [url.pathname, response.status.toString()], doubles: [duration], indexes: [url.hostname], }); // Writes are non-blocking - no need to await or use waitUntil() return response; }, }; ``` ```ts interface Env { ANALYTICS: AnalyticsEngineDataset; } export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // Write a page view event env.ANALYTICS.writeDataPoint({ blobs: [ url.pathname, request.headers.get("cf-connecting-country") ?? "unknown", ], doubles: [1], // Count indexes: [url.hostname], // Sampling key }); // Write a response timing event const start = Date.now(); const response = await fetch(request); const duration = Date.now() - start; env.ANALYTICS.writeDataPoint({ blobs: [url.pathname, response.status.toString()], doubles: [duration], indexes: [url.hostname], }); // Writes are non-blocking - no need to await or use waitUntil() return response; }, }; ``` ## Data point structure Each data point consists of: * **blobs** (strings) - Dimensions for grouping and filtering. Use for paths, regions, status codes, or customer IDs. * **doubles** (numbers) - Numeric values to record, such as counts, durations, or sizes. * **indexes** (strings) - A single string used as the [sampling key](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/#sampling). Group related events under the same index. ## Query your data Query your data using the [SQL API](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/): ```bash curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/analytics_engine/sql" \ --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ --data "SELECT blob1 AS path, SUM(_sample_interval) AS views FROM my_dataset WHERE timestamp > NOW() - INTERVAL '1' HOUR GROUP BY path ORDER BY views DESC LIMIT 10" ``` ## Related resources * [Analytics Engine documentation](https://developers.cloudflare.com/analytics/analytics-engine/) \- Full reference for Workers Analytics Engine. * [SQL API reference](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/) \- Query syntax and available functions. * [Grafana integration](https://developers.cloudflare.com/analytics/analytics-engine/grafana/) \- Visualize Analytics Engine data in Grafana. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/analytics-engine/#page","headline":"Write to Analytics Engine · Cloudflare Workers docs","description":"Write custom analytics events to Workers Analytics Engine for high-cardinality, time-series data.","url":"https://developers.cloudflare.com/workers/examples/analytics-engine/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Allow or deny a request based on a known pre-shared key in a header. This is not meant to replace the WebCrypto API. title: Auth with headers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Auth with headers Allow or deny a request based on a known pre-shared key in a header. This is not meant to replace the WebCrypto API. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/auth-with-headers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Caution when using in production The example code contains a generic header key and value of `X-Custom-PSK` and `mypresharedkey`. To best protect your resources, change the header key and value in the Workers editor before saving your code. ```js export default { async fetch(request) { /** * @param {string} PRESHARED_AUTH_HEADER_KEY Custom header to check for key * @param {string} PRESHARED_AUTH_HEADER_VALUE Hard coded key value */ const PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK"; const PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey"; const psk = request.headers.get(PRESHARED_AUTH_HEADER_KEY); if (psk === PRESHARED_AUTH_HEADER_VALUE) { // Correct preshared header key supplied. Fetch request from origin. return fetch(request); } // Incorrect key supplied. Reject the request. return new Response("Sorry, you have supplied an invalid key.", { status: 403, }); }, }; ``` ```ts export default { async fetch(request): Promise { /** * @param {string} PRESHARED_AUTH_HEADER_KEY Custom header to check for key * @param {string} PRESHARED_AUTH_HEADER_VALUE Hard coded key value */ const PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK"; const PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey"; const psk = request.headers.get(PRESHARED_AUTH_HEADER_KEY); if (psk === PRESHARED_AUTH_HEADER_VALUE) { // Correct preshared header key supplied. Fetch request from origin. return fetch(request); } // Incorrect key supplied. Reject the request. return new Response("Sorry, you have supplied an invalid key.", { status: 403, }); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): async def fetch(self, request): PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK" PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey" psk = request.headers[PRESHARED_AUTH_HEADER_KEY] if psk == PRESHARED_AUTH_HEADER_VALUE: # Correct preshared header key supplied. Fetch request from origin. return fetch(request) # Incorrect key supplied. Reject the request. return Response("Sorry, you have supplied an invalid key.", status=403) ``` ```ts import { Hono } from 'hono'; const app = new Hono(); // Add authentication middleware app.use('*', async (c, next) => { /** * Define authentication constants */ const PRESHARED_AUTH_HEADER_KEY = "X-Custom-PSK"; const PRESHARED_AUTH_HEADER_VALUE = "mypresharedkey"; // Get the pre-shared key from the request header const psk = c.req.header(PRESHARED_AUTH_HEADER_KEY); if (psk === PRESHARED_AUTH_HEADER_VALUE) { // Correct preshared header key supplied. Continue to the next handler. await next(); } else { // Incorrect key supplied. Reject the request. return c.text("Sorry, you have supplied an invalid key.", 403); } }); // Handle all authenticated requests by passing through to origin app.all('*', async (c) => { return fetch(c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/auth-with-headers/#page","headline":"Auth with headers · Cloudflare Workers docs","description":"Allow or deny a request based on a known pre-shared key in a header. This is not meant to replace the WebCrypto API.","url":"https://developers.cloudflare.com/workers/examples/auth-with-headers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Authentication","WebCrypto","JavaScript","TypeScript","Python"]} ``` --- --- description: Shows how to restrict access using the HTTP Basic schema. title: HTTP Basic Authentication image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # HTTP Basic Authentication Shows how to restrict access using the HTTP Basic schema. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/basic-auth/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note This example Worker makes use of the [Node.js Buffer API](https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/), which is available as part of the Workers runtime [Node.js compatibility mode](https://developers.cloudflare.com/workers/runtime-apis/nodejs/). To run this Worker, you will need to [enable the nodejs\_compat compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Caution when using in production This code is provided as a sample, and is not suitable for production use. Basic Authentication sends credentials unencrypted, and must be used with an HTTPS connection to be considered secure. For a production-ready authentication system, consider using [Cloudflare Access ↗](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/self-hosted-public-app/). ```js /** * Shows how to restrict access using the HTTP Basic schema. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication * @see https://tools.ietf.org/html/rfc7617 * */ import { Buffer } from "node:buffer"; const encoder = new TextEncoder(); /** * Protect against timing attacks by safely comparing values using `timingSafeEqual`. * Refer to https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#timingsafeequal for more details * @param {string} a * @param {string} b * @returns {boolean} */ function timingSafeEqual(a, b) { const aBytes = encoder.encode(a); const bBytes = encoder.encode(b); // Do not return early when lengths differ — that leaks the secret's // length through timing. Compare against self and negate instead. if (aBytes.byteLength !== bBytes.byteLength) { return !crypto.subtle.timingSafeEqual(aBytes, aBytes); } return crypto.subtle.timingSafeEqual(aBytes, bBytes); } export default { /** * * @param {Request} request * @param {{PASSWORD: string}} env * @returns */ async fetch(request, env) { const BASIC_USER = "admin"; // You will need an admin password. This should be // attached to your Worker as an encrypted secret. // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ const BASIC_PASS = env.PASSWORD ?? "password"; const url = new URL(request.url); switch (url.pathname) { case "/": return new Response("Anyone can access the homepage."); case "/logout": // Invalidate the "Authorization" header by returning a HTTP 401. // We do not send a "WWW-Authenticate" header, as this would trigger // a popup in the browser, immediately asking for credentials again. return new Response("Logged out.", { status: 401 }); case "/admin": { // The "Authorization" header is sent when authenticated. const authorization = request.headers.get("Authorization"); if (!authorization) { return new Response("You need to login.", { status: 401, headers: { // Prompts the user for credentials. "WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"', }, }); } const [scheme, encoded] = authorization.split(" "); // The Authorization header must start with Basic, followed by a space. if (!encoded || scheme !== "Basic") { return new Response("Malformed authorization header.", { status: 400, }); } const credentials = Buffer.from(encoded, "base64").toString(); // The username & password are split by the first colon. //=> example: "username:password" const index = credentials.indexOf(":"); const user = credentials.substring(0, index); const pass = credentials.substring(index + 1); if ( !timingSafeEqual(BASIC_USER, user) || !timingSafeEqual(BASIC_PASS, pass) ) { return new Response("You need to login.", { status: 401, headers: { // Prompts the user for credentials. "WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"', }, }); } return new Response("🎉 You have private access!", { status: 200, headers: { "Cache-Control": "no-store", }, }); } } return new Response("Not Found.", { status: 404 }); }, }; ``` ```ts /** * Shows how to restrict access using the HTTP Basic schema. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication * @see https://tools.ietf.org/html/rfc7617 * */ import { Buffer } from "node:buffer"; const encoder = new TextEncoder(); /** * Protect against timing attacks by safely comparing values using `timingSafeEqual`. * Refer to https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#timingsafeequal for more details */ function timingSafeEqual(a: string, b: string) { const aBytes = encoder.encode(a); const bBytes = encoder.encode(b); // Do not return early when lengths differ — that leaks the secret's // length through timing. Compare against self and negate instead. if (aBytes.byteLength !== bBytes.byteLength) { return !crypto.subtle.timingSafeEqual(aBytes, aBytes); } return crypto.subtle.timingSafeEqual(aBytes, bBytes); } interface Env { PASSWORD: string; } export default { async fetch(request, env): Promise { const BASIC_USER = "admin"; // You will need an admin password. This should be // attached to your Worker as an encrypted secret. // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ const BASIC_PASS = env.PASSWORD ?? "password"; const url = new URL(request.url); switch (url.pathname) { case "/": return new Response("Anyone can access the homepage."); case "/logout": // Invalidate the "Authorization" header by returning a HTTP 401. // We do not send a "WWW-Authenticate" header, as this would trigger // a popup in the browser, immediately asking for credentials again. return new Response("Logged out.", { status: 401 }); case "/admin": { // The "Authorization" header is sent when authenticated. const authorization = request.headers.get("Authorization"); if (!authorization) { return new Response("You need to login.", { status: 401, headers: { // Prompts the user for credentials. "WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"', }, }); } const [scheme, encoded] = authorization.split(" "); // The Authorization header must start with Basic, followed by a space. if (!encoded || scheme !== "Basic") { return new Response("Malformed authorization header.", { status: 400, }); } const credentials = Buffer.from(encoded, "base64").toString(); // The username and password are split by the first colon. //=> example: "username:password" const index = credentials.indexOf(":"); const user = credentials.substring(0, index); const pass = credentials.substring(index + 1); if ( !timingSafeEqual(BASIC_USER, user) || !timingSafeEqual(BASIC_PASS, pass) ) { return new Response("You need to login.", { status: 401, headers: { // Prompts the user for credentials. "WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"', }, }); } return new Response("🎉 You have private access!", { status: 200, headers: { "Cache-Control": "no-store", }, }); } } return new Response("Not Found.", { status: 404 }); }, } satisfies ExportedHandler; ``` ```rs use base64::prelude::*; use worker::*; #[event(fetch)] async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { let basic_user = "admin"; // You will need an admin password. This should be // attached to your Worker as an encrypted secret. // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ let basic_pass = match env.secret("PASSWORD") { Ok(s) => s.to_string(), Err(_) => "password".to_string(), }; let url = req.url()?; match url.path() { "/" => Response::ok("Anyone can access the homepage."), // Invalidate the "Authorization" header by returning a HTTP 401. // We do not send a "WWW-Authenticate" header, as this would trigger // a popup in the browser, immediately asking for credentials again. "/logout" => Response::error("Logged out.", 401), "/admin" => { // The "Authorization" header is sent when authenticated. let authorization = req.headers().get("Authorization")?; if authorization == None { let mut headers = Headers::new(); // Prompts the user for credentials. headers.set( "WWW-Authenticate", "Basic realm='my scope', charset='UTF-8'", )?; return Ok(Response::error("You need to login.", 401)?.with_headers(headers)); } let authorization = authorization.unwrap(); let auth: Vec<&str> = authorization.split(" ").collect(); let scheme = auth[0]; let encoded = auth[1]; // The Authorization header must start with Basic, followed by a space. if encoded == "" || scheme != "Basic" { return Response::error("Malformed authorization header.", 400); } let buff = BASE64_STANDARD.decode(encoded).unwrap(); let credentials = String::from_utf8_lossy(&buff); // The username & password are split by the first colon. //=> example: "username:password" let credentials: Vec<&str> = credentials.split(':').collect(); let user = credentials[0]; let pass = credentials[1]; if user != basic_user || pass != basic_pass { let mut headers = Headers::new(); // Prompts the user for credentials. headers.set( "WWW-Authenticate", "Basic realm='my scope', charset='UTF-8'", )?; return Ok(Response::error("You need to login.", 401)?.with_headers(headers)); } let mut headers = Headers::new(); headers.set("Cache-Control", "no-store")?; Ok(Response::ok("🎉 You have private access!")?.with_headers(headers)) } _ => Response::error("Not Found.", 404), } } ``` ```ts /** * Shows how to restrict access using the HTTP Basic schema with Hono. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication * @see https://tools.ietf.org/html/rfc7617 */ import { Hono } from "hono"; import { basicAuth } from "hono/basic-auth"; // Define environment interface interface Env { Bindings: { USERNAME: string; PASSWORD: string; }; } const app = new Hono(); // Public homepage - accessible to everyone app.get("/", (c) => { return c.text("Anyone can access the homepage."); }); // Admin route - protected with Basic Auth app.get( "/admin", async (c, next) => { const auth = basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD, }); return await auth(c, next); }, (c) => { return c.text("🎉 You have private access!", 200, { "Cache-Control": "no-store", }); }, ); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/basic-auth/#page","headline":"HTTP Basic Authentication · Cloudflare Workers docs","description":"Shows how to restrict access using the HTTP Basic schema.","url":"https://developers.cloudflare.com/workers/examples/basic-auth/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Security","Authentication","JavaScript","TypeScript","Rust"]} ``` --- --- description: Inspects the incoming request's TLS version and blocks if under TLSv1.2. title: Block on TLS image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Block on TLS Inspects the incoming request's TLS version and blocks if under TLSv1.2. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/block-on-tls/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ```js export default { async fetch(request) { try { const tlsVersion = request.cf.tlsVersion; // Allow only TLS versions 1.2 and 1.3 if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { return new Response("Please use TLS version 1.2 or higher.", { status: 403, }); } return fetch(request); } catch (err) { console.error( "request.cf does not exist in the previewer, only in production", ); return new Response(`Error in workers script ${err.message}`, { status: 500, }); } }, }; ``` ```ts export default { async fetch(request): Promise { try { const tlsVersion = request.cf.tlsVersion; // Allow only TLS versions 1.2 and 1.3 if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { return new Response("Please use TLS version 1.2 or higher.", { status: 403, }); } return fetch(request); } catch (err) { console.error( "request.cf does not exist in the previewer, only in production", ); return new Response(`Error in workers script ${err.message}`, { status: 500, }); } }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; const app = new Hono(); // Middleware to check TLS version app.use("*", async (c, next) => { // Access the raw request to get the cf object with TLS info const request = c.req.raw; const tlsVersion = request.cf?.tlsVersion; // Allow only TLS versions 1.2 and 1.3 if (tlsVersion !== "TLSv1.2" && tlsVersion !== "TLSv1.3") { return c.text("Please use TLS version 1.2 or higher.", 403); } await next(); }); app.onError((err, c) => { console.error( "request.cf does not exist in the previewer, only in production", ); return c.text(`Error in workers script: ${err.message}`, 500); }); app.get("/", async (c) => { return c.text(`TLS Version: ${c.req.raw.cf.tlsVersion}`); }); export default app; ``` ```py from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): async def fetch(self, request): tls_version = request.cf.tlsVersion if tls_version not in ("TLSv1.2", "TLSv1.3"): return Response("Please use TLS version 1.2 or higher.", status=403) return fetch(request) ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/block-on-tls/#page","headline":"Block on TLS · Cloudflare Workers docs","description":"Inspects the incoming request's TLS version and blocks if under TLSv1.2.","url":"https://developers.cloudflare.com/workers/examples/block-on-tls/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Security","Middleware","JavaScript","TypeScript","Python"]} ``` --- --- description: Resolve requests to your domain to a set of proxy third-party origin URLs. title: Bulk origin override image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Bulk origin override Resolve requests to your domain to a set of proxy third-party origin URLs. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/bulk-origin-proxy/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ```js export default { async fetch(request) { /** * An object with different URLs to fetch * @param {Object} ORIGINS */ const ORIGINS = { "starwarsapi.yourdomain.com": "swapi.dev", "google.yourdomain.com": "www.google.com", }; const url = new URL(request.url); // Check if incoming hostname is a key in the ORIGINS object if (url.hostname in ORIGINS) { const target = ORIGINS[url.hostname]; url.hostname = target; // If it is, proxy request to that third party origin return fetch(url.toString(), request); } // Otherwise, process request as normal return fetch(request); }, }; ``` ```ts export default { async fetch(request): Promise { /** * An object with different URLs to fetch * @param {Object} ORIGINS */ const ORIGINS = { "starwarsapi.yourdomain.com": "swapi.dev", "google.yourdomain.com": "www.google.com", }; const url = new URL(request.url); // Check if incoming hostname is a key in the ORIGINS object if (url.hostname in ORIGINS) { const target = ORIGINS[url.hostname]; url.hostname = target; // If it is, proxy request to that third party origin return fetch(url.toString(), request); } // Otherwise, process request as normal return fetch(request); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; import { proxy } from "hono/proxy"; // An object with different URLs to fetch const ORIGINS: Record = { "starwarsapi.yourdomain.com": "swapi.dev", "google.yourdomain.com": "www.google.com", }; const app = new Hono(); app.all("*", async (c) => { const url = new URL(c.req.url); // Check if incoming hostname is a key in the ORIGINS object if (url.hostname in ORIGINS) { const target = ORIGINS[url.hostname]; url.hostname = target; // If it is, proxy request to that third party origin return proxy(url, c.req.raw); } // Otherwise, process request as normal return proxy(c.req.raw); }); export default app; ``` ```py from workers import WorkerEntrypoint from js import fetch, URL class Default(WorkerEntrypoint): async def fetch(self, request): # A dict with different URLs to fetch ORIGINS = { "starwarsapi.yourdomain.com": "swapi.dev", "google.yourdomain.com": "www.google.com", } url = URL.new(request.url) # Check if incoming hostname is a key in the ORIGINS object if url.hostname in ORIGINS: url.hostname = ORIGINS[url.hostname] # If it is, proxy request to that third party origin return fetch(url.toString(), request) # Otherwise, process request as normal return fetch(request) ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/bulk-origin-proxy/#page","headline":"Bulk origin override · Cloudflare Workers docs","description":"Resolve requests to your domain to a set of proxy third-party origin URLs.","url":"https://developers.cloudflare.com/workers/examples/bulk-origin-proxy/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","JavaScript","TypeScript","Python"]} ``` --- --- description: Redirect requests to certain URLs based on a mapped object to the request's URL. title: Bulk redirects image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Bulk redirects Redirect requests to certain URLs based on a mapped object to the request's URL. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/bulk-redirects/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ```js export default { async fetch(request) { const externalHostname = "examples.cloudflareworkers.com"; const redirectMap = new Map([ ["/bulk1", "https://" + externalHostname + "/redirect2"], ["/bulk2", "https://" + externalHostname + "/redirect3"], ["/bulk3", "https://" + externalHostname + "/redirect4"], ["/bulk4", "https://google.com"], ]); const requestURL = new URL(request.url); const path = requestURL.pathname; const location = redirectMap.get(path); if (location) { return Response.redirect(location, 301); } // If request not in map, return the original request return fetch(request); }, }; ``` ```ts export default { async fetch(request): Promise { const externalHostname = "examples.cloudflareworkers.com"; const redirectMap = new Map([ ["/bulk1", "https://" + externalHostname + "/redirect2"], ["/bulk2", "https://" + externalHostname + "/redirect3"], ["/bulk3", "https://" + externalHostname + "/redirect4"], ["/bulk4", "https://google.com"], ]); const requestURL = new URL(request.url); const path = requestURL.pathname; const location = redirectMap.get(path); if (location) { return Response.redirect(location, 301); } // If request not in map, return the original request return fetch(request); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): external_hostname = "examples.cloudflareworkers.com" redirect_map = { "/bulk1": "https://" + external_hostname + "/redirect2", "/bulk2": "https://" + external_hostname + "/redirect3", "/bulk3": "https://" + external_hostname + "/redirect4", "/bulk4": "https://google.com", } url = urlparse(request.url) location = redirect_map.get(url.path, None) if location: return Response.redirect(location, 301) # If request not in map, return the original request return fetch(request) ``` ```ts import { Hono } from "hono"; const app = new Hono(); // Configure your redirects const externalHostname = "examples.cloudflareworkers.com"; const redirectMap = new Map([ ["/bulk1", `https://${externalHostname}/redirect2`], ["/bulk2", `https://${externalHostname}/redirect3`], ["/bulk3", `https://${externalHostname}/redirect4`], ["/bulk4", "https://google.com"], ]); // Middleware to handle redirects app.use("*", async (c, next) => { const path = c.req.path; const location = redirectMap.get(path); if (location) { // If path is in our redirect map, perform the redirect return c.redirect(location, 301); } // Otherwise, continue to the next handler await next(); }); // Default handler for requests that don't match any redirects app.all("*", async (c) => { // Pass through to origin return fetch(c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/bulk-redirects/#page","headline":"Bulk redirects · Cloudflare Workers docs","description":"Redirect requests to certain URLs based on a mapped object to the request's URL.","url":"https://developers.cloudflare.com/workers/examples/bulk-redirects/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Redirects","JavaScript","TypeScript","Python"]} ``` --- --- description: Use the Cache API to store responses in Cloudflare's cache. title: Using the Cache API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Using the Cache API Use the Cache API to store responses in Cloudflare's cache. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/cache-api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/cache-api) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request, env, ctx) { const cacheUrl = new URL(request.url); // Construct the cache key from the cache URL const cacheKey = new Request(cacheUrl.toString(), request); const cache = caches.default; // Check whether the value is already available in the cache // if not, you will need to fetch it from origin, and store it in the cache let response = await cache.match(cacheKey); if (!response) { console.log( `Response for request url: ${request.url} not present in cache. Fetching and caching request.`, ); // If not in cache, get it from origin response = await fetch(request); // Must use Response constructor to inherit all of response's fields response = new Response(response.body, response); // Cache API respects Cache-Control headers. Setting s-maxage to 10 // will limit the response to be in cache for 10 seconds max // Any changes made to the response here will be reflected in the cached value response.headers.append("Cache-Control", "s-maxage=10"); ctx.waitUntil(cache.put(cacheKey, response.clone())); } else { console.log(`Cache hit for: ${request.url}.`); } return response; }, }; ``` ```ts interface Env {} export default { async fetch(request, env, ctx): Promise { const cacheUrl = new URL(request.url); // Construct the cache key from the cache URL const cacheKey = new Request(cacheUrl.toString(), request); const cache = caches.default; // Check whether the value is already available in the cache // if not, you will need to fetch it from origin, and store it in the cache let response = await cache.match(cacheKey); if (!response) { console.log( `Response for request url: ${request.url} not present in cache. Fetching and caching request.`, ); // If not in cache, get it from origin response = await fetch(request); // Must use Response constructor to inherit all of response's fields response = new Response(response.body, response); // Cache API respects Cache-Control headers. Setting s-maxage to 10 // will limit the response to be in cache for 10 seconds max // Any changes made to the response here will be reflected in the cached value response.headers.append("Cache-Control", "s-maxage=10"); ctx.waitUntil(cache.put(cacheKey, response.clone())); } else { console.log(`Cache hit for: ${request.url}.`); } return response; }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import Response, Request, URL, caches, fetch class Default(WorkerEntrypoint): async def fetch(self, request): cache_url = request.url # Construct the cache key from the cache URL cache_key = Request.new(cache_url, request) cache = caches.default # Check whether the value is already available in the cache # if not, you will need to fetch it from origin, and store it in the cache response = await cache.match(cache_key) if response is None: print(f"Response for request url: {request.url} not present in cache. Fetching and caching request.") # If not in cache, get it from origin response = await fetch(request) # Must use Response constructor to inherit all of response's fields response = Response.new(response.body, response) # Cache API respects Cache-Control headers. Setting s-max-age to 10 # will limit the response to be in cache for 10 seconds s-maxage # Any changes made to the response here will be reflected in the cached value response.headers.append("Cache-Control", "s-maxage=10") self.ctx.waitUntil(create_proxy(cache.put(cache_key, response.clone()))) else: print(f"Cache hit for: {request.url}.") return response ``` ```ts import { Hono } from "hono"; import { cache } from "hono/cache"; const app = new Hono(); // We leverage hono built-in cache helper here app.get( "*", cache({ cacheName: "my-cache", cacheControl: "max-age=3600", // 1 hour }), ); // Add a route to handle the request if it's not in cache app.get("*", (c) => { return c.text("Hello from Hono!"); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/cache-api/#page","headline":"Using the Cache API · Cloudflare Workers docs","description":"Use the Cache API to store responses in Cloudflare's cache.","url":"https://developers.cloudflare.com/workers/examples/cache-api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Caching","JavaScript","TypeScript","Python"]} ``` --- --- description: Cache POST requests using the Cache API. title: Cache POST requests image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cache POST requests Cache POST requests using the Cache API. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/cache-post-request/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/cache-post-request) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request, env, ctx) { async function sha256(message) { // encode as UTF-8 const msgBuffer = await new TextEncoder().encode(message); // hash the message const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); // convert bytes to hex string return [...new Uint8Array(hashBuffer)] .map((b) => b.toString(16).padStart(2, "0")) .join(""); } try { if (request.method.toUpperCase() === "POST") { const body = await request.clone().text(); // Hash the request body to use it as a part of the cache key const hash = await sha256(body); const cacheUrl = new URL(request.url); // Store the URL in cache by prepending the body's hash cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash; // Convert to a GET to be able to cache const cacheKey = new Request(cacheUrl.toString(), { headers: request.headers, method: "GET", }); const cache = caches.default; // Find the cache key in the cache let response = await cache.match(cacheKey); // Otherwise, fetch response to POST request from origin if (!response) { response = await fetch(request); ctx.waitUntil(cache.put(cacheKey, response.clone())); } return response; } return fetch(request); } catch (e) { return new Response("Error thrown " + e.message); } }, }; ``` ```ts interface Env {} export default { async fetch(request, env, ctx): Promise { async function sha256(message) { // encode as UTF-8 const msgBuffer = await new TextEncoder().encode(message); // hash the message const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); // convert bytes to hex string return [...new Uint8Array(hashBuffer)] .map((b) => b.toString(16).padStart(2, "0")) .join(""); } try { if (request.method.toUpperCase() === "POST") { const body = await request.clone().text(); // Hash the request body to use it as a part of the cache key const hash = await sha256(body); const cacheUrl = new URL(request.url); // Store the URL in cache by prepending the body's hash cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash; // Convert to a GET to be able to cache const cacheKey = new Request(cacheUrl.toString(), { headers: request.headers, method: "GET", }); const cache = caches.default; // Find the cache key in the cache let response = await cache.match(cacheKey); // Otherwise, fetch response to POST request from origin if (!response) { response = await fetch(request); ctx.waitUntil(cache.put(cacheKey, response.clone())); } return response; } return fetch(request); } catch (e) { return new Response("Error thrown " + e.message); } }, } satisfies ExportedHandler; ``` ```py import hashlib from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import fetch, URL, Headers, Request, caches class Default(WorkerEntrypoint): async def fetch(self, request, _, ctx): if 'POST' in request.method: # Hash the request body to use it as a part of the cache key body = await request.clone().text() body_hash = hashlib.sha256(body.encode('UTF-8')).hexdigest() # Store the URL in cache by prepending the body's hash cache_url = URL.new(request.url) cache_url.pathname = "/posts" + cache_url.pathname + body_hash # Convert to a GET to be able to cache headers = Headers.new(dict(request.headers).items()) cache_key = Request.new(cache_url.toString(), method='GET', headers=headers) # Find the cache key in the cache cache = caches.default response = await cache.match(cache_key) # Otherwise, fetch response to POST request from origin if response is None: response = await fetch(request) ctx.waitUntil(create_proxy(cache.put(cache_key, response.clone()))) return response return fetch(request) ``` ```ts import { Hono } from "hono"; import { sha256 } from "hono/utils/crypto"; const app = new Hono(); // Middleware for caching POST requests app.post("*", async (c) => { try { // Get the request body const body = await c.req.raw.clone().text(); // Hash the request body to use it as part of the cache key const hash = await sha256(body); // Create the cache URL const cacheUrl = new URL(c.req.url); // Store the URL in cache by prepending the body's hash cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash; // Convert to a GET to be able to cache const cacheKey = new Request(cacheUrl.toString(), { headers: c.req.raw.headers, method: "GET", }); const cache = caches.default; // Find the cache key in the cache let response = await cache.match(cacheKey); // If not in cache, fetch response to POST request from origin if (!response) { response = await fetch(c.req.raw); c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); } return response; } catch (e) { return c.text("Error thrown " + e.message, 500); } }); // Handle all other HTTP methods app.all("*", (c) => { return fetch(c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/cache-post-request/#page","headline":"Cache POST requests · Cloudflare Workers docs","description":"Cache POST requests using the Cache API.","url":"https://developers.cloudflare.com/workers/examples/cache-post-request/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Caching","JavaScript","TypeScript","Python"]} ``` --- --- description: Send Additional Cache Tags using Workers title: Cache Tags using Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cache Tags using Workers Send Additional Cache Tags using Workers Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/cache-tags/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/cache-tags) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const requestUrl = new URL(request.url); const params = requestUrl.searchParams; const tags = params && params.has("tags") ? params.get("tags").split(",") : []; const url = params && params.has("uri") ? params.get("uri") : ""; if (!url) { const errorObject = { error: "URL cannot be empty", }; return new Response(JSON.stringify(errorObject), { status: 400 }); } const init = { cf: { cacheTags: tags, }, }; return fetch(url, init) .then((result) => { const cacheStatus = result.headers.get("cf-cache-status"); const lastModified = result.headers.get("last-modified"); const response = { cache: cacheStatus, lastModified: lastModified, }; return new Response(JSON.stringify(response), { status: result.status, }); }) .catch((err) => { const errorObject = { error: err.message, }; return new Response(JSON.stringify(errorObject), { status: 500 }); }); }, }; ``` ```ts export default { async fetch(request): Promise { const requestUrl = new URL(request.url); const params = requestUrl.searchParams; const tags = params && params.has("tags") ? params.get("tags").split(",") : []; const url = params && params.has("uri") ? params.get("uri") : ""; if (!url) { const errorObject = { error: "URL cannot be empty", }; return new Response(JSON.stringify(errorObject), { status: 400 }); } const init = { cf: { cacheTags: tags, }, }; return fetch(url, init) .then((result) => { const cacheStatus = result.headers.get("cf-cache-status"); const lastModified = result.headers.get("last-modified"); const response = { cache: cacheStatus, lastModified: lastModified, }; return new Response(JSON.stringify(response), { status: result.status, }); }) .catch((err) => { const errorObject = { error: err.message, }; return new Response(JSON.stringify(errorObject), { status: 500 }); }); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.all("*", async (c) => { const tags = c.req.query("tags") ? c.req.query("tags").split(",") : []; const uri = c.req.query("uri") ? c.req.query("uri") : ""; if (!uri) { return c.json({ error: "URL cannot be empty" }, 400); } const init = { cf: { cacheTags: tags, }, }; const result = await fetch(uri, init); const cacheStatus = result.headers.get("cf-cache-status"); const lastModified = result.headers.get("last-modified"); const response = { cache: cacheStatus, lastModified: lastModified, }; return c.json(response, result.status); }); app.onError((err, c) => { return c.json({ error: err.message }, 500); }); export default app; ``` ```py from workers import WorkerEntrypoint, Response, fetch from js import URL class Default(WorkerEntrypoint): async def fetch(self, request): request_url = URL.new(request.url) params = request_url.searchParams tags = params["tags"].split(",") if "tags" in params else [] url = params["uri"] or None if url is None: return Response.json({"error": "URL cannot be empty"}, status=400) result = await fetch(url, cf={"cacheTags": tags}) cache_status = result.headers["cf-cache-status"] last_modified = result.headers["last-modified"] return Response.json({"cache": cache_status, "lastModified": last_modified}, status=result.status) ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/cache-tags/#page","headline":"Cache Tags using Workers · Cloudflare Workers docs","description":"Send Additional Cache Tags using Workers","url":"https://developers.cloudflare.com/workers/examples/cache-tags/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Caching","JavaScript","TypeScript","Python"]} ``` --- --- description: Determine how to cache a resource by setting TTLs, custom cache keys, and cache headers in a fetch request. title: Cache using fetch image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cache using fetch Determine how to cache a resource by setting TTLs, custom cache keys, and cache headers in a fetch request. Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/cache-using-fetch/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/cache-using-fetch) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const url = new URL(request.url); // Only use the path for the cache key, removing query strings // and always store using HTTPS, for example, https://www.example.com/file-uri-here const someCustomKey = `https://${url.hostname}${url.pathname}`; let response = await fetch(request, { cf: { // Always cache this fetch regardless of content type // for a max of 5 seconds before revalidating the resource cacheTtl: 5, cacheEverything: true, //Enterprise only feature, see Cache API for other plans cacheKey: someCustomKey, }, }); // Reconstruct the Response object to make its headers mutable. response = new Response(response.body, response); // Set cache control headers to cache on browser for 25 minutes response.headers.set("Cache-Control", "max-age=1500"); return response; }, }; ``` ```ts export default { async fetch(request): Promise { const url = new URL(request.url); // Only use the path for the cache key, removing query strings // and always store using HTTPS, for example, https://www.example.com/file-uri-here const someCustomKey = `https://${url.hostname}${url.pathname}`; let response = await fetch(request, { cf: { // Always cache this fetch regardless of content type // for a max of 5 seconds before revalidating the resource cacheTtl: 5, cacheEverything: true, //Enterprise only feature, see Cache API for other plans cacheKey: someCustomKey, }, }); // Reconstruct the Response object to make its headers mutable. response = new Response(response.body, response); // Set cache control headers to cache on browser for 25 minutes response.headers.set("Cache-Control", "max-age=1500"); return response; }, } satisfies ExportedHandler; ``` ```ts import { Hono } from 'hono'; type Bindings = {}; const app = new Hono<{ Bindings: Bindings }>(); app.all('*', async (c) => { const url = new URL(c.req.url); // Only use the path for the cache key, removing query strings // and always store using HTTPS, for example, https://www.example.com/file-uri-here const someCustomKey = `https://${url.hostname}${url.pathname}`; // Fetch the request with custom cache settings let response = await fetch(c.req.raw, { cf: { // Always cache this fetch regardless of content type // for a max of 5 seconds before revalidating the resource cacheTtl: 5, cacheEverything: true, // Enterprise only feature, see Cache API for other plans cacheKey: someCustomKey, }, }); // Reconstruct the Response object to make its headers mutable response = new Response(response.body, response); // Set cache control headers to cache on browser for 25 minutes response.headers.set("Cache-Control", "max-age=1500"); return response; }); export default app; ``` ```py from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): url = urlparse(request.url) # Only use the path for the cache key, removing query strings # and always store using HTTPS, for example, https://www.example.com/file-uri-here some_custom_key = f"https://{url.hostname}{url.path}" response = await fetch( request, cf={ # Always cache this fetch regardless of content type # for a max of 5 seconds before revalidating the resource "cacheTtl": 5, "cacheEverything": True, # Enterprise only feature, see Cache API for other plans "cacheKey": some_custom_key, }, ) # Reconstruct the Response object to make its headers mutable new_response = Response(response.body, headers=dict(response.headers)) # Set cache control headers to cache on browser for 25 minutes new_response.headers["Cache-Control"] = "max-age=1500" return new_response ``` ```rs use worker::*; #[event(fetch)] async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { let url = req.url()?; // Only use the path for the cache key, removing query strings // and always store using HTTPS, for example, https://www.example.com/file-uri-here let custom_key = format!( "https://{host}{path}", host = url.host_str().unwrap(), path = url.path() ); let request = Request::new_with_init( url.as_str(), &RequestInit { headers: req.headers().clone(), method: req.method(), cf: CfProperties { // Always cache this fetch regardless of content type // for a max of 5 seconds before revalidating the resource cache_ttl: Some(5), cache_everything: Some(true), // Enterprise only feature, see Cache API for other plans cache_key: Some(custom_key), ..CfProperties::default() }, ..RequestInit::default() }, )?; let mut response = Fetch::Request(request).send().await?; // Set cache control headers to cache on browser for 25 minutes let _ = response.headers_mut().set("Cache-Control", "max-age=1500"); Ok(response) } ``` ## Caching HTML resources ```js // Force Cloudflare to cache an asset fetch(event.request, { cf: { cacheEverything: true } }); ``` Setting the cache level to **Cache Everything** will override the default cacheability of the asset. For time-to-live (TTL), Cloudflare will still rely on headers set by the origin. ## Custom cache keys Note This feature is available only to Enterprise customers. A request's cache key is what determines if two requests are the same for caching purposes. If a request has the same cache key as some previous request, then Cloudflare can serve the same cached response for both. For more about cache keys, refer to the [Create custom cache keys](https://developers.cloudflare.com/cache/how-to/cache-keys/#create-custom-cache-keys) documentation. ```js // Set cache key for this request to "some-string". fetch(event.request, { cf: { cacheKey: "some-string" } }); ``` Normally, Cloudflare computes the cache key for a request based on the request's URL. Sometimes, though, you may like different URLs to be treated as if they were the same for caching purposes. For example, if your website content is hosted from both Amazon S3 and Google Cloud Storage - you have the same content in both places, and you can use a Worker to randomly balance between the two. However, you do not want to end up caching two copies of your content. You could utilize custom cache keys to cache based on the original request URL rather than the subrequest URL: ```js export default { async fetch(request) { let url = new URL(request.url); if (Math.random() < 0.5) { url.hostname = "example.s3.amazonaws.com"; } else { url.hostname = "example.storage.googleapis.com"; } let newRequest = new Request(url, request); return fetch(newRequest, { cf: { cacheKey: request.url }, }); }, }; ``` ```ts export default { async fetch(request): Promise { let url = new URL(request.url); if (Math.random() < 0.5) { url.hostname = "example.s3.amazonaws.com"; } else { url.hostname = "example.storage.googleapis.com"; } let newRequest = new Request(url, request); return fetch(newRequest, { cf: { cacheKey: request.url }, }); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from 'hono'; type Bindings = {}; const app = new Hono<{ Bindings: Bindings }>(); app.all('*', async (c) => { const originalUrl = c.req.url; const url = new URL(originalUrl); // Randomly select a storage backend if (Math.random() < 0.5) { url.hostname = "example.s3.amazonaws.com"; } else { url.hostname = "example.storage.googleapis.com"; } // Create a new request to the selected backend const newRequest = new Request(url, c.req.raw); // Fetch using the original URL as the cache key return fetch(newRequest, { cf: { cacheKey: originalUrl }, }); }); export default app; ``` Workers operating on behalf of different zones cannot affect each other's cache. You can only override cache keys when making requests within your own zone (in the above example `event.request.url` was the key stored), or requests to hosts that are not on Cloudflare. When making a request to another Cloudflare zone (for example, belonging to a different Cloudflare customer), that zone fully controls how its own content is cached within Cloudflare; you cannot override it. ## Cache expected Vary responses Use `cf.vary` when an origin returns a `Vary` header and you want a Worker subrequest to cache expected variants. This setting applies only to the `fetch()` request where you set it. For Vary behavior details, refer to [Vary](https://developers.cloudflare.com/cache/concepts/vary/). For the full request init object, refer to [cf.vary](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cfvary-property). ```js 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"], }, }, }, }, }); }, }; ``` ```ts export default { async fetch(request): Promise { 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; ``` ## Override based on origin response code ```js // Force response to be cached for 86400 seconds for 200 status // codes, 1 second for 404, and do not cache 500 errors. fetch(request, { cf: { cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 } }, }); ``` This option is a version of the `cacheTtl` feature which chooses a TTL based on the response's status code and does not automatically set `cacheEverything: true`. If the response to this request has a status code that matches, Cloudflare will cache for the instructed time, and override cache directives sent by the origin. You can review [details on the cacheTtl feature on the Request page](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties). ## Customize cache behavior based on request file type Using custom cache keys and overrides based on response code, you can write a Worker that sets the TTL based on the response status code from origin, and request file type. The following example demonstrates how you might use this to cache requests for streaming media assets: ```js export default { async fetch(request) { // Instantiate new URL to make it mutable const newRequest = new URL(request.url); const customCacheKey = `${newRequest.hostname}${newRequest.pathname}`; const queryCacheKey = `${newRequest.hostname}${newRequest.pathname}${newRequest.search}`; // Different asset types usually have different caching strategies. Most of the time media content such as audio, videos and images that are not user-generated content would not need to be updated often so a long TTL would be best. However, with HLS streaming, manifest files usually are set with short TTLs so that playback will not be affected, as this files contain the data that the player would need. By setting each caching strategy for categories of asset types in an object within an array, you can solve complex needs when it comes to media content for your application const cacheAssets = [ { asset: "video", key: customCacheKey, regex: /(.*\/Video)|(.*\.(m4s|mp4|ts|avi|mpeg|mpg|mkv|bin|webm|vob|flv|m2ts|mts|3gp|m4v|wmv|qt))/, info: 0, ok: 31556952, redirects: 30, clientError: 10, serverError: 0, }, { asset: "image", key: queryCacheKey, regex: /(.*\/Images)|(.*\.(jpg|jpeg|png|bmp|pict|tif|tiff|webp|gif|heif|exif|bat|bpg|ppm|pgn|pbm|pnm))/, info: 0, ok: 3600, redirects: 30, clientError: 10, serverError: 0, }, { asset: "frontEnd", key: queryCacheKey, regex: /^.*\.(css|js)/, info: 0, ok: 3600, redirects: 30, clientError: 10, serverError: 0, }, { asset: "audio", key: customCacheKey, regex: /(.*\/Audio)|(.*\.(flac|aac|mp3|alac|aiff|wav|ogg|aiff|opus|ape|wma|3gp))/, info: 0, ok: 31556952, redirects: 30, clientError: 10, serverError: 0, }, { asset: "directPlay", key: customCacheKey, regex: /.*(\/Download)/, info: 0, ok: 31556952, redirects: 30, clientError: 10, serverError: 0, }, { asset: "manifest", key: customCacheKey, regex: /^.*\.(m3u8|mpd)/, info: 0, ok: 3, redirects: 2, clientError: 1, serverError: 0, }, ]; const { asset, regex, ...cache } = cacheAssets.find(({ regex }) => newRequest.pathname.match(regex)) ?? {}; const newResponse = await fetch(request, { cf: { cacheKey: cache.key, polish: false, cacheEverything: true, cacheTtlByStatus: { "100-199": cache.info, "200-299": cache.ok, "300-399": cache.redirects, "400-499": cache.clientError, "500-599": cache.serverError, }, cacheTags: ["static"], }, }); const response = new Response(newResponse.body, newResponse); // For debugging purposes response.headers.set("debug", JSON.stringify(cache)); return response; }, }; ``` Service Workers are deprecated Service Workers are deprecated, but still supported. We recommend using [Module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) instead. New features may not be supported for Service Workers. ```js addEventListener("fetch", (event) => { return event.respondWith(handleRequest(event.request)); }); async function handleRequest(request) { // Instantiate new URL to make it mutable const newRequest = new URL(request.url); // Set `const` to be used in the array later on const customCacheKey = `${newRequest.hostname}${newRequest.pathname}`; const queryCacheKey = `${newRequest.hostname}${newRequest.pathname}${newRequest.search}`; // Set all variables needed to manipulate Cloudflare's cache using the fetch API in the `cf` object. You will be passing these variables in the objects down below. const cacheAssets = [ { asset: "video", key: customCacheKey, regex: /(.*\/Video)|(.*\.(m4s|mp4|ts|avi|mpeg|mpg|mkv|bin|webm|vob|flv|m2ts|mts|3gp|m4v|wmv|qt))/, info: 0, ok: 31556952, redirects: 30, clientError: 10, serverError: 0, }, { asset: "image", key: queryCacheKey, regex: /(.*\/Images)|(.*\.(jpg|jpeg|png|bmp|pict|tif|tiff|webp|gif|heif|exif|bat|bpg|ppm|pgn|pbm|pnm))/, info: 0, ok: 3600, redirects: 30, clientError: 10, serverError: 0, }, { asset: "frontEnd", key: queryCacheKey, regex: /^.*\.(css|js)/, info: 0, ok: 3600, redirects: 30, clientError: 10, serverError: 0, }, { asset: "audio", key: customCacheKey, regex: /(.*\/Audio)|(.*\.(flac|aac|mp3|alac|aiff|wav|ogg|aiff|opus|ape|wma|3gp))/, info: 0, ok: 31556952, redirects: 30, clientError: 10, serverError: 0, }, { asset: "directPlay", key: customCacheKey, regex: /.*(\/Download)/, info: 0, ok: 31556952, redirects: 30, clientError: 10, serverError: 0, }, { asset: "manifest", key: customCacheKey, regex: /^.*\.(m3u8|mpd)/, info: 0, ok: 3, redirects: 2, clientError: 1, serverError: 0, }, ]; // the `.find` method is used to find elements in an array (`cacheAssets`), in this case, `regex`, which can passed to the .`match` method to match on file extensions to cache, since they are many media types in the array. If you want to add more types, update the array. Refer to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find for more information. const { asset, regex, ...cache } = cacheAssets.find(({ regex }) => newRequest.pathname.match(regex)) ?? {}; const newResponse = await fetch(request, { cf: { cacheKey: cache.key, polish: false, cacheEverything: true, cacheTtlByStatus: { "100-199": cache.info, "200-299": cache.ok, "300-399": cache.redirects, "400-499": cache.clientError, "500-599": cache.serverError, }, cacheTags: ["static"], }, }); const response = new Response(newResponse.body, newResponse); // For debugging purposes response.headers.set("debug", JSON.stringify(cache)); return response; } ``` ## Using the HTTP Cache API The `cache` mode can be set in `fetch` options. Currently Workers only support the `no-store` and `no-cache` mode for controlling the cache. When `no-store` is supplied the cache is bypassed on the way to the origin and the request is not cacheable. When `no-cache` is supplied the cache is forced to revalidate the currently cached response with the origin. ```js fetch(request, { cache: 'no-store'}); fetch(request, { cache: 'no-cache'}); ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/cache-using-fetch/#page","headline":"Cache using fetch · Cloudflare Workers docs","description":"Determine how to cache a resource by setting TTLs, custom cache keys, and cache headers in a fetch request.","url":"https://developers.cloudflare.com/workers/examples/cache-using-fetch/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Caching","Middleware","JavaScript","TypeScript","Python","Rust"]} ``` --- --- description: Return a response based on the incoming request's URL, HTTP method, User Agent, IP address, ASN or device type. title: Conditional response image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Conditional response Return a response based on the incoming request's URL, HTTP method, User Agent, IP address, ASN or device type. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/conditional-response/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/conditional-response) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]; // Return a new Response based on a URL's hostname const url = new URL(request.url); if (BLOCKED_HOSTNAMES.includes(url.hostname)) { return new Response("Blocked Host", { status: 403 }); } // Block paths ending in .doc or .xml based on the URL's file extension const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/); if (forbiddenExtRegExp.test(url.pathname)) { return new Response("Blocked Extension", { status: 403 }); } // On HTTP method if (request.method === "POST") { return new Response("Response for POST"); } // On User Agent const userAgent = request.headers.get("User-Agent") || ""; if (userAgent.includes("bot")) { return new Response("Block User Agent containing bot", { status: 403 }); } // On Client's IP address const clientIP = request.headers.get("CF-Connecting-IP"); if (clientIP === "1.2.3.4") { return new Response("Block the IP 1.2.3.4", { status: 403 }); } // On ASN if (request.cf && request.cf.asn == 64512) { return new Response("Block the ASN 64512 response"); } // On Device Type // Requires Enterprise "CF-Device-Type Header" zone setting or // Page Rule with "Cache By Device Type" setting applied. const device = request.headers.get("CF-Device-Type"); if (device === "mobile") { return Response.redirect("https://mobile.example.com"); } console.error( "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker", ); return fetch(request); }, }; ``` ```ts export default { async fetch(request): Promise { const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]; // Return a new Response based on a URL's hostname const url = new URL(request.url); if (BLOCKED_HOSTNAMES.includes(url.hostname)) { return new Response("Blocked Host", { status: 403 }); } // Block paths ending in .doc or .xml based on the URL's file extension const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/); if (forbiddenExtRegExp.test(url.pathname)) { return new Response("Blocked Extension", { status: 403 }); } // On HTTP method if (request.method === "POST") { return new Response("Response for POST"); } // On User Agent const userAgent = request.headers.get("User-Agent") || ""; if (userAgent.includes("bot")) { return new Response("Block User Agent containing bot", { status: 403 }); } // On Client's IP address const clientIP = request.headers.get("CF-Connecting-IP"); if (clientIP === "1.2.3.4") { return new Response("Block the IP 1.2.3.4", { status: 403 }); } // On ASN if (request.cf && request.cf.asn == 64512) { return new Response("Block the ASN 64512 response"); } // On Device Type // Requires Enterprise "CF-Device-Type Header" zone setting or // Page Rule with "Cache By Device Type" setting applied. const device = request.headers.get("CF-Device-Type"); if (device === "mobile") { return Response.redirect("https://mobile.example.com"); } console.error( "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker", ); return fetch(request); }, } satisfies ExportedHandler; ``` ```py import re from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): blocked_hostnames = ["nope.mywebsite.com", "bye.website.com"] url = urlparse(request.url) # Block on hostname if url.hostname in blocked_hostnames: return Response("Blocked Host", status=403) # On paths ending in .doc or .xml if re.search(r'\.(doc|xml)$', url.path): return Response("Blocked Extension", status=403) # On HTTP method if "POST" in request.method: return Response("Response for POST") # On User Agent user_agent = request.headers["User-Agent"] or "" if "bot" in user_agent: return Response("Block User Agent containing bot", status=403) # On Client's IP address client_ip = request.headers["CF-Connecting-IP"] if client_ip == "1.2.3.4": return Response("Block the IP 1.2.3.4", status=403) # On ASN if request.cf and request.cf.asn == 64512: return Response("Block the ASN 64512 response") # On Device Type # Requires Enterprise "CF-Device-Type Header" zone setting or # Page Rule with "Cache By Device Type" setting applied. device = request.headers["CF-Device-Type"] if device == "mobile": return Response.redirect("https://mobile.example.com") return fetch(request) ``` ```ts import { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; const app = new Hono(); // Middleware to handle all conditions before reaching the main handler app.use("*", async (c, next) => { const request = c.req.raw; const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]; const hostname = new URL(c.req.url)?.hostname; // Return a new Response based on a URL's hostname if (BLOCKED_HOSTNAMES.includes(hostname)) { return c.text("Blocked Host", 403); } // Block paths ending in .doc or .xml based on the URL's file extension const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/); if (forbiddenExtRegExp.test(c.req.pathname)) { return c.text("Blocked Extension", 403); } // On User Agent const userAgent = c.req.header("User-Agent") || ""; if (userAgent.includes("bot")) { return c.text("Block User Agent containing bot", 403); } // On Client's IP address const clientIP = c.req.header("CF-Connecting-IP"); if (clientIP === "1.2.3.4") { return c.text("Block the IP 1.2.3.4", 403); } // On ASN if (request.cf && request.cf.asn === 64512) { return c.text("Block the ASN 64512 response"); } // On Device Type // Requires Enterprise "CF-Device-Type Header" zone setting or // Page Rule with "Cache By Device Type" setting applied. const device = c.req.header("CF-Device-Type"); if (device === "mobile") { return c.redirect("https://mobile.example.com"); } // Continue to the next handler await next(); }); // Handle POST requests differently app.post("*", (c) => { return c.text("Response for POST"); }); // Default handler for other methods app.get("*", async (c) => { console.error( "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker", ); // Fetch the original request return fetch(c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/conditional-response/#page","headline":"Conditional response · Cloudflare Workers docs","description":"Return a response based on the incoming request's URL, HTTP method, User Agent, IP address, ASN or device type.","url":"https://developers.cloudflare.com/workers/examples/conditional-response/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","JavaScript","TypeScript","Python"]} ``` --- --- description: Add the necessary CORS headers to a third party API response. title: CORS header proxy image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # CORS header proxy Add the necessary CORS headers to a third party API response. Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/cors-header-proxy/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/cors-header-proxy) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", }; // The URL for the remote third party API you want to fetch from // but does not implement CORS const API_URL = "https://examples.cloudflareworkers.com/demos/demoapi"; // The endpoint you want the CORS reverse proxy to be on const PROXY_ENDPOINT = "/corsproxy/"; // The rest of this snippet for the demo page function rawHtmlResponse(html) { return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); } const DEMO_PAGE = `

API GET without CORS Proxy

Shows TypeError: Failed to fetch since CORS is misconfigured

Waiting

API GET with CORS Proxy

Waiting

API POST with CORS Proxy + Preflight

Waiting `; async function handleRequest(request) { const url = new URL(request.url); let apiUrl = url.searchParams.get("apiurl"); if (apiUrl == null) { apiUrl = API_URL; } // Rewrite request to point to API URL. This also makes the request mutable // so you can add the correct Origin header to make the API server think // that this request is not cross-site. request = new Request(apiUrl, request); request.headers.set("Origin", new URL(apiUrl).origin); let response = await fetch(request); // Recreate the response so you can modify the headers response = new Response(response.body, response); // Set CORS headers response.headers.set("Access-Control-Allow-Origin", url.origin); // Append to/Add Vary header so browser will cache response correctly response.headers.append("Vary", "Origin"); return response; } async function handleOptions(request) { if ( request.headers.get("Origin") !== null && request.headers.get("Access-Control-Request-Method") !== null && request.headers.get("Access-Control-Request-Headers") !== null ) { // Handle CORS preflight requests. return new Response(null, { headers: { ...corsHeaders, "Access-Control-Allow-Headers": request.headers.get( "Access-Control-Request-Headers", ), }, }); } else { // Handle standard OPTIONS request. return new Response(null, { headers: { Allow: "GET, HEAD, POST, OPTIONS", }, }); } } const url = new URL(request.url); if (url.pathname.startsWith(PROXY_ENDPOINT)) { if (request.method === "OPTIONS") { // Handle CORS preflight requests return handleOptions(request); } else if ( request.method === "GET" || request.method === "HEAD" || request.method === "POST" ) { // Handle requests to the API server return handleRequest(request); } else { return new Response(null, { status: 405, statusText: "Method Not Allowed", }); } } else { return rawHtmlResponse(DEMO_PAGE); } }, }; ``` ```ts export default { async fetch(request): Promise { const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", }; // The URL for the remote third party API you want to fetch from // but does not implement CORS const API_URL = "https://examples.cloudflareworkers.com/demos/demoapi"; // The endpoint you want the CORS reverse proxy to be on const PROXY_ENDPOINT = "/corsproxy/"; // The rest of this snippet for the demo page function rawHtmlResponse(html) { return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); } const DEMO_PAGE = `

API GET without CORS Proxy

Shows TypeError: Failed to fetch since CORS is misconfigured

Waiting

API GET with CORS Proxy

Waiting

API POST with CORS Proxy + Preflight

Waiting `; async function handleRequest(request) { const url = new URL(request.url); let apiUrl = url.searchParams.get("apiurl"); if (apiUrl == null) { apiUrl = API_URL; } // Rewrite request to point to API URL. This also makes the request mutable // so you can add the correct Origin header to make the API server think // that this request is not cross-site. request = new Request(apiUrl, request); request.headers.set("Origin", new URL(apiUrl).origin); let response = await fetch(request); // Recreate the response so you can modify the headers response = new Response(response.body, response); // Set CORS headers response.headers.set("Access-Control-Allow-Origin", url.origin); // Append to/Add Vary header so browser will cache response correctly response.headers.append("Vary", "Origin"); return response; } async function handleOptions(request) { if ( request.headers.get("Origin") !== null && request.headers.get("Access-Control-Request-Method") !== null && request.headers.get("Access-Control-Request-Headers") !== null ) { // Handle CORS preflight requests. return new Response(null, { headers: { ...corsHeaders, "Access-Control-Allow-Headers": request.headers.get( "Access-Control-Request-Headers", ), }, }); } else { // Handle standard OPTIONS request. return new Response(null, { headers: { Allow: "GET, HEAD, POST, OPTIONS", }, }); } } const url = new URL(request.url); if (url.pathname.startsWith(PROXY_ENDPOINT)) { if (request.method === "OPTIONS") { // Handle CORS preflight requests return handleOptions(request); } else if ( request.method === "GET" || request.method === "HEAD" || request.method === "POST" ) { // Handle requests to the API server return handleRequest(request); } else { return new Response(null, { status: 405, statusText: "Method Not Allowed", }); } } else { return rawHtmlResponse(DEMO_PAGE); } }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; import { cors } from "hono/cors"; // The URL for the remote third party API you want to fetch from // but does not implement CORS const API_URL = "https://examples.cloudflareworkers.com/demos/demoapi"; // The endpoint you want the CORS reverse proxy to be on const PROXY_ENDPOINT = "/corsproxy/"; const app = new Hono(); // Demo page handler app.get("*", async (c) => { // Only handle non-proxy requests with this handler if (c.req.path.startsWith(PROXY_ENDPOINT)) { return next(); } // Create the demo page HTML const DEMO_PAGE = `

API GET without CORS Proxy

Shows TypeError: Failed to fetch since CORS is misconfigured

Waiting

API GET with CORS Proxy

Waiting

API POST with CORS Proxy + Preflight

Waiting `; return c.html(DEMO_PAGE); }); // CORS proxy routes app.on(["GET", "HEAD", "POST", "OPTIONS"], PROXY_ENDPOINT + "*", async (c) => { const url = new URL(c.req.url); // Handle OPTIONS preflight requests if (c.req.method === "OPTIONS") { const origin = c.req.header("Origin"); const requestMethod = c.req.header("Access-Control-Request-Method"); const requestHeaders = c.req.header("Access-Control-Request-Headers"); if (origin && requestMethod && requestHeaders) { // Handle CORS preflight requests return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", "Access-Control-Allow-Headers": requestHeaders, }, }); } else { // Handle standard OPTIONS request return new Response(null, { headers: { Allow: "GET, HEAD, POST, OPTIONS", }, }); } } // Handle actual requests let apiUrl = url.searchParams.get("apiurl") || API_URL; // Rewrite request to point to API URL const modifiedRequest = new Request(apiUrl, c.req.raw); modifiedRequest.headers.set("Origin", new URL(apiUrl).origin); let response = await fetch(modifiedRequest); // Recreate the response so we can modify the headers response = new Response(response.body, response); // Set CORS headers response.headers.set("Access-Control-Allow-Origin", url.origin); // Append to/Add Vary header so browser will cache response correctly response.headers.append("Vary", "Origin"); return response; }); // Handle method not allowed for proxy endpoint app.all(PROXY_ENDPOINT + "*", (c) => { return new Response(null, { status: 405, statusText: "Method Not Allowed", }); }); export default app; ``` ```py from workers import WorkerEntrypoint, Response, fetch from js import Request from urllib.parse import urlparse, parse_qs class Default(WorkerEntrypoint): async def fetch(self, request): cors_headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Max-Age": "86400", } api_url = "https://examples.cloudflareworkers.com/demos/demoapi" proxy_endpoint = "/corsproxy/" def raw_html_response(html): return Response(html, headers={"content-type": "text/html;charset=UTF-8"}) demo_page = f'''

API GET without CORS Proxy

Shows TypeError: Failed to fetch since CORS is misconfigured

Waiting

API GET with CORS Proxy

Waiting

API POST with CORS Proxy + Preflight

Waiting ''' async def handle_request(request): parsed = urlparse(request.url) params = parse_qs(parsed.query) api_url2 = params.get("apiurl", [None])[0] if not api_url2: api_url2 = api_url target = urlparse(api_url2) request = Request.new(api_url2, request) request.headers["Origin"] = f"{target.scheme}://{target.netloc}" response = await fetch(request) response = Response(response.body, headers=dict(response.headers)) response.headers["Access-Control-Allow-Origin"] = f"{parsed.scheme}://{parsed.netloc}" response.headers["Vary"] = "Origin" return response async def handle_options(request): if "Origin" in request.headers and "Access-Control-Request-Method" in request.headers and "Access-Control-Request-Headers" in request.headers: return Response(None, headers={ **cors_headers, "Access-Control-Allow-Headers": request.headers["Access-Control-Request-Headers"], }) return Response(None, headers={"Allow": "GET, HEAD, POST, OPTIONS"}) parsed = urlparse(request.url) if parsed.path.startswith(proxy_endpoint): if request.method == "OPTIONS": return handle_options(request) if request.method in ("GET", "HEAD", "POST"): return handle_request(request) return Response(None, status=405) return raw_html_response(demo_page) ``` ```rs use std::{borrow::Cow, collections::HashMap}; use worker::*; fn raw_html_response(html: &str) -> Result { Response::from_html(html) } async fn handle_request(req: Request, api_url: &str) -> Result { let url = req.url().unwrap(); let mut api_url2 = url .query_pairs() .find(|x| x.0 == Cow::Borrowed("apiurl")) .unwrap() .1 .to_string(); if api_url2 == String::from("") { api_url2 = api_url.to_string(); } let mut request = req.clone_mut()?; *request.path_mut()? = api_url2.clone(); if let url::Origin::Tuple(origin, _, _) = Url::parse(&api_url2)?.origin() { (*request.headers_mut()?).set("Origin", &origin)?; } let mut response = Fetch::Request(request).send().await?.cloned()?; let headers = response.headers_mut(); if let url::Origin::Tuple(origin, _, _) = url.origin() { headers.set("Access-Control-Allow-Origin", &origin)?; headers.set("Vary", "Origin")?; } Ok(response) } fn handle_options(req: Request, cors_headers: &HashMap<&str, &str>) -> Result { let headers: Vec<_> = req.headers().keys().collect(); if [ "access-control-request-method", "access-control-request-headers", "origin", ] .iter() .all(|i| headers.contains(&i.to_string())) { let mut headers = Headers::new(); for (k, v) in cors_headers.iter() { headers.set(k, v)?; } return Ok(Response::empty()?.with_headers(headers)); } Response::empty() } #[event(fetch)] async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { let cors_headers = HashMap::from([ ("Access-Control-Allow-Origin", "*"), ("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS"), ("Access-Control-Max-Age", "86400"), ]); let api_url = "https://examples.cloudflareworkers.com/demos/demoapi"; let proxy_endpoint = "/corsproxy/"; let demo_page = format!( r#"

API GET without CORS Proxy

Shows TypeError: Failed to fetch since CORS is misconfigured

Waiting

API GET with CORS Proxy

Waiting

API POST with CORS Proxy + Preflight

Waiting "# ); if req.url()?.path().starts_with(proxy_endpoint) { match req.method() { Method::Options => return handle_options(req, &cors_headers), Method::Get | Method::Head | Method::Post => return handle_request(req, api_url).await, _ => return Response::error("Method Not Allowed", 405), } } raw_html_response(&demo_page) } ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/cors-header-proxy/#page","headline":"CORS header proxy · Cloudflare Workers docs","description":"Add the necessary CORS headers to a third party API response.","url":"https://developers.cloudflare.com/workers/examples/cors-header-proxy/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Security","Headers","JavaScript","TypeScript","Python","Rust"]} ``` --- --- description: Redirect a response based on the country code in the header of a visitor. title: Country code redirect image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Country code redirect Redirect a response based on the country code in the header of a visitor. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/country-code-redirect/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/country-code-redirect) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { /** * A map of the URLs to redirect to * @param {Object} countryMap */ const countryMap = { US: "https://example.com/us", EU: "https://example.com/eu", }; // Use the cf object to obtain the country of the request // more on the cf object: https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties const country = request.cf.country; if (country != null && country in countryMap) { const url = countryMap[country]; // Remove this logging statement from your final output. console.log( `Based on ${country}-based request, your user would go to ${url}.`, ); return Response.redirect(url); } else { return fetch("https://example.com", request); } }, }; ``` ```ts export default { async fetch(request): Promise { /** * A map of the URLs to redirect to * @param {Object} countryMap */ const countryMap = { US: "https://example.com/us", EU: "https://example.com/eu", }; // Use the cf object to obtain the country of the request // more on the cf object: https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties const country = request.cf.country; if (country != null && country in countryMap) { const url = countryMap[country]; return Response.redirect(url); } else { return fetch(request); } }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): async def fetch(self, request): countries = { "US": "https://example.com/us", "EU": "https://example.com/eu", } # Use the cf object to obtain the country of the request # more on the cf object: https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties country = request.cf.country if country and country in countries: url = countries[country] return Response.redirect(url) return fetch("https://example.com", request) ``` ```ts import { Hono } from 'hono'; // Define the RequestWithCf interface to add Cloudflare-specific properties interface RequestWithCf extends Request { cf: { country: string; // Other CF properties can be added as needed }; } const app = new Hono(); app.get('*', async (c) => { /** * A map of the URLs to redirect to */ const countryMap: Record = { US: "https://example.com/us", EU: "https://example.com/eu", }; // Cast the raw request to include Cloudflare-specific properties const request = c.req.raw as RequestWithCf; // Use the cf object to obtain the country of the request // more on the cf object: https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties const country = request.cf.country; if (country != null && country in countryMap) { const url = countryMap[country]; // Redirect using Hono's redirect helper return c.redirect(url); } else { // Default fallback return fetch("https://example.com", request); } }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/country-code-redirect/#page","headline":"Country code redirect · Cloudflare Workers docs","description":"Redirect a response based on the country code in the header of a visitor.","url":"https://developers.cloudflare.com/workers/examples/country-code-redirect/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Redirects","Geolocation","JavaScript","TypeScript","Python"]} ``` --- --- description: Set a Cron Trigger for your Worker. title: Setting Cron Triggers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Setting Cron Triggers Set a Cron Trigger for your Worker. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/cron-trigger/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ```js export default { async scheduled(controller, env, ctx) { console.log("cron processed"); }, }; ``` ```ts interface Env {} export default { async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { console.log("cron processed"); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx): print("cron processed") ``` ```ts import { Hono } from "hono"; interface Env {} // Create Hono app const app = new Hono<{ Bindings: Env }>(); // Regular routes for normal HTTP requests app.get("/", (c) => c.text("Hello World!")); // Export both the app and a scheduled function export default { // The Hono app handles regular HTTP requests fetch: app.fetch, // The scheduled function handles Cron triggers async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { console.log("cron processed"); // You could also perform actions like: // - Fetching data from external APIs // - Updating KV or Durable Object storage // - Running maintenance tasks // - Sending notifications }, }; ``` ## Set Cron Triggers in Wrangler Refer to [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) for more information on how to add a Cron Trigger. If you are deploying with Wrangler, set the cron syntax (once per hour as shown below) by adding this to your Wrangler file: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker", // ... "triggers": { "crons": [ "0 * * * *" ] } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker" [triggers] crons = [ "0 * * * *" ] ``` You also can set a different Cron Trigger for each [environment](https://developers.cloudflare.com/workers/wrangler/environments/) in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). You need to put the `[triggers]` table under your chosen environment. For example: ```jsonc { "env": { "dev": { "triggers": { "crons": [ "0 * * * *" ] } } } } ``` ```toml [env.dev.triggers] crons = [ "0 * * * *" ] ``` ## Test Cron Triggers using Wrangler The recommended way of testing Cron Triggers is using Wrangler. Cron Triggers can be tested using Wrangler by passing in the `--test-scheduled` flag to [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev). This will expose a `/__scheduled` (or `/cdn-cgi/handler/scheduled` for Python Workers) route which can be used to test using a HTTP request. To simulate different cron patterns, a `cron` query parameter can be passed in. ```sh npx wrangler dev --test-scheduled curl "http://localhost:8787/__scheduled?cron=0+*+*+*+*" curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*" # Python Workers ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/cron-trigger/#page","headline":"Setting Cron Triggers · Cloudflare Workers docs","description":"Set a Cron Trigger for your Worker.","url":"https://developers.cloudflare.com/workers/examples/cron-trigger/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","JavaScript","TypeScript"]} ``` --- --- description: Protect sensitive data to prevent data loss, and send alerts to a webhooks server in the event of a data breach. title: Data loss prevention image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Data loss prevention Protect sensitive data to prevent data loss, and send alerts to a webhooks server in the event of a data breach. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/data-loss-prevention/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/data-loss-prevention) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const DEBUG = true; const SOME_HOOK_SERVER = "https://webhook.flow-wolf.io/hook"; /** * Alert a data breach by posting to a webhook server */ async function postDataBreach(request) { return await fetch(SOME_HOOK_SERVER, { method: "POST", headers: { "content-type": "application/json;charset=UTF-8", }, body: JSON.stringify({ ip: request.headers.get("cf-connecting-ip"), time: Date.now(), request: request, }), }); } /** * Define personal data with regular expressions. * Respond with block if credit card data, and strip * emails and phone numbers from the response. * Execution will be limited to MIME type "text/*". */ const response = await fetch(request); // Return origin response, if response wasn’t text const contentType = response.headers.get("content-type") || ""; if (!contentType.toLowerCase().includes("text/")) { return response; } let text = await response.text(); // When debugging replace the response // from the origin with an email text = DEBUG ? text.replace("You may use this", "me@example.com may use this") : text; const sensitiveRegexsMap = { creditCard: String.raw`\b(?:4[0-9]{12}(?:[0-9]{3})?|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b`, email: String.raw`\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b`, phone: String.raw`\b07\d{9}\b`, }; for (const kind in sensitiveRegexsMap) { const sensitiveRegex = new RegExp(sensitiveRegexsMap[kind], "ig"); const match = await sensitiveRegex.test(text); if (match) { // Alert a data breach await postDataBreach(request); // Respond with a block if credit card, // otherwise replace sensitive text with `*`s return kind === "creditCard" ? new Response(kind + " found\nForbidden\n", { status: 403, statusText: "Forbidden", }) : new Response(text.replace(sensitiveRegex, "**********"), response); } } return new Response(text, response); }, }; ``` ```ts export default { async fetch(request): Promise { const DEBUG = true; const SOME_HOOK_SERVER = "https://webhook.flow-wolf.io/hook"; /** * Alert a data breach by posting to a webhook server */ async function postDataBreach(request) { return await fetch(SOME_HOOK_SERVER, { method: "POST", headers: { "content-type": "application/json;charset=UTF-8", }, body: JSON.stringify({ ip: request.headers.get("cf-connecting-ip"), time: Date.now(), request: request, }), }); } /** * Define personal data with regular expressions. * Respond with block if credit card data, and strip * emails and phone numbers from the response. * Execution will be limited to MIME type "text/*". */ const response = await fetch(request); // Return origin response, if response wasn’t text const contentType = response.headers.get("content-type") || ""; if (!contentType.toLowerCase().includes("text/")) { return response; } let text = await response.text(); // When debugging replace the response // from the origin with an email text = DEBUG ? text.replace("You may use this", "me@example.com may use this") : text; const sensitiveRegexsMap = { creditCard: String.raw`\b(?:4[0-9]{12}(?:[0-9]{3})?|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b`, email: String.raw`\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b`, phone: String.raw`\b07\d{9}\b`, }; for (const kind in sensitiveRegexsMap) { const sensitiveRegex = new RegExp(sensitiveRegexsMap[kind], "ig"); const match = await sensitiveRegex.test(text); if (match) { // Alert a data breach await postDataBreach(request); // Respond with a block if credit card, // otherwise replace sensitive text with `*`s return kind === "creditCard" ? new Response(kind + " found\nForbidden\n", { status: 403, statusText: "Forbidden", }) : new Response(text.replace(sensitiveRegex, "**********"), response); } } return new Response(text, response); }, } satisfies ExportedHandler; ``` ```py import re from workers import WorkerEntrypoint from datetime import datetime from js import Response, fetch, JSON, Headers # Alert a data breach by posting to a webhook server async def post_data_breach(request): some_hook_server = "https://webhook.flow-wolf.io/hook" headers = Headers.new({"content-type": "application/json"}.items()) body = JSON.stringify({ "ip": request.headers["cf-connecting-ip"], "time": datetime.now(), "request": request, }) return await fetch(some_hook_server, method="POST", headers=headers, body=body) class Default(WorkerEntrypoint): async def fetch(self, request): debug = True # Define personal data with regular expressions. # Respond with block if credit card data, and strip # emails and phone numbers from the response. # Execution will be limited to MIME type "text/*". response = await fetch(request) # Return origin response, if response wasn’t text content_type = response.headers["content-type"] or "" if "text" not in content_type: return response text = await response.text() # When debugging replace the response from the origin with an email text = text.replace("You may use this", "me@example.com may use this") if debug else text sensitive_regex = [ ("credit_card", r'\b(?:4[0-9]{12}(?:[0-9]{3})?|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b'), ("email", r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b'), ("phone", r'\b07\d{9}\b'), ] for (kind, regex) in sensitive_regex: match = re.search(regex, text, flags=re.IGNORECASE) if match: # Alert a data breach await post_data_breach(request) # Respond with a block if credit card, otherwise replace sensitive text with `*`s card_resp = Response.new(kind + " found\nForbidden\n", status=403,statusText="Forbidden") sensitive_resp = Response.new(re.sub(regex, "*"*10, text, flags=re.IGNORECASE), response) return card_resp if kind == "credit_card" else sensitive_resp return Response.new(text, response) ``` ```ts import { Hono } from 'hono'; const app = new Hono(); // Configuration const DEBUG = true; const SOME_HOOK_SERVER = "https://webhook.flow-wolf.io/hook"; // Define sensitive data patterns const sensitiveRegexsMap = { creditCard: String.raw`\b(?:4[0-9]{12}(?:[0-9]{3})?|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b`, email: String.raw`\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b`, phone: String.raw`\b07\d{9}\b`, }; /** * Alert a data breach by posting to a webhook server */ async function postDataBreach(request: Request) { return await fetch(SOME_HOOK_SERVER, { method: "POST", headers: { "content-type": "application/json;charset=UTF-8", }, body: JSON.stringify({ ip: request.headers.get("cf-connecting-ip"), time: Date.now(), request: request, }), }); } // Main middleware to handle data loss prevention app.use('*', async (c) => { // Fetch the origin response const response = await fetch(c.req.raw); // Return origin response if response wasn't text const contentType = response.headers.get("content-type") || ""; if (!contentType.toLowerCase().includes("text/")) { return response; } // Get the response text let text = await response.text(); // When debugging, replace the response from the origin with an email text = DEBUG ? text.replace("You may use this", "me@example.com may use this") : text; // Check for sensitive data for (const kind in sensitiveRegexsMap) { const sensitiveRegex = new RegExp(sensitiveRegexsMap[kind], "ig"); const match = sensitiveRegex.test(text); if (match) { // Alert a data breach await postDataBreach(c.req.raw); // Respond with a block if credit card, otherwise replace sensitive text with `*`s if (kind === "creditCard") { return c.text(`${kind} found\nForbidden\n`, 403); } else { return new Response(text.replace(sensitiveRegex, "**********"), { status: response.status, statusText: response.statusText, headers: response.headers, }); } } } // Return the modified response return new Response(text, { status: response.status, statusText: response.statusText, headers: response.headers, }); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/data-loss-prevention/#page","headline":"Data loss prevention · Cloudflare Workers docs","description":"Protect sensitive data to prevent data loss, and send alerts to a webhooks server in the event of a data breach.","url":"https://developers.cloudflare.com/workers/examples/data-loss-prevention/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Security","JavaScript","TypeScript","Python"]} ``` --- --- description: Send debugging information in an errored response to a logging service. title: Debugging logs image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Debugging logs Send debugging information in an errored response to a logging service. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/debugging-logs/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/debugging-logs) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request, env, ctx) { // Service configured to receive logs const LOG_URL = "https://log-service.example.com/"; async function postLog(data) { return await fetch(LOG_URL, { method: "POST", body: data, }); } let response; try { response = await fetch(request); if (!response.ok && !response.redirected) { const body = await response.text(); throw new Error( "Bad response at origin. Status: " + response.status + " Body: " + // Ensure the string is small enough to be a header body.trim().substring(0, 10), ); } } catch (err) { // Without ctx.waitUntil(), your fetch() to Cloudflare's // logging service may or may not complete ctx.waitUntil(postLog(err.toString())); const stack = JSON.stringify(err.stack) || err; // Copy the response and initialize body to the stack trace response = new Response(stack, response); // Add the error stack into a header to find out what happened response.headers.set("X-Debug-stack", stack); response.headers.set("X-Debug-err", err); } return response; }, }; ``` ```ts interface Env {} export default { async fetch(request, env, ctx): Promise { // Service configured to receive logs const LOG_URL = "https://log-service.example.com/"; async function postLog(data) { return await fetch(LOG_URL, { method: "POST", body: data, }); } let response; try { response = await fetch(request); if (!response.ok && !response.redirected) { const body = await response.text(); throw new Error( "Bad response at origin. Status: " + response.status + " Body: " + // Ensure the string is small enough to be a header body.trim().substring(0, 10), ); } } catch (err) { // Without ctx.waitUntil(), your fetch() to Cloudflare's // logging service may or may not complete ctx.waitUntil(postLog(err.toString())); const stack = JSON.stringify(err.stack) || err; // Copy the response and initialize body to the stack trace response = new Response(stack, response); // Add the error stack into a header to find out what happened response.headers.set("X-Debug-stack", stack); response.headers.set("X-Debug-err", err); } return response; }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import Response, fetch async def post_log(data): log_url = "https://log-service.example.com/" await fetch(log_url, method="POST", body=data) class Default(WorkerEntrypoint): async def fetch(self, request): # Service configured to receive logs response = await fetch(request) try: if not response.ok and not response.redirected: body = await response.text() # Simulating an error. Ensure the string is small enough to be a header raise Exception(f'Bad response at origin. Status:{response.status} Body:{body.strip()[:10]}') except Exception as e: # Without ctx.waitUntil(), your fetch() to Cloudflare's # logging service may or may not complete self.ctx.waitUntil(create_proxy(post_log(str(e)))) # Copy the response and add to header response = Response.new(stack, response) response.headers["X-Debug-err"] = str(e) return response ``` ```ts import { Hono } from 'hono'; // Define the environment with appropriate types interface Env {} const app = new Hono<{ Bindings: Env }>(); // Service configured to receive logs const LOG_URL = "https://log-service.example.com/"; // Function to post logs to an external service async function postLog(data: string) { return await fetch(LOG_URL, { method: "POST", body: data, }); } // Middleware to handle error logging app.use('*', async (c, next) => { try { // Process the request with the next handler await next(); // After processing, check if the response indicates an error if (c.res && (!c.res.ok && !c.res.redirected)) { const body = await c.res.clone().text(); throw new Error( "Bad response at origin. Status: " + c.res.status + " Body: " + // Ensure the string is small enough to be a header body.trim().substring(0, 10) ); } } catch (err) { // Without waitUntil, the fetch to the logging service may not complete c.executionCtx.waitUntil( postLog(err.toString()) ); // Get the error stack or error itself const stack = JSON.stringify(err.stack) || err.toString(); // Create a new response with the error information const response = c.res ? new Response(stack, { status: c.res.status, headers: c.res.headers }) : new Response(stack, { status: 500 }); // Add debug headers response.headers.set("X-Debug-stack", stack); response.headers.set("X-Debug-err", err.toString()); // Set the modified response c.res = response; } }); // Default route handler that passes requests through app.all('*', async (c) => { return fetch(c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/debugging-logs/#page","headline":"Debugging logs · Cloudflare Workers docs","description":"Send debugging information in an errored response to a logging service.","url":"https://developers.cloudflare.com/workers/examples/debugging-logs/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Debugging","JavaScript","TypeScript","Python"]} ``` --- --- description: Given the cookie name, get the value of a cookie. You can also use cookies for A/B testing. title: Cookie parsing image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cookie parsing Given the cookie name, get the value of a cookie. You can also use cookies for A/B testing. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/extract-cookie-value/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/extract-cookie-value) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js import { parse } from "cookie"; export default { async fetch(request) { // The name of the cookie const COOKIE_NAME = "__uid"; const cookie = parse(request.headers.get("Cookie") || ""); if (cookie[COOKIE_NAME] != null) { // Respond with the cookie value return new Response(cookie[COOKIE_NAME]); } return new Response("No cookie with name: " + COOKIE_NAME); }, }; ``` ```ts import { parse } from "cookie"; export default { async fetch(request): Promise { // The name of the cookie const COOKIE_NAME = "__uid"; const cookie = parse(request.headers.get("Cookie") || ""); if (cookie[COOKIE_NAME] != null) { // Respond with the cookie value return new Response(cookie[COOKIE_NAME]); } return new Response("No cookie with name: " + COOKIE_NAME); }, } satisfies ExportedHandler; ``` ```py from http.cookies import SimpleCookie from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): # Name of the cookie cookie_name = "__uid" cookies = SimpleCookie(request.headers["Cookie"] or "") if cookie_name in cookies: # Respond with cookie value return Response(cookies[cookie_name].value) return Response("No cookie with name: " + cookie_name) ``` ```ts import { Hono } from 'hono'; import { getCookie } from 'hono/cookie'; const app = new Hono(); app.get('*', (c) => { // The name of the cookie const COOKIE_NAME = "__uid"; // Get the specific cookie value using Hono's cookie helper const cookieValue = getCookie(c, COOKIE_NAME); if (cookieValue) { // Respond with the cookie value return c.text(cookieValue); } return c.text("No cookie with name: " + COOKIE_NAME); }); export default app; ``` External dependencies This example requires the npm package [cookie ↗](https://www.npmjs.com/package/cookie) to be installed in your JavaScript project. The Hono example uses the built-in cookie utilities provided by Hono, so no external dependencies are needed for that implementation. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/extract-cookie-value/#page","headline":"Cookie parsing · Cloudflare Workers docs","description":"Given the cookie name, get the value of a cookie. You can also use cookies for A/B testing.","url":"https://developers.cloudflare.com/workers/examples/extract-cookie-value/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Headers","JavaScript","TypeScript","Python"]} ``` --- --- description: Send a request to a remote server, read HTML from the response, and serve that HTML. title: Fetch HTML image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Fetch HTML Send a request to a remote server, read HTML from the response, and serve that HTML. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/fetch-html/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/fetch-html) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { /** * Replace `remote` with the host you wish to send requests to */ const remote = "https://example.com"; return await fetch(remote, request); }, }; ``` ```ts export default { async fetch(request: Request): Promise { /** * Replace `remote` with the host you wish to send requests to */ const remote = "https://example.com"; return await fetch(remote, request); }, }; ``` ```py from workers import WorkerEntrypoint from js import fetch class Default(WorkerEntrypoint): async def fetch(self, request): # Replace `remote` with the host you wish to send requests to remote = "https://example.com" return await fetch(remote, request) ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.all("*", async (c) => { /** * Replace `remote` with the host you wish to send requests to */ const remote = "https://example.com"; // Forward the request to the remote server return await fetch(remote, c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/fetch-html/#page","headline":"Fetch HTML · Cloudflare Workers docs","description":"Send a request to a remote server, read HTML from the response, and serve that HTML.","url":"https://developers.cloudflare.com/workers/examples/fetch-html/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Send a GET request and read in JSON from the response. Use to fetch external data. title: Fetch JSON image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Fetch JSON Send a GET request and read in JSON from the response. Use to fetch external data. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/fetch-json/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/fetch-json) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request, env, ctx) { const url = "https://jsonplaceholder.typicode.com/todos/1"; // gatherResponse returns both content-type & response body as a string async function gatherResponse(response) { const { headers } = response; const contentType = headers.get("content-type") || ""; if (contentType.includes("application/json")) { return { contentType, result: JSON.stringify(await response.json()) }; } return { contentType, result: await response.text() }; } const response = await fetch(url); const { contentType, result } = await gatherResponse(response); const options = { headers: { "content-type": contentType } }; return new Response(result, options); }, }; ``` ```ts interface Env {} export default { async fetch(request, env, ctx): Promise { const url = "https://jsonplaceholder.typicode.com/todos/1"; // gatherResponse returns both content-type & response body as a string async function gatherResponse(response) { const { headers } = response; const contentType = headers.get("content-type") || ""; if (contentType.includes("application/json")) { return { contentType, result: JSON.stringify(await response.json()) }; } return { contentType, result: await response.text() }; } const response = await fetch(url); const { contentType, result } = await gatherResponse(response); const options = { headers: { "content-type": contentType } }; return new Response(result, options); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch import json class Default(WorkerEntrypoint): async def fetch(self, request): url = "https://jsonplaceholder.typicode.com/todos/1" # gather_response returns both content-type & response body as a string async def gather_response(response): headers = response.headers content_type = headers["content-type"] or "" if "application/json" in content_type: return (content_type, json.dumps(await response.json())) return (content_type, await response.text()) response = await fetch(url) content_type, result = await gather_response(response) headers = {"content-type": content_type} return Response(result, headers=headers) ``` ```ts import { Hono } from 'hono'; type Env = {}; const app = new Hono<{ Bindings: Env }>(); app.get('*', async (c) => { const url = "https://jsonplaceholder.typicode.com/todos/1"; // gatherResponse returns both content-type & response body as a string async function gatherResponse(response: Response) { const { headers } = response; const contentType = headers.get("content-type") || ""; if (contentType.includes("application/json")) { return { contentType, result: JSON.stringify(await response.json()) }; } return { contentType, result: await response.text() }; } const response = await fetch(url); const { contentType, result } = await gatherResponse(response); return new Response(result, { headers: { "content-type": contentType } }); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/fetch-json/#page","headline":"Fetch JSON · Cloudflare Workers docs","description":"Send a GET request and read in JSON from the response. Use to fetch external data.","url":"https://developers.cloudflare.com/workers/examples/fetch-json/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JSON","JavaScript","TypeScript","Python"]} ``` --- --- description: Fetch weather data from an API using the user's geolocation data. title: Geolocation: Weather application image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Geolocation: Weather application Fetch weather data from an API using the user's geolocation data. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/geolocation-app-weather/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/geolocation-app-weather) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { let endpoint = "https://api.waqi.info/feed/geo:"; const token = ""; //Use a token from https://aqicn.org/api/ let html_style = `body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}`; let html_content = "

Weather 🌦

"; const latitude = request.cf.latitude; const longitude = request.cf.longitude; endpoint += `${latitude};${longitude}/?token=${token}`; const init = { headers: { "content-type": "application/json;charset=UTF-8", }, }; const response = await fetch(endpoint, init); const content = await response.json(); html_content += `

This is a demo using Workers geolocation data.

`; html_content += `You are located at: ${latitude},${longitude}.

`; html_content += `

Based off sensor data from content.data.city.url}">${content.data.city.name}:

`; html_content += `

The AQI level is: ${content.data.aqi}.

`; html_content += `

The N02 level is: ${content.data.iaqi.no2?.v}.

`; html_content += `

The O3 level is: ${content.data.iaqi.o3?.v}.

`; html_content += `

The temperature is: ${content.data.iaqi.t?.v}°C.

`; let html = ` Geolocation: Weather
${html_content}
`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { let endpoint = "https://api.waqi.info/feed/geo:"; const token = ""; //Use a token from https://aqicn.org/api/ let html_style = `body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}`; let html_content = "

Weather 🌦

"; const latitude = request.cf.latitude; const longitude = request.cf.longitude; endpoint += `${latitude};${longitude}/?token=${token}`; const init = { headers: { "content-type": "application/json;charset=UTF-8", }, }; const response = await fetch(endpoint, init); const content = await response.json(); html_content += `

This is a demo using Workers geolocation data.

`; html_content += `You are located at: ${latitude},${longitude}.

`; html_content += `

Based off sensor data from content.data.city.url}">${content.data.city.name}:

`; html_content += `

The AQI level is: ${content.data.aqi}.

`; html_content += `

The N02 level is: ${content.data.iaqi.no2?.v}.

`; html_content += `

The O3 level is: ${content.data.iaqi.o3?.v}.

`; html_content += `

The temperature is: ${content.data.iaqi.t?.v}°C.

`; let html = ` Geolocation: Weather
${html_content}
`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from 'hono'; import { html } from 'hono/html'; type Bindings = {}; interface WeatherApiResponse { data: { aqi: number; city: { name: string; url: string; }; iaqi: { no2?: { v: number }; o3?: { v: number }; t?: { v: number }; }; }; } const app = new Hono<{ Bindings: Bindings }>(); app.get('*', async (c) => { // Get API endpoint let endpoint = "https://api.waqi.info/feed/geo:"; const token = ""; // Use a token from https://aqicn.org/api/ // Define styles const html_style = `body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}`; // Get geolocation from Cloudflare request const req = c.req.raw; const latitude = req.cf?.latitude; const longitude = req.cf?.longitude; // Create complete API endpoint with coordinates endpoint += `${latitude};${longitude}/?token=${token}`; // Fetch weather data const init = { headers: { "content-type": "application/json;charset=UTF-8", }, }; const response = await fetch(endpoint, init); const content = await response.json() as WeatherApiResponse; // Build HTML content const weatherContent = html`

Weather 🌦

This is a demo using Workers geolocation data.

You are located at: ${latitude},${longitude}.

Based off sensor data from content.data.city.url}">${content.data.city.name}:

The AQI level is: ${content.data.aqi}.

The N02 level is: ${content.data.iaqi.no2?.v}.

The O3 level is: ${content.data.iaqi.o3?.v}.

The temperature is: ${content.data.iaqi.t?.v}°C.

`; // Complete HTML document const htmlDocument = html` Geolocation: Weather
${weatherContent}
`; // Return HTML response return c.html(htmlDocument); }); export default app; ``` ```py from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): async def fetch(self, request): endpoint = "https://api.waqi.info/feed/geo:" token = "" # Use a token from https://aqicn.org/api/ html_style = "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}" html_content = "

Weather 🌦

" latitude = request.cf.latitude longitude = request.cf.longitude endpoint += f"{latitude};{longitude}/?token={token}" response = await fetch(endpoint) content = await response.json() html_content += "

This is a demo using Workers geolocation data.

" html_content += f"You are located at: {latitude},{longitude}.

" html_content += f"

Based off sensor data from {content['data']['city']['name']}:

" html_content += f"

The AQI level is: {content['data']['aqi']}.

" html_content += f"

The N02 level is: {content['data']['iaqi']['no2']['v']}.

" html_content += f"

The O3 level is: {content['data']['iaqi']['o3']['v']}.

" html_content += f"

The temperature is: {content['data']['iaqi']['t']['v']}°C.

" html = f""" Geolocation: Weather
{html_content}
""" headers = {"content-type": "text/html;charset=UTF-8"} return Response(html, headers=headers) ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/geolocation-app-weather/#page","headline":"Geolocation: Weather application · Cloudflare Workers docs","description":"Fetch weather data from an API using the user's geolocation data.","url":"https://developers.cloudflare.com/workers/examples/geolocation-app-weather/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Geolocation","JavaScript","TypeScript","Python"]} ``` --- --- description: Personalize website styling based on localized user time. title: Geolocation: Custom Styling image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Geolocation: Custom Styling Personalize website styling based on localized user time. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/geolocation-custom-styling/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/geolocation-custom-styling) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { let grads = [ [ { color: "00000c", position: 0 }, { color: "00000c", position: 0 }, ], [ { color: "020111", position: 85 }, { color: "191621", position: 100 }, ], [ { color: "020111", position: 60 }, { color: "20202c", position: 100 }, ], [ { color: "020111", position: 10 }, { color: "3a3a52", position: 100 }, ], [ { color: "20202c", position: 0 }, { color: "515175", position: 100 }, ], [ { color: "40405c", position: 0 }, { color: "6f71aa", position: 80 }, { color: "8a76ab", position: 100 }, ], [ { color: "4a4969", position: 0 }, { color: "7072ab", position: 50 }, { color: "cd82a0", position: 100 }, ], [ { color: "757abf", position: 0 }, { color: "8583be", position: 60 }, { color: "eab0d1", position: 100 }, ], [ { color: "82addb", position: 0 }, { color: "ebb2b1", position: 100 }, ], [ { color: "94c5f8", position: 1 }, { color: "a6e6ff", position: 70 }, { color: "b1b5ea", position: 100 }, ], [ { color: "b7eaff", position: 0 }, { color: "94dfff", position: 100 }, ], [ { color: "9be2fe", position: 0 }, { color: "67d1fb", position: 100 }, ], [ { color: "90dffe", position: 0 }, { color: "38a3d1", position: 100 }, ], [ { color: "57c1eb", position: 0 }, { color: "246fa8", position: 100 }, ], [ { color: "2d91c2", position: 0 }, { color: "1e528e", position: 100 }, ], [ { color: "2473ab", position: 0 }, { color: "1e528e", position: 70 }, { color: "5b7983", position: 100 }, ], [ { color: "1e528e", position: 0 }, { color: "265889", position: 50 }, { color: "9da671", position: 100 }, ], [ { color: "1e528e", position: 0 }, { color: "728a7c", position: 50 }, { color: "e9ce5d", position: 100 }, ], [ { color: "154277", position: 0 }, { color: "576e71", position: 30 }, { color: "e1c45e", position: 70 }, { color: "b26339", position: 100 }, ], [ { color: "163C52", position: 0 }, { color: "4F4F47", position: 30 }, { color: "C5752D", position: 60 }, { color: "B7490F", position: 80 }, { color: "2F1107", position: 100 }, ], [ { color: "071B26", position: 0 }, { color: "071B26", position: 30 }, { color: "8A3B12", position: 80 }, { color: "240E03", position: 100 }, ], [ { color: "010A10", position: 30 }, { color: "59230B", position: 80 }, { color: "2F1107", position: 100 }, ], [ { color: "090401", position: 50 }, { color: "4B1D06", position: 100 }, ], [ { color: "00000c", position: 80 }, { color: "150800", position: 100 }, ], ]; async function toCSSGradient(hour) { let css = "linear-gradient(to bottom,"; const data = grads[hour]; const len = data.length; for (let i = 0; i < len; i++) { const item = data[i]; css += ` #${item.color} ${item.position}%`; if (i < len - 1) css += ","; } return css + ")"; } let html_content = ""; let html_style = ` html{width:100vw; height:100vh;} body{padding:0; margin:0 !important;height:100%;} #container { display: flex; flex-direction:column; align-items: center; justify-content: center; height: 100%; color:white; font-family:sans-serif; }`; const timezone = request.cf.timezone; console.log(timezone); let localized_date = new Date( new Date().toLocaleString("en-US", { timeZone: timezone }), ); let hour = localized_date.getHours(); let minutes = localized_date.getMinutes(); html_content += "

" + hour + ":" + minutes + "

"; html_content += "

" + timezone + "

"; html_style += "body{background:" + (await toCSSGradient(hour)) + ";}"; let html = ` Geolocation: Customized Design
${html_content}
`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8" }, }); }, }; ``` ```ts export default { async fetch(request): Promise { let grads = [ [ { color: "00000c", position: 0 }, { color: "00000c", position: 0 }, ], [ { color: "020111", position: 85 }, { color: "191621", position: 100 }, ], [ { color: "020111", position: 60 }, { color: "20202c", position: 100 }, ], [ { color: "020111", position: 10 }, { color: "3a3a52", position: 100 }, ], [ { color: "20202c", position: 0 }, { color: "515175", position: 100 }, ], [ { color: "40405c", position: 0 }, { color: "6f71aa", position: 80 }, { color: "8a76ab", position: 100 }, ], [ { color: "4a4969", position: 0 }, { color: "7072ab", position: 50 }, { color: "cd82a0", position: 100 }, ], [ { color: "757abf", position: 0 }, { color: "8583be", position: 60 }, { color: "eab0d1", position: 100 }, ], [ { color: "82addb", position: 0 }, { color: "ebb2b1", position: 100 }, ], [ { color: "94c5f8", position: 1 }, { color: "a6e6ff", position: 70 }, { color: "b1b5ea", position: 100 }, ], [ { color: "b7eaff", position: 0 }, { color: "94dfff", position: 100 }, ], [ { color: "9be2fe", position: 0 }, { color: "67d1fb", position: 100 }, ], [ { color: "90dffe", position: 0 }, { color: "38a3d1", position: 100 }, ], [ { color: "57c1eb", position: 0 }, { color: "246fa8", position: 100 }, ], [ { color: "2d91c2", position: 0 }, { color: "1e528e", position: 100 }, ], [ { color: "2473ab", position: 0 }, { color: "1e528e", position: 70 }, { color: "5b7983", position: 100 }, ], [ { color: "1e528e", position: 0 }, { color: "265889", position: 50 }, { color: "9da671", position: 100 }, ], [ { color: "1e528e", position: 0 }, { color: "728a7c", position: 50 }, { color: "e9ce5d", position: 100 }, ], [ { color: "154277", position: 0 }, { color: "576e71", position: 30 }, { color: "e1c45e", position: 70 }, { color: "b26339", position: 100 }, ], [ { color: "163C52", position: 0 }, { color: "4F4F47", position: 30 }, { color: "C5752D", position: 60 }, { color: "B7490F", position: 80 }, { color: "2F1107", position: 100 }, ], [ { color: "071B26", position: 0 }, { color: "071B26", position: 30 }, { color: "8A3B12", position: 80 }, { color: "240E03", position: 100 }, ], [ { color: "010A10", position: 30 }, { color: "59230B", position: 80 }, { color: "2F1107", position: 100 }, ], [ { color: "090401", position: 50 }, { color: "4B1D06", position: 100 }, ], [ { color: "00000c", position: 80 }, { color: "150800", position: 100 }, ], ]; async function toCSSGradient(hour) { let css = "linear-gradient(to bottom,"; const data = grads[hour]; const len = data.length; for (let i = 0; i < len; i++) { const item = data[i]; css += ` #${item.color} ${item.position}%`; if (i < len - 1) css += ","; } return css + ")"; } let html_content = ""; let html_style = ` html{width:100vw; height:100vh;} body{padding:0; margin:0 !important;height:100%;} #container { display: flex; flex-direction:column; align-items: center; justify-content: center; height: 100%; color:white; font-family:sans-serif; }`; const timezone = request.cf.timezone; console.log(timezone); let localized_date = new Date( new Date().toLocaleString("en-US", { timeZone: timezone }), ); let hour = localized_date.getHours(); let minutes = localized_date.getMinutes(); html_content += "

" + hour + ":" + minutes + "

"; html_content += "

" + timezone + "

"; html_style += "body{background:" + (await toCSSGradient(hour)) + ";}"; let html = ` Geolocation: Customized Design
${html_content}
`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8" }, }); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from 'hono'; type Bindings = {}; type ColorStop = { color: string; position: number }; const app = new Hono<{ Bindings: Bindings }>(); // Gradient configurations for each hour of the day (0-23) const grads: ColorStop[][] = [ [ { color: "00000c", position: 0 }, { color: "00000c", position: 0 }, ], [ { color: "020111", position: 85 }, { color: "191621", position: 100 }, ], [ { color: "020111", position: 60 }, { color: "20202c", position: 100 }, ], [ { color: "020111", position: 10 }, { color: "3a3a52", position: 100 }, ], [ { color: "20202c", position: 0 }, { color: "515175", position: 100 }, ], [ { color: "40405c", position: 0 }, { color: "6f71aa", position: 80 }, { color: "8a76ab", position: 100 }, ], [ { color: "4a4969", position: 0 }, { color: "7072ab", position: 50 }, { color: "cd82a0", position: 100 }, ], [ { color: "757abf", position: 0 }, { color: "8583be", position: 60 }, { color: "eab0d1", position: 100 }, ], [ { color: "82addb", position: 0 }, { color: "ebb2b1", position: 100 }, ], [ { color: "94c5f8", position: 1 }, { color: "a6e6ff", position: 70 }, { color: "b1b5ea", position: 100 }, ], [ { color: "b7eaff", position: 0 }, { color: "94dfff", position: 100 }, ], [ { color: "9be2fe", position: 0 }, { color: "67d1fb", position: 100 }, ], [ { color: "90dffe", position: 0 }, { color: "38a3d1", position: 100 }, ], [ { color: "57c1eb", position: 0 }, { color: "246fa8", position: 100 }, ], [ { color: "2d91c2", position: 0 }, { color: "1e528e", position: 100 }, ], [ { color: "2473ab", position: 0 }, { color: "1e528e", position: 70 }, { color: "5b7983", position: 100 }, ], [ { color: "1e528e", position: 0 }, { color: "265889", position: 50 }, { color: "9da671", position: 100 }, ], [ { color: "1e528e", position: 0 }, { color: "728a7c", position: 50 }, { color: "e9ce5d", position: 100 }, ], [ { color: "154277", position: 0 }, { color: "576e71", position: 30 }, { color: "e1c45e", position: 70 }, { color: "b26339", position: 100 }, ], [ { color: "163C52", position: 0 }, { color: "4F4F47", position: 30 }, { color: "C5752D", position: 60 }, { color: "B7490F", position: 80 }, { color: "2F1107", position: 100 }, ], [ { color: "071B26", position: 0 }, { color: "071B26", position: 30 }, { color: "8A3B12", position: 80 }, { color: "240E03", position: 100 }, ], [ { color: "010A10", position: 30 }, { color: "59230B", position: 80 }, { color: "2F1107", position: 100 }, ], [ { color: "090401", position: 50 }, { color: "4B1D06", position: 100 }, ], [ { color: "00000c", position: 80 }, { color: "150800", position: 100 }, ], ]; // Convert hour to CSS gradient async function toCSSGradient(hour: number): Promise { let css = "linear-gradient(to bottom,"; const data = grads[hour]; const len = data.length; for (let i = 0; i < len; i++) { const item = data[i]; css += ` #${item.color} ${item.position}%`; if (i < len - 1) css += ","; } return css + ")"; } app.get('*', async (c) => { const request = c.req.raw; // Base HTML style let html_style = ` html{width:100vw; height:100vh;} body{padding:0; margin:0 !important;height:100%;} #container { display: flex; flex-direction:column; align-items: center; justify-content: center; height: 100%; color:white; font-family:sans-serif; }`; // Get timezone from Cloudflare request const timezone = request.cf?.timezone || 'UTC'; console.log(timezone); // Get localized time let localized_date = new Date( new Date().toLocaleString("en-US", { timeZone: timezone }) ); let hour = localized_date.getHours(); let minutes = localized_date.getMinutes(); // Generate HTML content let html_content = `

${hour}:${minutes}

`; html_content += `

${timezone}

`; // Add background gradient based on hour html_style += `body{background:${await toCSSGradient(hour)};}`; // Complete HTML document let html = ` Geolocation: Customized Design
${html_content}
`; return c.html(html); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/geolocation-custom-styling/#page","headline":"Geolocation: Custom Styling · Cloudflare Workers docs","description":"Personalize website styling based on localized user time.","url":"https://developers.cloudflare.com/workers/examples/geolocation-custom-styling/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Geolocation","JavaScript","TypeScript"]} ``` --- --- description: Get all geolocation data fields and display them in HTML. title: Geolocation: Hello World image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Geolocation: Hello World Get all geolocation data fields and display them in HTML. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/geolocation-hello-world/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/geolocation-hello-world) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { let html_content = ""; let html_style = "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f;}"; html_content += "

Colo: " + request.cf.colo + "

"; html_content += "

Country: " + request.cf.country + "

"; html_content += "

City: " + request.cf.city + "

"; html_content += "

Continent: " + request.cf.continent + "

"; html_content += "

Latitude: " + request.cf.latitude + "

"; html_content += "

Longitude: " + request.cf.longitude + "

"; html_content += "

PostalCode: " + request.cf.postalCode + "

"; html_content += "

MetroCode: " + request.cf.metroCode + "

"; html_content += "

Region: " + request.cf.region + "

"; html_content += "

RegionCode: " + request.cf.regionCode + "

"; html_content += "

Timezone: " + request.cf.timezone + "

"; let html = ` Geolocation: Hello World

Geolocation: Hello World!

You now have access to geolocation data about where your user is visiting from.

${html_content} `; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { let html_content = ""; let html_style = "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f;}"; html_content += "

Colo: " + request.cf.colo + "

"; html_content += "

Country: " + request.cf.country + "

"; html_content += "

City: " + request.cf.city + "

"; html_content += "

Continent: " + request.cf.continent + "

"; html_content += "

Latitude: " + request.cf.latitude + "

"; html_content += "

Longitude: " + request.cf.longitude + "

"; html_content += "

PostalCode: " + request.cf.postalCode + "

"; html_content += "

MetroCode: " + request.cf.metroCode + "

"; html_content += "

Region: " + request.cf.region + "

"; html_content += "

RegionCode: " + request.cf.regionCode + "

"; html_content += "

Timezone: " + request.cf.timezone + "

"; let html = ` Geolocation: Hello World

Geolocation: Hello World!

You now have access to geolocation data about where your user is visiting from.

${html_content} `; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): html_content = "" html_style = "body{padding:6em font-family: sans-serif;} h1{color:#f6821f;}" html_content += "

Colo: " + request.cf.colo + "

" html_content += "

Country: " + request.cf.country + "

" html_content += "

City: " + request.cf.city + "

" html_content += "

Continent: " + request.cf.continent + "

" html_content += "

Latitude: " + request.cf.latitude + "

" html_content += "

Longitude: " + request.cf.longitude + "

" html_content += "

PostalCode: " + request.cf.postalCode + "

" html_content += "

Region: " + request.cf.region + "

" html_content += "

RegionCode: " + request.cf.regionCode + "

" html_content += "

Timezone: " + request.cf.timezone + "

" html = f""" Geolocation: Hello World

Geolocation: Hello World!

You now have access to geolocation data about where your user is visiting from.

{html_content} """ headers = {"content-type": "text/html;charset=UTF-8"} return Response(html, headers=headers) ``` ```ts import { Hono } from "hono"; import { html } from "hono/html"; // Define the RequestWithCf interface to add Cloudflare-specific properties interface RequestWithCf extends Request { cf: { // Cloudflare-specific properties for geolocation colo: string; country: string; city: string; continent: string; latitude: string; longitude: string; postalCode: string; metroCode: string; region: string; regionCode: string; timezone: string; // Add other CF properties as needed }; } const app = new Hono(); app.get("*", (c) => { // Cast the raw request to include Cloudflare-specific properties const request = c.req.raw; // Define styles const html_style = "body{padding:6em; font-family: sans-serif;} h1{color:#f6821f;}"; // Create content with geolocation data let html_content = html`

Colo: ${request.cf.colo}

Country: ${request.cf.country}

City: ${request.cf.city}

Continent: ${request.cf.continent}

Latitude: ${request.cf.latitude}

Longitude: ${request.cf.longitude}

PostalCode: ${request.cf.postalCode}

MetroCode: ${request.cf.metroCode}

Region: ${request.cf.region}

RegionCode: ${request.cf.regionCode}

Timezone: ${request.cf.timezone}

`; // Compose the full HTML const htmlContent = html` Geolocation: Hello World

Geolocation: Hello World!

You now have access to geolocation data about where your user is visiting from.

${html_content} `; // Return the HTML response return c.html(htmlContent); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/geolocation-hello-world/#page","headline":"Geolocation: Hello World · Cloudflare Workers docs","description":"Get all geolocation data fields and display them in HTML.","url":"https://developers.cloudflare.com/workers/examples/geolocation-hello-world/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Geolocation","JavaScript","TypeScript","Python"]} ``` --- --- description: Block other websites from linking to your content. This is useful for protecting images. title: Hot-link protection image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Hot-link protection Block other websites from linking to your content. This is useful for protecting images. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/hot-link-protection/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/hot-link-protection) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const HOMEPAGE_URL = "https://tutorial.cloudflareworkers.com/"; const PROTECTED_TYPE = "image/"; // Fetch the original request const response = await fetch(request); // If it's an image, engage hotlink protection based on the // Referer header. const referer = request.headers.get("Referer"); const contentType = response.headers.get("Content-Type") || ""; if (referer && contentType.startsWith(PROTECTED_TYPE)) { // If the hostnames don't match, it's a hotlink if (new URL(referer).hostname !== new URL(request.url).hostname) { // Redirect the user to your website return Response.redirect(HOMEPAGE_URL, 302); } } // Everything is fine, return the response normally. return response; }, }; ``` ```ts export default { async fetch(request): Promise { const HOMEPAGE_URL = "https://tutorial.cloudflareworkers.com/"; const PROTECTED_TYPE = "image/"; // Fetch the original request const response = await fetch(request); // If it's an image, engage hotlink protection based on the // Referer header. const referer = request.headers.get("Referer"); const contentType = response.headers.get("Content-Type") || ""; if (referer && contentType.startsWith(PROTECTED_TYPE)) { // If the hostnames don't match, it's a hotlink if (new URL(referer).hostname !== new URL(request.url).hostname) { // Redirect the user to your website return Response.redirect(HOMEPAGE_URL, 302); } } // Everything is fine, return the response normally. return response; }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): homepage_url = "https://tutorial.cloudflareworkers.com/" protected_type = "image/" # Fetch the original request response = await fetch(request) # If it's an image, engage hotlink protection based on the referer header referer = request.headers["Referer"] content_type = response.headers["Content-Type"] or "" if referer and content_type.startswith(protected_type): # If the hostnames don't match, it's a hotlink if urlparse(referer).hostname != urlparse(request.url).hostname: # Redirect the user to your website return Response.redirect(homepage_url, 302) # Everything is fine, return the response normally return response ``` ```ts import { Hono } from 'hono'; const app = new Hono(); // Middleware for hot-link protection app.use('*', async (c, next) => { const HOMEPAGE_URL = "https://tutorial.cloudflareworkers.com/"; const PROTECTED_TYPE = "image/"; // Continue to the next handler to get the response await next(); // If we have a response, check for hotlinking if (c.res) { // If it's an image, engage hotlink protection based on the Referer header const referer = c.req.header("Referer"); const contentType = c.res.headers.get("Content-Type") || ""; if (referer && contentType.startsWith(PROTECTED_TYPE)) { // If the hostnames don't match, it's a hotlink if (new URL(referer).hostname !== new URL(c.req.url).hostname) { // Redirect the user to your website c.res = c.redirect(HOMEPAGE_URL, 302); } } } }); // Default route handler that passes through the request to the origin app.all('*', async (c) => { // Fetch the original request return fetch(c.req.raw); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/hot-link-protection/#page","headline":"Hot-link protection · Cloudflare Workers docs","description":"Block other websites from linking to your content. This is useful for protecting images.","url":"https://developers.cloudflare.com/workers/examples/hot-link-protection/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Security","Headers","JavaScript","TypeScript","Python"]} ``` --- --- description: Set up custom domain for Images using a Worker or serve images using a prefix path and Cloudflare registered domain. title: Custom Domain with Images image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Custom Domain with Images Set up custom domain for Images using a Worker or serve images using a prefix path and Cloudflare registered domain. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/images-workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/images-workers) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. To serve images from a custom domain: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select **Create application** \> **Workers** \> **Create Worker** and create your Worker. 3. In your Worker, select **Quick edit** and paste the following code. ```js export default { async fetch(request) { // You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA const accountHash = ""; const { pathname } = new URL(request.url); // A request to something like cdn.example.com/83eb7b2-5392-4565-b69e-aff66acddd00/public // will fetch "https://imagedelivery.net//83eb7b2-5392-4565-b69e-aff66acddd00/public" return fetch(`https://imagedelivery.net/${accountHash}${pathname}`); }, }; ``` ```ts export default { async fetch(request): Promise { // You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA const accountHash = ""; const { pathname } = new URL(request.url); // A request to something like cdn.example.com/83eb7b2-5392-4565-b69e-aff66acddd00/public // will fetch "https://imagedelivery.net//83eb7b2-5392-4565-b69e-aff66acddd00/public" return fetch(`https://imagedelivery.net/${accountHash}${pathname}`); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from 'hono'; interface Env { // You can store your account hash as a binding variable ACCOUNT_HASH?: string; } const app = new Hono<{ Bindings: Env }>(); app.get('*', async (c) => { // You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA // Either get it from environment or hardcode it here const accountHash = c.env.ACCOUNT_HASH || ""; const url = new URL(c.req.url); // A request to something like cdn.example.com/83eb7b2-5392-4565-b69e-aff66acddd00/public // will fetch "https://imagedelivery.net//83eb7b2-5392-4565-b69e-aff66acddd00/public" return fetch(`https://imagedelivery.net/${accountHash}${url.pathname}`); }); export default app; ``` ```py from workers import WorkerEntrypoint from js import URL, fetch class Default(WorkerEntrypoint): async def fetch(self, request): # You can find this in the dashboard, it should look something like this: ZWd9g1K7eljCn_KDTu_MWA account_hash = "" url = URL.new(request.url) # A request to something like cdn.example.com/83eb7b2-5392-4565-b69e-aff66acddd00/public # will fetch "https://imagedelivery.net//83eb7b2-5392-4565-b69e-aff66acddd00/public" return fetch(f'https://imagedelivery.net/{account_hash}{url.pathname}') ``` Another way you can serve images from a custom domain is by using the `cdn-cgi/imagedelivery` prefix path which is used as path to trigger `cdn-cgi` image proxy. Below is an example showing the hostname as a Cloudflare proxied domain under the same account as the Image, followed with the prefix path and the image ``, `` and `` which can be found in the **Images** on the Cloudflare dashboard. ```js https://example.com/cdn-cgi/imagedelivery/// ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/images-workers/#page","headline":"Custom Domain with Images · Cloudflare Workers docs","description":"Set up custom domain for Images using a Worker or serve images using a prefix path and Cloudflare registered domain.","url":"https://developers.cloudflare.com/workers/examples/images-workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Examine the contents of a Headers object by logging to console with a Map. title: Logging headers to console image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Logging headers to console Examine the contents of a Headers object by logging to console with a Map. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/logging-headers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/logging-headers) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { console.log(new Map(request.headers)); return new Response("Hello world"); }, }; ``` ```ts export default { async fetch(request): Promise { console.log(new Map(request.headers)); return new Response("Hello world"); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): print(dict(request.headers)) return Response('Hello world') ``` ```rs use worker::*; #[event(fetch)] async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { console_log!("{:?}", req.headers()); Response::ok("hello world") } ``` ```ts import { Hono } from 'hono'; const app = new Hono(); app.get('*', (c) => { // Different ways to log headers in Hono: // 1. Using Map to display headers in console console.log('Headers as Map:', new Map(c.req.raw.headers)); // 2. Using spread operator to log headers console.log('Headers spread:', [...c.req.raw.headers]); // 3. Using Object.fromEntries to convert to an object console.log('Headers as Object:', Object.fromEntries(c.req.raw.headers)); // 4. Hono's built-in header accessor (for individual headers) console.log('User-Agent:', c.req.header('User-Agent')); // 5. Using c.req.headers to get all headers console.log('All headers from Hono context:', c.req.header()); return c.text('Hello world'); }); export default app; ``` --- ## Console-logging headers Use a `Map` if you need to log a `Headers` object to the console: ```js console.log(new Map(request.headers)); ``` Use the `spread` operator if you need to quickly stringify a `Headers` object: ```js let requestHeaders = JSON.stringify([...request.headers]); ``` Use `Object.fromEntries` to convert the headers to an object: ```js let requestHeaders = Object.fromEntries(request.headers); ``` ### The problem When debugging Workers, examine the headers on a request or response. A common mistake is to try to log headers to the developer console via code like this: ```js console.log(request.headers); ``` Or this: ```js console.log(`Request headers: ${JSON.stringify(request.headers)}`); ``` Both attempts result in what appears to be an empty object — the string `"{}"` — even though calling `request.headers.has("Your-Header-Name")` might return true. This is the same behavior that browsers implement. The reason this happens is because [Headers ↗](https://developer.mozilla.org/en-US/docs/Web/API/Headers) objects do not store headers in enumerable JavaScript properties, so the developer console and JSON stringifier do not know how to read the names and values of the headers. It is not actually an empty object, but rather an opaque object. `Headers` objects are iterable, which you can take advantage of to develop a couple of quick one-liners for debug-printing headers. ### Pass headers through a Map The first common idiom for making Headers `console.log()`\-friendly is to construct a `Map` object from the `Headers` object and log the `Map` object. ```js console.log(new Map(request.headers)); ``` This works because: * `Map` objects can be constructed from iterables, like `Headers`. * The `Map` object does store its entries in enumerable JavaScript properties, so the developer console can see into it. ### Spread headers into an array The `Map` approach works for calls to `console.log()`. If you need to stringify your headers, you will discover that stringifying a `Map` yields nothing more than `[object Map]`. Even though a `Map` stores its data in enumerable properties, those properties are [Symbol ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Symbol)\-keyed. Because of this, `JSON.stringify()` will [ignore Symbol-keyed properties ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Symbol#symbols%5Fand%5Fjson.stringify) and you will receive an empty `{}`. Instead, you can take advantage of the iterability of the `Headers` object in a new way by applying the [spread operator ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread%5Fsyntax) (`...`) to it. ```js let requestHeaders = JSON.stringify([...request.headers], null, 2); console.log(`Request headers: ${requestHeaders}`); ``` ### Convert headers into an object with Object.fromEntries (ES2019) ES2019 provides [Object.fromEntries ↗](https://github.com/tc39/proposal-object-from-entries) which is a call to convert the headers into an object: ```js let headersObject = Object.fromEntries(request.headers); let requestHeaders = JSON.stringify(headersObject, null, 2); console.log(`Request headers: ${requestHeaders}`); ``` This results in something like: ```js Request headers: { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.9", "cf-ipcountry": "US", // ... }" ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/logging-headers/#page","headline":"Logging headers to console · Cloudflare Workers docs","description":"Examine the contents of a Headers object by logging to console with a Map.","url":"https://developers.cloudflare.com/workers/examples/logging-headers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Debugging","Headers","JavaScript","Rust","TypeScript","Python"]} ``` --- --- description: Create a modified request with edited properties based off of an incoming request. title: Modify request property image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Modify request property Create a modified request with edited properties based off of an incoming request. Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/modify-request-property/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/modify-request-property) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { /** * Example someHost is set up to return raw JSON * @param {string} someUrl the URL to send the request to, since we are setting hostname too only path is applied * @param {string} someHost the host the request will resolve too */ const someHost = "example.com"; const someUrl = "https://foo.example.com/api.js"; /** * The best practice is to only assign new RequestInit properties * on the request object using either a method or the constructor */ const newRequestInit = { // Change method method: "POST", // Change body body: JSON.stringify({ bar: "foo" }), // Change the redirect mode. redirect: "follow", // Change headers, note this method will erase existing headers headers: { "Content-Type": "application/json", }, // Change a Cloudflare feature on the outbound response cf: { apps: false }, }; // Change just the host const url = new URL(someUrl); url.hostname = someHost; // Best practice is to always use the original request to construct the new request // to clone all the attributes. Applying the URL also requires a constructor // since once a Request has been constructed, its URL is immutable. const newRequest = new Request( url.toString(), new Request(request, newRequestInit), ); // Set headers using method newRequest.headers.set("X-Example", "bar"); newRequest.headers.set("Content-Type", "application/json"); try { return await fetch(newRequest); } catch (e) { return new Response(JSON.stringify({ error: e.message }), { status: 500, }); } }, }; ``` ```ts export default { async fetch(request): Promise { /** * Example someHost is set up to return raw JSON * @param {string} someUrl the URL to send the request to, since we are setting hostname too only path is applied * @param {string} someHost the host the request will resolve too */ const someHost = "example.com"; const someUrl = "https://foo.example.com/api.js"; /** * The best practice is to only assign new RequestInit properties * on the request object using either a method or the constructor */ const newRequestInit = { // Change method method: "POST", // Change body body: JSON.stringify({ bar: "foo" }), // Change the redirect mode. redirect: "follow", // Change headers, note this method will erase existing headers headers: { "Content-Type": "application/json", }, // Change a Cloudflare feature on the outbound response cf: { apps: false }, }; // Change just the host const url = new URL(someUrl); url.hostname = someHost; // Best practice is to always use the original request to construct the new request // to clone all the attributes. Applying the URL also requires a constructor // since once a Request has been constructed, its URL is immutable. const newRequest = new Request( url.toString(), new Request(request, newRequestInit), ); // Set headers using method newRequest.headers.set("X-Example", "bar"); newRequest.headers.set("Content-Type", "application/json"); try { return await fetch(newRequest); } catch (e) { return new Response(JSON.stringify({ error: e.message }), { status: 500, }); } }, } satisfies ExportedHandler; ``` ```py import json from workers import WorkerEntrypoint, Response, fetch from js import Request from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): some_host = "example.com" some_url = "https://foo.example.com/api.js" # The best practice is to only assign new_request_init properties # on the request object using either a method or the constructor new_request_init = { "method": "POST", # Change method "body": json.dumps({"bar": "foo"}), # Change body "redirect": "follow", # Change the redirect mode # Change headers, note this method will erase existing headers "headers": { "Content-Type": "application/json", }, # Change a Cloudflare feature on the outbound response "cf": {"apps": False}, } # Change just the host parsed = urlparse(some_url) new_url = parsed._replace(netloc=some_host).geturl() # Best practice is to always use the original request to construct the new request # to clone all the attributes. Applying the URL also requires a constructor # since once a Request has been constructed, its URL is immutable. org_request = Request.new(request, new_request_init) new_request = Request.new(new_url, org_request) new_request.headers["X-Example"] = "bar" new_request.headers["Content-Type"] = "application/json" try: return await fetch(new_request) except Exception as e: return Response.join({"error": str(e)}, status=500) ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.all("*", async (c) => { /** * Example someHost is set up to return raw JSON */ const someHost = "example.com"; const someUrl = "https://foo.example.com/api.js"; // Create a URL object to modify the hostname const url = new URL(someUrl); url.hostname = someHost; // Create a new request // First create a clone of the original request with the new properties const requestClone = new Request(c.req.raw, { // Change method method: "POST", // Change body body: JSON.stringify({ bar: "foo" }), // Change the redirect mode redirect: "follow" as RequestRedirect, // Change headers, note this method will erase existing headers headers: { "Content-Type": "application/json", "X-Example": "bar", }, // Change a Cloudflare feature on the outbound response cf: { apps: false }, }); // Then create a new request with the modified URL const newRequest = new Request(url.toString(), requestClone); // Send the modified request const response = await fetch(newRequest); // Return the response return response; }); // Handle errors app.onError((err, c) => { return err.getResponse(); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/modify-request-property/#page","headline":"Modify request property · Cloudflare Workers docs","description":"Create a modified request with edited properties based off of an incoming request.","url":"https://developers.cloudflare.com/workers/examples/modify-request-property/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Headers","JavaScript","TypeScript","Python"]} ``` --- --- description: Fetch and modify response properties which are immutable by creating a copy first. title: Modify response image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Modify response Fetch and modify response properties which are immutable by creating a copy first. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/modify-response/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/modify-response) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { /** * @param {string} headerNameSrc Header to get the new value from * @param {string} headerNameDst Header to set based off of value in src */ const headerNameSrc = "foo"; //"Orig-Header" const headerNameDst = "Last-Modified"; /** * Response properties are immutable. To change them, construct a new * Response and pass modified status or statusText in the ResponseInit * object. Response headers can be modified through the headers `set` method. */ const originalResponse = await fetch(request); // Change status and statusText, but preserve body and headers let response = new Response(originalResponse.body, { status: 500, statusText: "some message", headers: originalResponse.headers, }); // Change response body by adding the foo prop const originalBody = await originalResponse.json(); const body = JSON.stringify({ foo: "bar", ...originalBody }); response = new Response(body, response); // Add a header using set method response.headers.set("foo", "bar"); // Set destination header to the value of the source header const src = response.headers.get(headerNameSrc); if (src != null) { response.headers.set(headerNameDst, src); console.log( `Response header "${headerNameDst}" was set to "${response.headers.get( headerNameDst, )}"`, ); } return response; }, }; ``` ```ts export default { async fetch(request): Promise { /** * @param {string} headerNameSrc Header to get the new value from * @param {string} headerNameDst Header to set based off of value in src */ const headerNameSrc = "foo"; //"Orig-Header" const headerNameDst = "Last-Modified"; /** * Response properties are immutable. To change them, construct a new * Response and pass modified status or statusText in the ResponseInit * object. Response headers can be modified through the headers `set` method. */ const originalResponse = await fetch(request); // Change status and statusText, but preserve body and headers let response = new Response(originalResponse.body, { status: 500, statusText: "some message", headers: originalResponse.headers, }); // Change response body by adding the foo prop const originalBody = await originalResponse.json(); const body = JSON.stringify({ foo: "bar", ...originalBody }); response = new Response(body, response); // Add a header using set method response.headers.set("foo", "bar"); // Set destination header to the value of the source header const src = response.headers.get(headerNameSrc); if (src != null) { response.headers.set(headerNameDst, src); console.log( `Response header "${headerNameDst}" was set to "${response.headers.get( headerNameDst, )}"`, ); } return response; }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch import json class Default(WorkerEntrypoint): async def fetch(self, request): header_name_src = "foo" # Header to get the new value from header_name_dst = "Last-Modified" # Header to set based off of value in src # Response properties are immutable. To change them, construct a new response original_response = await fetch(request) # Change status and statusText, but preserve body and headers response = Response(original_response.body, status=500, status_text="some message", headers=original_response.headers) # Change response body by adding the foo prop new_body = await original_response.json() new_body["foo"] = "bar" response.replace_body(json.dumps(new_body)) # Add a new header response.headers["foo"] = "bar" # Set destination header to the value of the source header src = response.headers[header_name_src] if src is not None: response.headers[header_name_dst] = src print(f'Response header {header_name_dst} was set to {response.headers[header_name_dst]}') return response ``` ```ts import { Hono } from 'hono'; const app = new Hono(); app.get('*', async (c) => { /** * Header configuration */ const headerNameSrc = "foo"; // Header to get the new value from const headerNameDst = "Last-Modified"; // Header to set based off of value in src /** * Response properties are immutable. With Hono, we can modify the response * by creating custom response objects. */ const originalResponse = await fetch(c.req.raw); // Get the JSON body from the original response const originalBody = await originalResponse.json(); // Modify the body by adding a new property const modifiedBody = { foo: "bar", ...originalBody }; // Create a new custom response with modified status, headers, and body const response = new Response(JSON.stringify(modifiedBody), { status: 500, statusText: "some message", headers: originalResponse.headers, }); // Add a header using set method response.headers.set("foo", "bar"); // Set destination header to the value of the source header const src = response.headers.get(headerNameSrc); if (src != null) { response.headers.set(headerNameDst, src); console.log( `Response header "${headerNameDst}" was set to "${response.headers.get(headerNameDst)}"` ); } return response; }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/modify-response/#page","headline":"Modify response · Cloudflare Workers docs","description":"Fetch and modify response properties which are immutable by creating a copy first.","url":"https://developers.cloudflare.com/workers/examples/modify-response/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Headers","JavaScript","TypeScript","Python"]} ``` --- --- description: Set multiple Cron Triggers on three different schedules. title: Multiple Cron Triggers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Multiple Cron Triggers Set multiple Cron Triggers on three different schedules. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/multiple-cron-triggers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/multiple-cron-triggers) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async scheduled(event, env, ctx) { // Write code for updating your API switch (event.cron) { case "*/3 * * * *": // Every three minutes await updateAPI(); break; case "*/10 * * * *": // Every ten minutes await updateAPI2(); break; case "*/45 * * * *": // Every forty-five minutes await updateAPI3(); break; } console.log("cron processed"); }, }; ``` ```ts interface Env {} export default { async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { // Write code for updating your API switch (controller.cron) { case "*/3 * * * *": // Every three minutes await updateAPI(); break; case "*/10 * * * *": // Every ten minutes await updateAPI2(); break; case "*/45 * * * *": // Every forty-five minutes await updateAPI3(); break; } console.log("cron processed"); }, }; ``` ```ts import { Hono } from "hono"; interface Env {} // Create Hono app const app = new Hono<{ Bindings: Env }>(); // Regular routes for normal HTTP requests app.get("/", (c) => c.text("Multiple Cron Trigger Example")); // Export both the app and a scheduled function export default { // The Hono app handles regular HTTP requests fetch: app.fetch, // The scheduled function handles Cron triggers async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { // Check which cron schedule triggered this execution switch (controller.cron) { case "*/3 * * * *": // Every three minutes await updateAPI(); break; case "*/10 * * * *": // Every ten minutes await updateAPI2(); break; case "*/45 * * * *": // Every forty-five minutes await updateAPI3(); break; } console.log("cron processed"); }, }; ``` ## Test Cron Triggers using Wrangler The recommended way of testing Cron Triggers is using Wrangler. Cron Triggers can be tested using Wrangler by passing in the `--test-scheduled` flag to [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev). This will expose a `/__scheduled` (or `/cdn-cgi/handler/scheduled` for Python Workers) route which can be used to test using a HTTP request. To simulate different cron patterns, a `cron` query parameter can be passed in. ```sh npx wrangler dev --test-scheduled curl "http://localhost:8787/__scheduled?cron=*%2F3+*+*+*+*" curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*" # Python Workers ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/multiple-cron-triggers/#page","headline":"Multiple Cron Triggers · Cloudflare Workers docs","description":"Set multiple Cron Triggers on three different schedules.","url":"https://developers.cloudflare.com/workers/examples/multiple-cron-triggers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","JavaScript","TypeScript"]} ``` --- --- description: Use the OpenAI v4 SDK to stream responses from OpenAI. title: Stream OpenAI API Responses image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Stream OpenAI API Responses Use the OpenAI v4 SDK to stream responses from OpenAI. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/openai-sdk-streaming/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/openai-sdk-streaming) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. In order to run this code, you must install the OpenAI SDK by running `npm i openai`. Note For analytics, caching, rate limiting, and more, you can also send requests like this through Cloudflare's [AI Gateway](https://developers.cloudflare.com/ai-gateway/usage/providers/openai/). ```ts import OpenAI from "openai"; export default { async fetch(request, env, ctx): Promise { const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY, }); // Create a TransformStream to handle streaming data let { readable, writable } = new TransformStream(); let writer = writable.getWriter(); const textEncoder = new TextEncoder(); ctx.waitUntil( (async () => { const stream = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Tell me a story" }], stream: true, }); // loop over the data as it is streamed and write to the writeable for await (const part of stream) { writer.write( textEncoder.encode(part.choices[0]?.delta?.content || ""), ); } writer.close(); })(), ); // Send the readable back to the browser return new Response(readable); }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; import { streamText } from "hono/streaming"; import OpenAI from "openai"; interface Env { OPENAI_API_KEY: string; } const app = new Hono<{ Bindings: Env }>(); app.get("*", async (c) => { const openai = new OpenAI({ apiKey: c.env.OPENAI_API_KEY, }); const chatStream = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Tell me a story" }], stream: true, }); return streamText(c, async (stream) => { for await (const message of chatStream) { await stream.write(message.choices[0].delta.content || ""); } stream.close(); }); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/openai-sdk-streaming/#page","headline":"Stream OpenAI API Responses · Cloudflare Workers docs","description":"Use the OpenAI v4 SDK to stream responses from OpenAI.","url":"https://developers.cloudflare.com/workers/examples/openai-sdk-streaming/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI","JavaScript","TypeScript"]} ``` --- --- description: Send a POST request with JSON data. Use to share data with external servers. title: Post JSON image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Post JSON Send a POST request with JSON data. Use to share data with external servers. Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/post-json/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/post-json) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { /** * Example someHost is set up to take in a JSON request * Replace url with the host you wish to send requests to * @param {string} url the URL to send the request to * @param {BodyInit} body the JSON data to send in the request */ const someHost = "https://examples.cloudflareworkers.com/demos"; const url = someHost + "/requests/json"; const body = { results: ["default data to send"], errors: null, msg: "I sent this to the fetch", }; /** * gatherResponse awaits and returns a response body as a string. * Use await gatherResponse(..) in an async function to get the response body * @param {Response} response */ async function gatherResponse(response) { const { headers } = response; const contentType = headers.get("content-type") || ""; if (contentType.includes("application/json")) { return JSON.stringify(await response.json()); } else if (contentType.includes("application/text")) { return response.text(); } else if (contentType.includes("text/html")) { return response.text(); } else { return response.text(); } } const init = { body: JSON.stringify(body), method: "POST", headers: { "content-type": "application/json;charset=UTF-8", }, }; const response = await fetch(url, init); const results = await gatherResponse(response); return new Response(results, init); }, }; ``` ```ts export default { async fetch(request): Promise { /** * Example someHost is set up to take in a JSON request * Replace url with the host you wish to send requests to * @param {string} url the URL to send the request to * @param {BodyInit} body the JSON data to send in the request */ const someHost = "https://examples.cloudflareworkers.com/demos"; const url = someHost + "/requests/json"; const body = { results: ["default data to send"], errors: null, msg: "I sent this to the fetch", }; /** * gatherResponse awaits and returns a response body as a string. * Use await gatherResponse(..) in an async function to get the response body * @param {Response} response */ async function gatherResponse(response) { const { headers } = response; const contentType = headers.get("content-type") || ""; if (contentType.includes("application/json")) { return JSON.stringify(await response.json()); } else if (contentType.includes("application/text")) { return response.text(); } else if (contentType.includes("text/html")) { return response.text(); } else { return response.text(); } } const init = { body: JSON.stringify(body), method: "POST", headers: { "content-type": "application/json;charset=UTF-8", }, }; const response = await fetch(url, init); const results = await gatherResponse(response); return new Response(results, init); }, } satisfies ExportedHandler; ``` ```py import json from workers import WorkerEntrypoint, Response, fetch async def gather_response(response): headers = response.headers content_type = headers["content-type"] or "" if "application/json" in content_type: return (content_type, json.dumps(dict(await response.json()))) return (content_type, await response.text()) class Default(WorkerEntrypoint): async def fetch(self, _request): url = "https://jsonplaceholder.typicode.com/todos/1" body = { "results": ["default data to send"], "errors": None, "msg": "I sent this to the fetch", } response = await fetch( url, method="POST", body=json.dumps(body), headers={"content-type": "application/json;charset=UTF-8"}, ) content_type, result = await gather_response(response) return Response(result, headers={"content-type": content_type}) ``` ```ts import { Hono } from 'hono'; const app = new Hono(); app.get('*', async (c) => { /** * Example someHost is set up to take in a JSON request * Replace url with the host you wish to send requests to */ const someHost = "https://examples.cloudflareworkers.com/demos"; const url = someHost + "/requests/json"; const body = { results: ["default data to send"], errors: null, msg: "I sent this to the fetch", }; /** * gatherResponse awaits and returns a response body as a string. * Use await gatherResponse(..) in an async function to get the response body */ async function gatherResponse(response: Response) { const { headers } = response; const contentType = headers.get("content-type") || ""; if (contentType.includes("application/json")) { return { contentType, result: JSON.stringify(await response.json()) }; } else if (contentType.includes("application/text")) { return { contentType, result: await response.text() }; } else if (contentType.includes("text/html")) { return { contentType, result: await response.text() }; } else { return { contentType, result: await response.text() }; } } const init = { body: JSON.stringify(body), method: "POST", headers: { "content-type": "application/json;charset=UTF-8", }, }; const response = await fetch(url, init); const { contentType, result } = await gatherResponse(response); return new Response(result, { headers: { "content-type": contentType, }, }); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/post-json/#page","headline":"Post JSON · Cloudflare Workers docs","description":"Send a POST request with JSON data. Use to share data with external servers.","url":"https://developers.cloudflare.com/workers/examples/post-json/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JSON","JavaScript","TypeScript","Python"]} ``` --- --- description: Protect against timing attacks by safely comparing values using `timingSafeEqual`. title: Using timingSafeEqual image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Using timingSafeEqual Protect against timing attacks by safely comparing values using `timingSafeEqual`. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/protect-against-timing-attacks) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. The [crypto.subtle.timingSafeEqual](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#timingsafeequal) function compares two values using a constant-time algorithm. The time taken is independent of the contents of the values. When strings are compared using the equality operator (`==` or `===`), the comparison will end at the first mismatched character. By using `timingSafeEqual`, an attacker would not be able to use timing to find where at which point in the two strings there is a difference. The `timingSafeEqual` function takes two `ArrayBuffer` or `TypedArray` values to compare. These buffers must be of equal length, otherwise an exception is thrown. Note that this function is not constant time with respect to the length of the parameters and also does not guarantee constant time for the surrounding code. Handling of secrets should be taken with care to not introduce timing side channels. Caution Do not return early when the input and secret have different lengths. An early return leaks the length of the secret through response timing. Instead, always perform a constant-time comparison as shown in the examples below — when lengths differ, compare the user input against itself and negate the result so the check still fails but takes the same amount of time. In order to compare two strings, you must use the [TextEncoder](https://developers.cloudflare.com/workers/runtime-apis/encoding/#textencoder) API. ```ts interface Environment { MY_SECRET_VALUE?: string; } export default { async fetch(req: Request, env: Environment) { if (!env.MY_SECRET_VALUE) { return new Response("Missing secret binding", { status: 500 }); } const authToken = req.headers.get("Authorization") || ""; const encoder = new TextEncoder(); const userValue = encoder.encode(authToken); const secretValue = encoder.encode(env.MY_SECRET_VALUE); // Do not return early when lengths differ — that leaks the secret's // length through timing. Instead, always perform a constant-time // comparison: when the lengths match compare directly; otherwise // compare the user input against itself (always true) and negate. const lengthsMatch = userValue.byteLength === secretValue.byteLength; const isEqual = lengthsMatch ? crypto.subtle.timingSafeEqual(userValue, secretValue) : !crypto.subtle.timingSafeEqual(userValue, userValue); if (!isEqual) { return new Response("Unauthorized", { status: 401 }); } return new Response("Welcome!"); }, }; ``` ```py from workers import WorkerEntrypoint, Response from js import TextEncoder, crypto class Default(WorkerEntrypoint): async def fetch(self, request): auth_token = request.headers["Authorization"] or "" secret = self.env.MY_SECRET_VALUE if secret is None: return Response("Missing secret binding", status=500) encoder = TextEncoder.new() user_value = encoder.encode(auth_token) secret_value = encoder.encode(secret) # Do not return early when lengths differ — that leaks the secret's # length through timing. Always perform a constant-time comparison. if user_value.byteLength == secret_value.byteLength: is_equal = crypto.subtle.timingSafeEqual(user_value, secret_value) else: is_equal = not crypto.subtle.timingSafeEqual(user_value, user_value) if not is_equal: return Response("Unauthorized", status=401) return Response("Welcome!") ``` ```ts import { Hono } from 'hono'; interface Environment { Bindings: { MY_SECRET_VALUE?: string; } } const app = new Hono(); // Middleware to handle authentication with timing-safe comparison app.use('*', async (c, next) => { const secret = c.env.MY_SECRET_VALUE; if (!secret) { return c.text("Missing secret binding", 500); } const authToken = c.req.header("Authorization") || ""; const encoder = new TextEncoder(); const userValue = encoder.encode(authToken); const secretValue = encoder.encode(secret); // Do not return early when lengths differ — that leaks the secret's // length through timing. Instead, always perform a constant-time // comparison: when the lengths match compare directly; otherwise // compare the user input against itself (always true) and negate. const lengthsMatch = userValue.byteLength === secretValue.byteLength; const isEqual = lengthsMatch ? crypto.subtle.timingSafeEqual(userValue, secretValue) : !crypto.subtle.timingSafeEqual(userValue, userValue); if (!isEqual) { return c.text("Unauthorized", 401); } // If we got here, the auth token is valid await next(); }); // Protected route app.get('*', (c) => { return c.text("Welcome!"); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/#page","headline":"Using timingSafeEqual · Cloudflare Workers docs","description":"Protect against timing attacks by safely comparing values using timingSafeEqual.","url":"https://developers.cloudflare.com/workers/examples/protect-against-timing-attacks/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Security","WebCrypto","TypeScript","Python"]} ``` --- --- description: Serve an HTML form, then read POST requests. Use also to read JSON or POST data from an incoming request. title: Read POST image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Read POST Serve an HTML form, then read POST requests. Use also to read JSON or POST data from an incoming request. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/read-post/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/read-post) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { /** * rawHtmlResponse returns HTML inputted directly * into the worker script * @param {string} html */ function rawHtmlResponse(html) { return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); } /** * readRequestBody reads in the incoming request body * Use await readRequestBody(..) in an async function to get the string * @param {Request} request the incoming request to read from */ async function readRequestBody(request) { const contentType = request.headers.get("content-type"); if (contentType.includes("application/json")) { return JSON.stringify(await request.json()); } else if (contentType.includes("application/text")) { return request.text(); } else if (contentType.includes("text/html")) { return request.text(); } else if (contentType.includes("form")) { const formData = await request.formData(); const body = {}; for (const entry of formData.entries()) { body[entry[0]] = entry[1]; } return JSON.stringify(body); } else { // Perhaps some other type of data was submitted in the form // like an image, or some other binary data. return "a file"; } } const { url } = request; if (url.includes("form")) { return rawHtmlResponse(someForm); } if (request.method === "POST") { const reqBody = await readRequestBody(request); const retBody = `The request body sent in was ${reqBody}`; return new Response(retBody); } else if (request.method === "GET") { return new Response("The request was a GET"); } }, }; ``` ```ts export default { async fetch(request): Promise { /** * rawHtmlResponse returns HTML inputted directly * into the worker script * @param {string} html */ function rawHtmlResponse(html) { return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); } /** * readRequestBody reads in the incoming request body * Use await readRequestBody(..) in an async function to get the string * @param {Request} request the incoming request to read from */ async function readRequestBody(request: Request) { const contentType = request.headers.get("content-type"); if (contentType.includes("application/json")) { return JSON.stringify(await request.json()); } else if (contentType.includes("application/text")) { return request.text(); } else if (contentType.includes("text/html")) { return request.text(); } else if (contentType.includes("form")) { const formData = await request.formData(); const body = {}; for (const entry of formData.entries()) { body[entry[0]] = entry[1]; } return JSON.stringify(body); } else { // Perhaps some other type of data was submitted in the form // like an image, or some other binary data. return "a file"; } } const { url } = request; if (url.includes("form")) { return rawHtmlResponse(someForm); } if (request.method === "POST") { const reqBody = await readRequestBody(request); const retBody = `The request body sent in was ${reqBody}`; return new Response(retBody); } else if (request.method === "GET") { return new Response("The request was a GET"); } }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint from js import Object, Response, Headers, JSON async def read_request_body(request): headers = request.headers content_type = headers["content-type"] or "" if "application/json" in content_type: return JSON.stringify(await request.json()) if "form" in content_type: form = await request.formData() data = Object.fromEntries(form.entries()) return JSON.stringify(data) return await request.text() class Default(WorkerEntrypoint): async def fetch(self, request): def raw_html_response(html): headers = Headers.new({"content-type": "text/html;charset=UTF-8"}.items()) return Response.new(html, headers=headers) if "form" in request.url: return raw_html_response("") if "POST" in request.method: req_body = await read_request_body(request) ret_body = f"The request body sent in was {req_body}" return Response.new(ret_body) return Response.new("The request was not POST") ``` ```rs use serde::{Deserialize, Serialize}; use worker::*; fn raw_html_response(html: &str) -> Result { Response::from_html(html) } #[derive(Deserialize, Serialize, Debug)] struct Payload { msg: String, } async fn read_request_body(mut req: Request) -> String { let ctype = req.headers().get("content-type").unwrap().unwrap(); match ctype.as_str() { "application/json" => format!("{:?}", req.json::().await.unwrap()), "text/html" => req.text().await.unwrap(), "multipart/form-data" => format!("{:?}", req.form_data().await.unwrap()), _ => String::from("a file"), } } #[event(fetch)] async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { if String::from(req.url()?).contains("form") { return raw_html_response("some html form"); } match req.method() { Method::Post => { let req_body = read_request_body(req).await; Response::ok(format!("The request body sent in was {}", req_body)) } _ => Response::ok(format!("The result was a {:?}", req.method())), } } ``` ```ts import { Hono } from "hono"; import { html } from "hono/html"; const app = new Hono(); /** * readRequestBody reads in the incoming request body * @param {Request} request the incoming request to read from */ async function readRequestBody(request: Request): Promise { const contentType = request.headers.get("content-type") || ""; if (contentType.includes("application/json")) { const body = await request.json(); return JSON.stringify(body); } else if (contentType.includes("application/text")) { return request.text(); } else if (contentType.includes("text/html")) { return request.text(); } else if (contentType.includes("form")) { const formData = await request.formData(); const body: Record = {}; for (const [key, value] of formData.entries()) { body[key] = value.toString(); } return JSON.stringify(body); } else { // Perhaps some other type of data was submitted in the form // like an image, or some other binary data. return "a file"; } } const someForm = html`
`; app.get("*", async (c) => { const url = c.req.url; if (url.includes("form")) { return c.html(someForm); } return c.text("The request was a GET"); }); app.post("*", async (c) => { const reqBody = await readRequestBody(c.req.raw); const retBody = `The request body sent in was ${reqBody}`; return c.text(retBody); }); export default app; ``` Prevent potential errors when accessing request.body The body of a [Request ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request) can only be accessed once. If you previously used `request.formData()` in the same request, you may encounter a TypeError when attempting to access `request.body`. To avoid errors, create a clone of the Request object with `request.clone()` for each subsequent attempt to access a Request's body. Keep in mind that Workers have a [memory limit of 128 MB per Worker](https://developers.cloudflare.com/workers/platform/limits/#memory) and loading particularly large files into a Worker's memory multiple times may reach this limit. To ensure memory usage does not reach this limit, consider using [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/read-post/#page","headline":"Read POST · Cloudflare Workers docs","description":"Serve an HTML form, then read POST requests. Use also to read JSON or POST data from an incoming request.","url":"https://developers.cloudflare.com/workers/examples/read-post/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JSON","JavaScript","TypeScript","Python","Rust"]} ``` --- --- description: Redirect requests from one URL to another or from one set of URLs to another set. title: Redirect image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Redirect Redirect requests from one URL to another or from one set of URLs to another set. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/redirect/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/redirect) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ## Redirect all requests to one URL ```js export default { async fetch(request) { const destinationURL = "https://example.com"; const statusCode = 301; return Response.redirect(destinationURL, statusCode); }, }; ``` ```ts export default { async fetch(request): Promise { const destinationURL = "https://example.com"; const statusCode = 301; return Response.redirect(destinationURL, statusCode); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): def fetch(self, request): destinationURL = "https://example.com" statusCode = 301 return Response.redirect(destinationURL, statusCode) ``` ```rs use worker::*; #[event(fetch)] async fn fetch(_req: Request, _env: Env, _ctx: Context) -> Result { let destination_url = Url::parse("https://example.com")?; let status_code = 301; Response::redirect_with_status(destination_url, status_code) } ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.all("*", (c) => { const destinationURL = "https://example.com"; const statusCode = 301; return c.redirect(destinationURL, statusCode); }); export default app; ``` ## Redirect requests from one domain to another ```js export default { async fetch(request) { const base = "https://example.com"; const statusCode = 301; const url = new URL(request.url); const { pathname, search } = url; const destinationURL = `${base}${pathname}${search}`; console.log(destinationURL); return Response.redirect(destinationURL, statusCode); }, }; ``` ```ts export default { async fetch(request): Promise { const base = "https://example.com"; const statusCode = 301; const url = new URL(request.url); const { pathname, search } = url; const destinationURL = `${base}${pathname}${search}`; console.log(destinationURL); return Response.redirect(destinationURL, statusCode); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): base = "https://example.com" statusCode = 301 url = urlparse(request.url) destinationURL = f'{base}{url.path}{url.query}' print(destinationURL) return Response.redirect(destinationURL, statusCode) ``` ```rs use worker::*; #[event(fetch)] async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { let mut base = Url::parse("https://example.com")?; let status_code = 301; let url = req.url()?; base.set_path(url.path()); base.set_query(url.query()); console_log!("{:?}", base.to_string()); Response::redirect_with_status(base, status_code) } ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.all("*", (c) => { const base = "https://example.com"; const statusCode = 301; const { pathname, search } = new URL(c.req.url); const destinationURL = `${base}${pathname}${search}`; console.log(destinationURL); return c.redirect(destinationURL, statusCode); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/redirect/#page","headline":"Redirect · Cloudflare Workers docs","description":"Redirect requests from one URL to another or from one set of URLs to another set.","url":"https://developers.cloudflare.com/workers/examples/redirect/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","Redirects","JavaScript","TypeScript","Python","Rust"]} ``` --- --- description: Respond to the Worker request with the response from another website (example.com in this example). title: Respond with another site image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Respond with another site Respond to the Worker request with the response from another website (example.com in this example). Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/respond-with-another-site/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/respond-with-another-site) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { function MethodNotAllowed(request) { return new Response(`Method ${request.method} not allowed.`, { status: 405, headers: { Allow: "GET", }, }); } // Only GET requests work with this proxy. if (request.method !== "GET") return MethodNotAllowed(request); return fetch(`https://example.com`); }, }; ``` ```ts export default { async fetch(request): Promise { function MethodNotAllowed(request) { return new Response(`Method ${request.method} not allowed.`, { status: 405, headers: { Allow: "GET", }, }); } // Only GET requests work with this proxy. if (request.method !== "GET") return MethodNotAllowed(request); return fetch(`https://example.com`); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): def fetch(self, request): def method_not_allowed(request): msg = f'Method {request.method} not allowed.' headers = {"Allow": "GET"} return Response(msg, headers=headers, status=405) # Only GET requests work with this proxy. if request.method != "GET": return method_not_allowed(request) return fetch("https://example.com") ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/respond-with-another-site/#page","headline":"Respond with another site · Cloudflare Workers docs","description":"Respond to the Worker request with the response from another website (example.com in this example).","url":"https://developers.cloudflare.com/workers/examples/respond-with-another-site/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","JavaScript","TypeScript","Python"]} ``` --- --- description: Deliver an HTML page from an HTML string directly inside the Worker script. title: Return small HTML page image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Return small HTML page Deliver an HTML page from an HTML string directly inside the Worker script. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/return-html/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/return-html) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const html = `

Hello World

This markup was generated by a Cloudflare Worker.

`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const html = `

Hello World

This markup was generated by a Cloudflare Worker.

`; return new Response(html, { headers: { "content-type": "text/html;charset=UTF-8", }, }); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): html = """

Hello World

This markup was generated by a Cloudflare Worker.

""" headers = {"content-type": "text/html;charset=UTF-8"} return Response(html, headers=headers) ``` ```rs use worker::*; #[event(fetch)] async fn fetch(_req: Request, _env: Env, _ctx: Context) -> Result { let html = r#"

Hello World

This markup was generated by a Cloudflare Worker.

"#; Response::from_html(html) } ``` ```ts import { Hono } from "hono"; import { html } from "hono/html"; const app = new Hono(); app.get("*", (c) => { const doc = html`

Hello World

This markup was generated by a Cloudflare Worker with Hono.

`; return c.html(doc); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/return-html/#page","headline":"Return small HTML page · Cloudflare Workers docs","description":"Deliver an HTML page from an HTML string directly inside the Worker script.","url":"https://developers.cloudflare.com/workers/examples/return-html/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python","Rust"]} ``` --- --- description: Return JSON directly from a Worker script, useful for building APIs and middleware. title: Return JSON image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Return JSON Return JSON directly from a Worker script, useful for building APIs and middleware. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/return-json/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/return-json) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const data = { hello: "world", }; return Response.json(data); }, }; ``` ```ts export default { async fetch(request): Promise { const data = { hello: "world", }; return Response.json(data); }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint, Response import json class Default(WorkerEntrypoint): def fetch(self, request): data = json.dumps({"hello": "world"}) headers = {"content-type": "application/json"} return Response(data, headers=headers) ``` ```rs use serde::{Deserialize, Serialize}; use worker::*; #[derive(Deserialize, Serialize, Debug)] struct Json { hello: String, } #[event(fetch)] async fn fetch(_req: Request, _env: Env, _ctx: Context) -> Result { let data = Json { hello: String::from("world"), }; Response::from_json(&data) } ``` ```ts import { Hono } from "hono"; const app = new Hono(); app.get("*", (c) => { const data = { hello: "world", }; return c.json(data); }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/return-json/#page","headline":"Return JSON · Cloudflare Workers docs","description":"Return JSON directly from a Worker script, useful for building APIs and middleware.","url":"https://developers.cloudflare.com/workers/examples/return-json/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JSON","JavaScript","TypeScript","Python","Rust"]} ``` --- --- description: Rewrite URL links in HTML using the HTMLRewriter. This is useful for JAMstack websites. title: Rewrite links image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Rewrite links Rewrite URL links in HTML using the HTMLRewriter. This is useful for JAMstack websites. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/rewrite-links/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/rewrite-links) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. ```js export default { async fetch(request) { const OLD_URL = "developer.mozilla.org"; const NEW_URL = "mynewdomain.com"; class AttributeRewriter { constructor(attributeName) { this.attributeName = attributeName; } element(element) { const attribute = element.getAttribute(this.attributeName); if (attribute) { element.setAttribute( this.attributeName, attribute.replace(OLD_URL, NEW_URL), ); } } } const rewriter = new HTMLRewriter() .on("a", new AttributeRewriter("href")) .on("img", new AttributeRewriter("src")); const res = await fetch(request); const contentType = res.headers.get("Content-Type"); // If the response is HTML, it can be transformed with // HTMLRewriter -- otherwise, it should pass through if (contentType.startsWith("text/html")) { return rewriter.transform(res); } else { return res; } }, }; ``` ```ts export default { async fetch(request): Promise { const OLD_URL = "developer.mozilla.org"; const NEW_URL = "mynewdomain.com"; class AttributeRewriter { constructor(attributeName) { this.attributeName = attributeName; } element(element) { const attribute = element.getAttribute(this.attributeName); if (attribute) { element.setAttribute( this.attributeName, attribute.replace(OLD_URL, NEW_URL), ); } } } const rewriter = new HTMLRewriter() .on("a", new AttributeRewriter("href")) .on("img", new AttributeRewriter("src")); const res = await fetch(request); const contentType = res.headers.get("Content-Type"); // If the response is HTML, it can be transformed with // HTMLRewriter -- otherwise, it should pass through if (contentType.startsWith("text/html")) { return rewriter.transform(res); } else { return res; } }, } satisfies ExportedHandler; ``` ```py from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import HTMLRewriter, fetch class AttributeRewriter: old_url = "developer.mozilla.org" new_url = "mynewdomain.com" def __init__(self, attr_name): self.attr_name = attr_name def element(self, element): attr = element.getAttribute(self.attr_name) if attr: element.setAttribute( self.attr_name, attr.replace(self.old_url, self.new_url) ) href = create_proxy(AttributeRewriter("href")) src = create_proxy(AttributeRewriter("src")) rewriter = HTMLRewriter.new().on("a", href).on("img", src) class Default(WorkerEntrypoint): async def fetch(self, request): res = await fetch(request) content_type = res.headers["Content-Type"] # If the response is HTML, it can be transformed with # HTMLRewriter -- otherwise, it should pass through if content_type.startswith("text/html"): return rewriter.transform(res) return res ``` ```ts import { Hono } from 'hono'; import { html } from 'hono/html'; const app = new Hono(); app.get('*', async (c) => { const OLD_URL = "developer.mozilla.org"; const NEW_URL = "mynewdomain.com"; class AttributeRewriter { attributeName: string; constructor(attributeName: string) { this.attributeName = attributeName; } element(element: Element) { const attribute = element.getAttribute(this.attributeName); if (attribute) { element.setAttribute( this.attributeName, attribute.replace(OLD_URL, NEW_URL) ); } } } // Make a fetch request using the original request const res = await fetch(c.req.raw); const contentType = res.headers.get("Content-Type") || ""; // If the response is HTML, transform it with HTMLRewriter if (contentType.startsWith("text/html")) { const rewriter = new HTMLRewriter() .on("a", new AttributeRewriter("href")) .on("img", new AttributeRewriter("src")); return new Response(rewriter.transform(res).body, { headers: res.headers }); } else { // Pass through the response as is return res; } }); export default app; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/rewrite-links/#page","headline":"Rewrite links · Cloudflare Workers docs","description":"Rewrite URL links in HTML using the HTMLRewriter. This is useful for JAMstack websites.","url":"https://developers.cloudflare.com/workers/examples/rewrite-links/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Set common security headers (X-XSS-Protection, X-Frame-Options, X-Content-Type-Options, Permissions-Policy, Referrer-Policy, Strict-Transport-Security, Content-Security-Policy). title: Set security headers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Set security headers Set common security headers (X-XSS-Protection, X-Frame-Options, X-Content-Type-Options, Permissions-Policy, Referrer-Policy, Strict-Transport-Security, Content-Security-Policy). Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/security-headers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If you want to get started quickly, click on the button below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/security-headers) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. To inject CSP nonces into inline ``, { html: true }, ); } }, }) .transform(shell); }, }; async function fetchBootstrapData(env, pathname, headers) { try { const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, { headers: { Cookie: headers.get("Cookie") || "", "X-Request-Path": pathname, }, }); if (!res.ok) return null; return await res.json(); } catch { // If the API is down, the shell still loads and the SPA // falls back to client-side data fetching. return null; } } ``` ```ts // Env is generated by `wrangler types` — run it whenever you change your config. // Do not manually define Env — it drifts from your actual bindings. export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // Serve root-level static files (favicon.ico, robots.txt) directly. // Hashed assets under /assets/* skip the Worker entirely via run_worker_first. if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) { return env.ASSETS.fetch(request); } // Start fetching bootstrap data immediately — do not await yet. const dataPromise = fetchBootstrapData(env, url.pathname, request.headers); // Fetch the SPA shell from static assets (co-located, sub-millisecond). const shell = await env.ASSETS.fetch( new Request(new URL("/index.html", request.url)), ); // Use HTMLRewriter to stream the shell and inject data into . return new HTMLRewriter() .on("body", { async element(el) { const data = await dataPromise; if (data) { el.prepend( ``, { html: true }, ); } }, }) .transform(shell); }, } satisfies ExportedHandler; async function fetchBootstrapData( env: Env, pathname: string, headers: Headers, ): Promise { try { const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, { headers: { Cookie: headers.get("Cookie") || "", "X-Request-Path": pathname, }, }); if (!res.ok) return null; return await res.json(); } catch { // If the API is down, the shell still loads and the SPA // falls back to client-side data fetching. return null; } } ``` --- ## Option 2: SPA hosted on an external origin Use this variant when your HTML, CSS, and JavaScript are deployed outside Cloudflare. The Worker fetches the SPA shell from the external origin, uses HTMLRewriter to inject bootstrap data, and streams the modified response to the browser. ### Configure the Worker Because the SPA is not in Workers Static Assets, you do not need an `assets` block. Instead, store the external origin URL as an environment variable. Attach the Worker to your domain with a [Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) or a [Route](https://developers.cloudflare.com/workers/configuration/routing/routes/). ```jsonc { "name": "my-spa-proxy", "main": "src/worker.ts", // Set this to today's date "compatibility_date": "2026-08-14", "compatibility_flags": ["nodejs_compat"], "vars": { "SPA_ORIGIN": "https://my-spa.example-hosting.com", "API_BASE_URL": "https://api.example.com", }, } ``` ```toml name = "my-spa-proxy" main = "src/worker.ts" # Set this to today's date compatibility_date = "2026-08-14" compatibility_flags = [ "nodejs_compat" ] [vars] SPA_ORIGIN = "https://my-spa.example-hosting.com" API_BASE_URL = "https://api.example.com" ``` ### Inject bootstrap data with HTMLRewriter The Worker fetches both the SPA shell and API data in parallel. When the SPA origin responds, HTMLRewriter streams the HTML while injecting bootstrap data into ``. Static assets (CSS, JS, images) are passed through to the external origin without modification. ```js // Env is generated by `wrangler types` — run it whenever you change your config. // Do not manually define Env — it drifts from your actual bindings. export default { async fetch(request, env) { const url = new URL(request.url); // Pass static asset requests through to the external origin unmodified. if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) { return fetch(new Request(`${env.SPA_ORIGIN}${url.pathname}`, request)); } // Start fetching bootstrap data immediately — do not await yet. const dataPromise = fetchBootstrapData(env, url.pathname, request.headers); // Fetch the SPA shell from the external origin. // SPA routers serve index.html for all routes. const shell = await fetch(`${env.SPA_ORIGIN}/index.html`); if (!shell.ok) { return new Response("Origin returned an error", { status: 502 }); } // Use HTMLRewriter to stream the shell and inject data into . return new HTMLRewriter() .on("body", { async element(el) { const data = await dataPromise; if (data) { el.prepend( ``, { html: true }, ); } }, }) .transform(shell); }, }; async function fetchBootstrapData(env, pathname, headers) { try { const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, { headers: { Cookie: headers.get("Cookie") || "", "X-Request-Path": pathname, }, }); if (!res.ok) return null; return await res.json(); } catch { // If the API is down, the shell still loads and the SPA // falls back to client-side data fetching. return null; } } ``` ```ts // Env is generated by `wrangler types` — run it whenever you change your config. // Do not manually define Env — it drifts from your actual bindings. export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); // Pass static asset requests through to the external origin unmodified. if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) { return fetch(new Request(`${env.SPA_ORIGIN}${url.pathname}`, request)); } // Start fetching bootstrap data immediately — do not await yet. const dataPromise = fetchBootstrapData(env, url.pathname, request.headers); // Fetch the SPA shell from the external origin. // SPA routers serve index.html for all routes. const shell = await fetch(`${env.SPA_ORIGIN}/index.html`); if (!shell.ok) { return new Response("Origin returned an error", { status: 502 }); } // Use HTMLRewriter to stream the shell and inject data into . return new HTMLRewriter() .on("body", { async element(el) { const data = await dataPromise; if (data) { el.prepend( ``, { html: true }, ); } }, }) .transform(shell); }, } satisfies ExportedHandler; async function fetchBootstrapData( env: Env, pathname: string, headers: Headers, ): Promise { try { const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, { headers: { Cookie: headers.get("Cookie") || "", "X-Request-Path": pathname, }, }); if (!res.ok) return null; return await res.json(); } catch { // If the API is down, the shell still loads and the SPA // falls back to client-side data fetching. return null; } } ``` ## Consume prefetched data in your SPA On the client, read `window.__BOOTSTRAP_DATA__` before making any API calls. If the data exists, use it directly. Otherwise, fall back to a normal fetch. ```tsx // React example — works the same way in Vue, Svelte, or any other framework. import { useEffect, useState } from "react"; function App() { const [data, setData] = useState(window.__BOOTSTRAP_DATA__ || null); const [loading, setLoading] = useState(!data); useEffect(() => { if (data) return; // Already have prefetched data — skip the API call. fetch("/api/bootstrap") .then((res) => res.json()) .then((result) => { setData(result); setLoading(false); }); }, []); if (loading) return ; return ; } ``` Add a type declaration so TypeScript recognizes the global property: ```ts declare global { interface Window { __BOOTSTRAP_DATA__?: unknown; } } ``` ## Additional injection techniques You can chain multiple HTMLRewriter handlers to inject more than bootstrap data. ### Set meta tags Inject Open Graph or other `` tags based on the request path. This gives social-media crawlers correct previews without a full server-side rendering framework. ```ts new HTMLRewriter() .on("head", { element(el) { el.append(``, { html: true, }); }, }) .transform(shell); ``` ### Add CSP nonces Generate a nonce per request and inject it into both the Content-Security-Policy header and each inline ``, { html: true }, ); }, }) .transform(shell); ``` ## Related resources * [HTMLRewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/) — Streaming HTML parser and transformer. * [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) — Serve static files alongside your Worker. * [Static Assets routing](https://developers.cloudflare.com/workers/static-assets/routing/) — Configure `run_worker_first` and `not_found_handling`. * [Static Assets binding](https://developers.cloudflare.com/workers/static-assets/binding/) — Reference for the `ASSETS` binding and routing options. * [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) — Attach a Worker to a domain as the origin. * [Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) — Run a Worker in front of an existing origin server. * [Workers Best Practices](https://developers.cloudflare.com/workers/best-practices/workers-best-practices/) — Code patterns and configuration guidance for Workers. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/spa-shell/#page","headline":"Single Page App (SPA) shell with bootstrap data · Cloudflare Workers docs","description":"Use HTMLRewriter to inject prefetched bootstrap data into an SPA shell, eliminating client-side data fetching on initial load. Works with Workers Static Assets or an externally hosted SPA.","url":"https://developers.cloudflare.com/workers/examples/spa-shell/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["TypeScript","SPA"]} ``` --- --- description: Parse and transform large JSON request and response bodies using streaming. title: Stream large JSON image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Stream large JSON Parse and transform large JSON request and response bodies using streaming. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/streaming-json/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Use the [Streams API](https://developers.cloudflare.com/workers/runtime-apis/streams/) to process JSON payloads that would exceed a Worker's 128 MB memory limit if fully buffered. Streaming allows you to parse and transform JSON data incrementally as it arrives. This is faster than buffering the entire payload into memory, as your Worker can start processing data incrementally, and allows your Worker to handle multi-gigabyte payloads or files within its memory limits. The [@streamparser/json-whatwg ↗](https://www.npmjs.com/package/@streamparser/json-whatwg) library provides a streaming JSON parser compatible with the Web Streams API. Install the dependency: ```sh npm install @streamparser/json-whatwg ``` ## Stream a JSON request body This example parses a large JSON request body and extracts specific fields without loading the entire payload into memory. ```ts import { JSONParser } from "@streamparser/json-whatwg"; export default { async fetch(request): Promise { const parser = new JSONParser({ paths: ["$.users.*"] }); const users: string[] = []; // Pipe the request body through the JSON parser const reader = request.body .pipeThrough(parser) .getReader(); // Process matching JSON values as they stream in while (true) { const { done, value } = await reader.read(); if (done) break; // Extract only the name field from each user object if (value.value?.name) { users.push(value.value.name); } } return Response.json({ userNames: users }); }, } satisfies ExportedHandler; ``` ```js import { JSONParser } from "@streamparser/json-whatwg"; export default { async fetch(request) { const parser = new JSONParser({ paths: ["$.users.*"] }); const users = []; // Pipe the request body through the JSON parser const reader = request.body .pipeThrough(parser) .getReader(); // Process matching JSON values as they stream in while (true) { const { done, value } = await reader.read(); if (done) break; // Extract only the name field from each user object if (value.value?.name) { users.push(value.value.name); } } return Response.json({ userNames: users }); }, }; ``` ## Stream and transform a JSON response This example fetches a large JSON response from an upstream API, transforms specific fields, and streams the modified response to the client. ```ts import { JSONParser } from "@streamparser/json-whatwg"; export default { async fetch(request): Promise { const response = await fetch("https://api.example.com/large-dataset.json"); const parser = new JSONParser({ paths: ["$.items.*"] }); const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); const encoder = new TextEncoder(); // Process the upstream response in the background (async () => { const reader = response.body .pipeThrough(parser) .getReader(); await writer.write(encoder.encode('{"processedItems":[')); let first = true; while (true) { const { done, value } = await reader.read(); if (done) break; // Transform each item as it streams through const item = value.value; const transformed = { id: item.id, title: item.title.toUpperCase(), processed: true, }; if (!first) await writer.write(encoder.encode(",")); first = false; await writer.write(encoder.encode(JSON.stringify(transformed))); } await writer.write(encoder.encode("]}")); await writer.close(); })(); return new Response(readable, { headers: { "Content-Type": "application/json" }, }); }, } satisfies ExportedHandler; ``` ```js import { JSONParser } from "@streamparser/json-whatwg"; export default { async fetch(request) { const response = await fetch("https://api.example.com/large-dataset.json"); const parser = new JSONParser({ paths: ["$.items.*"] }); const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); const encoder = new TextEncoder(); // Process the upstream response in the background (async () => { const reader = response.body .pipeThrough(parser) .getReader(); await writer.write(encoder.encode('{"processedItems":[')); let first = true; while (true) { const { done, value } = await reader.read(); if (done) break; // Transform each item as it streams through const item = value.value; const transformed = { id: item.id, title: item.title.toUpperCase(), processed: true, }; if (!first) await writer.write(encoder.encode(",")); first = false; await writer.write(encoder.encode(JSON.stringify(transformed))); } await writer.write(encoder.encode("]}")); await writer.close(); })(); return new Response(readable, { headers: { "Content-Type": "application/json" }, }); }, }; ``` ## Related resources * [Streams API](https://developers.cloudflare.com/workers/runtime-apis/streams/) \- Learn more about streaming in Workers * [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/) \- Create custom stream transformations * [@streamparser/json-whatwg ↗](https://www.npmjs.com/package/@streamparser/json-whatwg) \- Streaming JSON parser documentation Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/streaming-json/#page","headline":"Stream large JSON · Cloudflare Workers docs","description":"Parse and transform large JSON request and response bodies using streaming.","url":"https://developers.cloudflare.com/workers/examples/streaming-json/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Middleware","JSON","JavaScript","TypeScript"]} ``` --- --- description: Inject [Turnstile](/turnstile/) implicitly into HTML elements using the HTMLRewriter runtime API. title: Turnstile with Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Turnstile with Workers Inject [Turnstile](https://developers.cloudflare.com/turnstile/) implicitly into HTML elements using the HTMLRewriter runtime API. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/turnstile-html-rewriter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ```js export default { async fetch(request, env) { const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret) const TURNSTILE_ATTR_NAME = "your_id_to_replace"; // The id of the element to put a Turnstile widget in let res = await fetch(request); // Instantiate the API to run on specific elements, for example, `head`, `div` let newRes = new HTMLRewriter() // `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API .on("head", { element(element) { // In this case, you are using `append` to add a new script to the `head` element element.append( ``, { html: true }, ); }, }) .on("div", { element(element) { // Add a turnstile widget element into if an element with the id of TURNSTILE_ATTR_NAME is found if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) { element.append( `
`, { html: true }, ); } }, }) .transform(res); return newRes; }, }; ``` ```ts export default { async fetch(request, env): Promise { const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret) const TURNSTILE_ATTR_NAME = "your_id_to_replace"; // The id of the element to put a Turnstile widget in let res = await fetch(request); // Instantiate the API to run on specific elements, for example, `head`, `div` let newRes = new HTMLRewriter() // `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API .on("head", { element(element) { // In this case, you are using `append` to add a new script to the `head` element element.append( ``, { html: true }, ); }, }) .on("div", { element(element) { // Add a turnstile widget element into if an element with the id of TURNSTILE_ATTR_NAME is found if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) { element.append( `
`, { html: true }, ); } }, }) .transform(res); return newRes; }, } satisfies ExportedHandler; ``` ```ts import { Hono } from "hono"; interface Env { SITE_KEY: string; SECRET_KEY: string; TURNSTILE_ATTR_NAME?: string; } const app = new Hono<{ Bindings: Env }>(); // Middleware to inject Turnstile widget app.use("*", async (c, next) => { const SITE_KEY = c.env.SITE_KEY; // The Turnstile Sitekey from environment const TURNSTILE_ATTR_NAME = c.env.TURNSTILE_ATTR_NAME || "your_id_to_replace"; // The target element ID // Process the request through the original endpoint await next(); // Only process HTML responses const contentType = c.res.headers.get("content-type"); if (!contentType || !contentType.includes("text/html")) { return; } // Clone the response to make it modifiable const originalResponse = c.res; const responseBody = await originalResponse.text(); // Create an HTMLRewriter instance to modify the HTML const rewriter = new HTMLRewriter() // Add the Turnstile script to the head .on("head", { element(element) { element.append( ``, { html: true }, ); }, }) // Add the Turnstile widget to the target div .on("div", { element(element) { if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) { element.append( `
`, { html: true }, ); } }, }); // Create a new response with the same properties as the original const modifiedResponse = new Response(responseBody, { status: originalResponse.status, statusText: originalResponse.statusText, headers: originalResponse.headers, }); // Transform the response using HTMLRewriter c.res = rewriter.transform(modifiedResponse); }); // Handle POST requests for form submission with Turnstile validation app.post("*", async (c) => { const formData = await c.req.formData(); const token = formData.get("cf-turnstile-response"); const ip = c.req.header("CF-Connecting-IP"); // If no token, return an error if (!token) { return c.text("Missing Turnstile token", 400); } // Prepare verification data const verifyFormData = new FormData(); verifyFormData.append("secret", c.env.SECRET_KEY || ""); verifyFormData.append("response", token.toString()); if (ip) verifyFormData.append("remoteip", ip); // Verify the token with Turnstile API const verifyResult = await fetch( "https://challenges.cloudflare.com/turnstile/v0/siteverify", { method: "POST", body: verifyFormData, }, ); const outcome = await verifyResult.json<{ success: boolean }>; // If verification fails, return an error if (!outcome.success) { return c.text("The provided Turnstile token was not valid!", 401); } // If verification succeeds, proceed with the original request // You would typically handle the form submission logic here // For this example, we'll just send a success response return c.text("Form submission successful!"); }); // Default handler for GET requests app.get("*", async (c) => { // Fetch the original content (you'd replace this with your actual content source) return await fetch(c.req.raw); }); export default app; ``` ```py from workers import WorkerEntrypoint from pyodide.ffi import create_proxy from js import HTMLRewriter, fetch class Default(WorkerEntrypoint): async def fetch(self, request): site_key = self.env.SITE_KEY attr_name = self.env.TURNSTILE_ATTR_NAME res = await fetch(request) class Append: def element(self, element): s = '' element.append(s, {"html": True}) class AppendOnID: def __init__(self, name): self.name = name def element(self, element): # You are using the `getAttribute` method here to retrieve the `id` or `class` of an element if element.getAttribute("id") == self.name: div = f'
' element.append(div, { "html": True }) # Instantiate the API to run on specific elements, for example, `head`, `div` head = create_proxy(Append()) div = create_proxy(AppendOnID(attr_name)) new_res = HTMLRewriter.new().on("head", head).on("div", div).transform(res) return new_res ``` Note This is only half the implementation for Turnstile. The corresponding token that is a result of a widget being rendered also needs to be verified using the [Siteverify API](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/). Refer to the example below for one such implementation. ```js async function handlePost(request, env) { const body = await request.formData(); // Turnstile injects a token in `cf-turnstile-response`. const token = body.get('cf-turnstile-response'); const ip = request.headers.get('CF-Connecting-IP'); // Validate the token by calling the `/siteverify` API. let formData = new FormData(); // `secret_key` here is the Turnstile Secret key, which should be set using Wrangler secrets formData.append('secret', self.env.SECRET_KEY); formData.append('response', token); formData.append('remoteip', ip); //This is optional. const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; const result = await fetch(url, { body: formData, method: 'POST', }); const outcome = await result.json(); if (!outcome.success) { return new Response('The provided Turnstile token was not valid!', { status: 401 }); } // The Turnstile token was successfully validated. Proceed with your application logic. // Validate login, redirect user, etc. // Clone the original request with a new body const newRequest = new Request(request, { body: request.body, // Reuse the body method: request.method, headers: request.headers }); return await fetch(newRequest); } export default { async fetch(request, env) { const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret) const TURNSTILE_ATTR_NAME = 'your_id_to_replace'; // The id of the element to put a Turnstile widget in let res = await fetch(request) if (request.method === 'POST') { return handlePost(request, env) } // Instantiate the API to run on specific elements, for example, `head`, `div` let newRes = new HTMLRewriter() // `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API .on('head', { element(element) { // In this case, you are using `append` to add a new script to the `head` element element.append(``, { html: true }); }, }) .on('div', { element(element) { // You are using the `getAttribute` method here to retrieve the `id` or `class` of an element if (element.getAttribute('id') === ) { element.append(`
`, { html: true }); } }, }) .transform(res); return newRes } } ``` Prevent potential errors when accessing request.body The body of a [Request ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request) can only be accessed once. If you previously used `request.formData()` in the same request, you may encounter a TypeError when attempting to access `request.body`. To avoid errors, create a clone of the Request object with `request.clone()` for each subsequent attempt to access a Request's body. Keep in mind that Workers have a [memory limit of 128 MB per Worker](https://developers.cloudflare.com/workers/platform/limits/#memory) and loading particularly large files into a Worker's memory multiple times may reach this limit. To ensure memory usage does not reach this limit, consider using [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/turnstile-html-rewriter/#page","headline":"Turnstile with Workers · Cloudflare Workers docs","description":"Inject Turnstile implicitly into HTML elements using the HTMLRewriter runtime API.","url":"https://developers.cloudflare.com/workers/examples/turnstile-html-rewriter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","TypeScript","Python"]} ``` --- --- description: Use the WebSockets API to communicate in real time with your Cloudflare Workers. title: Using the WebSockets API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Using the WebSockets API Use the WebSockets API to communicate in real time with your Cloudflare Workers. Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/examples/websockets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) WebSockets allow you to communicate in real time with your Cloudflare Workers serverless functions. In this guide, you will learn the basics of WebSockets on Cloudflare Workers, both from the perspective of writing WebSocket servers in your Workers functions, as well as connecting to and working with those WebSocket servers as a client. WebSockets are open connections sustained between the client and the origin server. Inside a WebSocket connection, the client and the origin can pass data back and forth without having to reestablish sessions. This makes exchanging data within a WebSocket connection fast. WebSockets are often used for real-time applications such as live chat and gaming. Note WebSockets utilize an event-based system for receiving and sending messages, much like the Workers runtime model of responding to events. Note If your application needs to coordinate among multiple WebSocket connections, such as a chat room or game match, you will need clients to send messages to a single-point-of-coordination. Durable Objects provide a single-point-of-coordination for Cloudflare Workers, and are often used in parallel with WebSockets to persist state over multiple clients and connections. In this case, refer to [Durable Objects](https://developers.cloudflare.com/durable-objects/) to get started, and prefer using the Durable Objects' extended [WebSockets API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/). ## Write a WebSocket Server WebSocket servers in Cloudflare Workers allow you to receive messages from a client in real time. This guide will show you how to set up a WebSocket server in Workers. A client can make a WebSocket request in the browser by instantiating a new instance of `WebSocket`, passing in the URL for your Workers function: ```js // In client-side JavaScript, connect to your Workers function using WebSockets: const websocket = new WebSocket( "wss://example-websocket.signalnerve.workers.dev", ); ``` Note For more details about creating and working with WebSockets in the client, refer to [Writing a WebSocket client](#write-a-websocket-client). When an incoming WebSocket request reaches the Workers function, it will contain an `Upgrade` header, set to the string value `websocket`. Check for this header before continuing to instantiate a WebSocket: ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Expected Upgrade: websocket', { status: 426 }); } } ``` ```rs use worker::*; #[event(fetch)] async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { let upgrade_header = match req.headers().get("Upgrade") { Some(h) => h.to_str().unwrap(), None => "", }; if upgrade_header != "websocket" { return worker::Response::error("Expected Upgrade: websocket", 426); } } ``` After you have appropriately checked for the `Upgrade` header, you can create a new instance of `WebSocketPair`, which contains server and client WebSockets. One of these WebSockets should be handled by the Workers function and the other should be returned as part of a `Response` with the [101 status code ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/101), indicating the request is switching protocols: ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Expected Upgrade: websocket', { status: 426 }); } const webSocketPair = new WebSocketPair(); const client = webSocketPair[0], server = webSocketPair[1]; return new Response(null, { status: 101, webSocket: client, }); } ``` ```rs use worker::*; #[event(fetch)] async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { let upgrade_header = match req.headers().get("Upgrade") { Some(h) => h.to_str().unwrap(), None => "", }; if upgrade_header != "websocket" { return worker::Response::error("Expected Upgrade: websocket", 426); } let ws = WebSocketPair::new()?; let client = ws.client; let server = ws.server; server.accept()?; worker::Response::from_websocket(client) } ``` The `WebSocketPair` constructor returns an Object, with the `0` and `1` keys each holding a `WebSocket` instance as its value. It is common to grab the two WebSockets from this pair using [Object.values ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5Fobjects/Object/values) and [ES6 destructuring ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring%5Fassignment), as seen in the below example. In order to begin communicating with the `client` WebSocket in your Worker, call `accept` on the `server` WebSocket. This will tell the Workers runtime that it should listen for WebSocket data and keep the connection open with your `client` WebSocket: ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Expected Upgrade: websocket', { status: 426 }); } const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair); server.accept(); return new Response(null, { status: 101, webSocket: client, }); } ``` ```rs use worker::*; #[event(fetch)] async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { let upgrade_header = match req.headers().get("Upgrade") { Some(h) => h.to_str().unwrap(), None => "", }; if upgrade_header != "websocket" { return worker::Response::error("Expected Upgrade: websocket", 426); } let ws = WebSocketPair::new()?; let client = ws.client; let server = ws.server; server.accept()?; worker::Response::from_websocket(client) } ``` WebSockets emit a number of [Events](https://developers.cloudflare.com/workers/runtime-apis/websockets/#events) that can be connected to using `addEventListener`. The below example hooks into the `message` event and emits a `console.log` with the data from it: ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Expected Upgrade: websocket', { status: 426 }); } const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair); server.accept(); server.addEventListener('message', event => { console.log(event.data); }); return new Response(null, { status: 101, webSocket: client, }); } ``` ```rs use futures::StreamExt; use worker::*; #[event(fetch)] async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { let upgrade_header = match req.headers().get("Upgrade") { Some(h) => h.to_str().unwrap(), None => "", }; if upgrade_header != "websocket" { return worker::Response::error("Expected Upgrade: websocket", 426); } let ws = WebSocketPair::new()?; let client = ws.client; let server = ws.server; server.accept()?; wasm_bindgen_futures::spawn_local(async move { let mut event_stream = server.events().expect("could not open stream"); while let Some(event) = event_stream.next().await { match event.expect("received error in websocket") { WebsocketEvent::Message(msg) => server.send(&msg.text()).unwrap(), WebsocketEvent::Close(event) => console_log!("{:?}", event), } } }); worker::Response::from_websocket(client) } ``` ```ts import { Hono } from 'hono' import { upgradeWebSocket } from 'hono/cloudflare-workers' const app = new Hono() app.get( '*', upgradeWebSocket((c) => { return { onMessage(event, ws) { console.log('Received message from client:', event.data) ws.send(`Echo: ${event.data}`) }, onClose: () => { console.log('WebSocket closed:', event) }, onError: () => { console.error('WebSocket error:', event) }, } }) ) export default app; ``` ### Connect to the WebSocket server from a client Writing WebSocket clients that communicate with your Workers function is a two-step process: first, create the WebSocket instance, and then attach event listeners to it: ```js const websocket = new WebSocket( "wss://websocket-example.signalnerve.workers.dev", ); websocket.addEventListener("message", (event) => { console.log("Message received from server"); console.log(event.data); }); ``` WebSocket clients can send messages back to the server using the [send](https://developers.cloudflare.com/workers/runtime-apis/websockets/#send) function: ```js websocket.send("MESSAGE"); ``` When the WebSocket interaction is complete, the client can close the connection using [close](https://developers.cloudflare.com/workers/runtime-apis/websockets/#close): ```js websocket.close(); ``` For an example of this in practice, refer to the [websocket-template ↗](https://github.com/cloudflare/websocket-template) to get started with WebSockets. ## Write a WebSocket client Cloudflare Workers supports the `new WebSocket(url)` constructor. A Worker can establish a WebSocket connection to a remote server in the same manner as the client implementation described above. Additionally, Cloudflare supports establishing WebSocket connections by making a fetch request to a URL with the `Upgrade` header set. ```js async function websocket(url) { // Make a fetch request including `Upgrade: websocket` header. // The Workers Runtime will automatically handle other requirements // of the WebSocket protocol, like the Sec-WebSocket-Key header. let resp = await fetch(url, { headers: { Upgrade: "websocket", }, }); // If the WebSocket handshake completed successfully, then the // response has a `webSocket` property. let ws = resp.webSocket; if (!ws) { throw new Error("server didn't accept WebSocket"); } // Call accept() to indicate that you'll be handling the socket here // in JavaScript, as opposed to returning it on to a client. // You can pass { allowHalfOpen: true } if you need to coordinate // the close handshake manually (for example, when proxying). ws.accept(); // Now you can send and receive messages like before. ws.send("hello"); ws.addEventListener("message", (msg) => { console.log(msg.data); }); } ``` ## WebSocket close behavior With the [web\_socket\_auto\_reply\_to\_close](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#websocket-auto-reply-to-close) compatibility flag (enabled by default on compatibility dates on or after `2026-04-07`), the Workers runtime automatically replies to incoming Close frames and transitions `readyState` to `CLOSED` before firing the `close` event. You do not need to call `close()` in your `close` event handler, but doing so is safe (the call is silently ignored). If you need half-open behavior (for example, for WebSocket proxying), pass `{ allowHalfOpen: true }` to `accept()`. Note that `new WebSocket(url)` always auto-replies after this flag takes effect. To get half-open behavior for a client WebSocket, use the `fetch()`\-based pattern shown above and call `ws.accept({ allowHalfOpen: true })`. For more details, refer to [WebSocket close behavior](https://developers.cloudflare.com/workers/runtime-apis/websockets/#close-behavior). ## WebSocket compression Cloudflare Workers supports WebSocket compression. Refer to [WebSocket Compression](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#websocket-compression) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/examples/websockets/#page","headline":"Using the WebSockets API · Cloudflare Workers docs","description":"Use the WebSockets API to communicate in real time with your Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/examples/websockets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["WebSockets","JavaScript","Rust"]} ``` --- --- description: Step-by-step Workers tutorials and video guides to help you build projects on Cloudflare. title: Tutorials image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Tutorials Last updated May 19, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) View tutorials to help you get started with Workers. | Name | Last Updated | Difficulty | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | | [Build an Interactive ChatGPT App](https://developers.cloudflare.com/workers/demos/chatgpt-app/) | 9 months ago | Advanced | | [Deploy an Express.js application on Cloudflare Workers](https://developers.cloudflare.com/workers/tutorials/deploy-an-express-app/) | 10 months ago | Beginner | | [Connect to a PostgreSQL database with Cloudflare Workers](https://developers.cloudflare.com/workers/tutorials/postgres/) | last year | Beginner | | [Migrate from Netlify to Workers](https://developers.cloudflare.com/workers/static-assets/migration-guides/netlify-to-workers/) | last year | Beginner | | [Migrate from Vercel to Workers](https://developers.cloudflare.com/workers/static-assets/migration-guides/vercel-to-workers/) | last year | Beginner | | [Tutorial - React SPA with an API](https://developers.cloudflare.com/workers/vite-plugin/tutorial/) | last year | Beginner | | [Connect to a MySQL database with Cloudflare Workers](https://developers.cloudflare.com/workers/tutorials/mysql/) | last year | Beginner | | [Set up and use a Prisma Postgres database](https://developers.cloudflare.com/workers/tutorials/using-prisma-postgres-with-workers/) | 2 years ago | Beginner | | [Create a fine-tuned OpenAI model with R2](https://developers.cloudflare.com/workers/tutorials/create-finetuned-chatgpt-ai-models-with-r2/) | 2 years ago | Intermediate | | [Build a Slackbot](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/) | 2 years ago | Beginner | | [Use Workers KV directly from Rust](https://developers.cloudflare.com/workers/tutorials/workers-kv-from-rust/) | 2 years ago | Intermediate | | [Build a todo list Jamstack application](https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/) | 2 years ago | Beginner | | [Send Emails With Postmark](https://developers.cloudflare.com/workers/tutorials/send-emails-with-postmark/) | 2 years ago | Beginner | | [Send Emails With Resend](https://developers.cloudflare.com/workers/tutorials/send-emails-with-resend/) | 2 years ago | Beginner | | [GitHub SMS notifications using Twilio](https://developers.cloudflare.com/workers/tutorials/github-sms-notifications-using-twilio/) | 3 years ago | Beginner | | [Deploy a real-time chat application](https://developers.cloudflare.com/workers/tutorials/deploy-a-realtime-chat-app/) | 3 years ago | Intermediate | | [Build a QR code generator](https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/) | 3 years ago | Beginner | | [Securely access and upload assets with Cloudflare R2](https://developers.cloudflare.com/workers/tutorials/upload-assets-with-r2/) | 3 years ago | Beginner | | [OpenAI GPT function calling with JavaScript and Cloudflare Workers](https://developers.cloudflare.com/workers/tutorials/openai-function-calls-workers/) | 3 years ago | Beginner | | [Handle form submissions with Airtable](https://developers.cloudflare.com/workers/tutorials/handle-form-submissions-with-airtable/) | 3 years ago | Beginner | | [Connect to and query your Turso database using Workers](https://developers.cloudflare.com/workers/tutorials/connect-to-turso-using-workers/) | 3 years ago | Beginner | | [Generate YouTube thumbnails with Workers and Cloudflare Image Resizing](https://developers.cloudflare.com/workers/tutorials/generate-youtube-thumbnails-with-workers-and-images/) | 3 years ago | Intermediate | Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/tutorials/#page","headline":"Tutorials · Cloudflare Workers docs","description":"Step-by-step Workers tutorials and video guides to help you build projects on Cloudflare.","url":"https://developers.cloudflare.com/workers/tutorials/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-19","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: This tutorial explains how to build a todo list application using HTML, CSS, and JavaScript. title: Build a todo list Jamstack application image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build a todo list Jamstack application Last updated Mar 30, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will build a todo list application using HTML, CSS, and JavaScript. The application data will be stored in [Workers KV](https://developers.cloudflare.com/kv/api/). ![Preview of a finished todo list. Continue reading for instructions on how to set up a todo list.](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1378,height=730,format=webp/_astro/finished.CHDh55j7.png) Before starting this project, you should have some experience with HTML, CSS, and JavaScript. You will learn: 1. How building with Workers makes allows you to focus on writing code and ship finished products. 2. How the addition of Workers KV makes this tutorial a great introduction to building full, data-driven applications. If you would like to see the finished code for this project, find the [project on GitHub ↗](https://github.com/lauragift21/cloudflare-workers-todos) and refer to the [live demo ↗](https://todos.examples.workers.dev/) to review what you will be building. ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## 1\. Create a new Workers project First, use the [create-cloudflare ↗](https://www.npmjs.com/package/create-cloudflare) CLI tool to create a new Cloudflare Workers project named `todos`. In this tutorial, you will use the default `Hello World` template to create a Workers project. npmyarnpnpm ``` npm create cloudflare@latest -- todos ``` ``` yarn create cloudflare todos ``` ``` pnpm create cloudflare@latest todos ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `JavaScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Move into your newly created directory: ```sh cd todos ``` Inside of your new `todos` Worker project directory, `index.js` represents the entry point to your Cloudflare Workers application. All incoming HTTP requests to a Worker are passed to the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) as a [request](https://developers.cloudflare.com/workers/runtime-apis/request/) object. After a request is received by the Worker, the response your application constructs will be returned to the user. This tutorial will guide you through understanding how the request/response pattern works and how you can use it to build fully featured applications. ```js export default { async fetch(request, env, ctx) { return new Response("Hello World!"); }, }; ``` In your default `index.js` file, you can see that request/response pattern in action. The `fetch` constructs a new `Response` with the body text `'Hello World!'`. When a Worker receives a `request`, the Worker returns the newly constructed response to the client. Your Worker will serve new responses directly from [Cloudflare's global network ↗](https://www.cloudflare.com/network) instead of continuing to your origin server. A standard server would accept requests and return responses. Cloudflare Workers allows you to respond by constructing responses directly on the Cloudflare global network. ## 2\. Review project details Any project you deploy to Cloudflare Workers can make use of modern JavaScript tooling like [ES modules](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/), `npm` packages, and [async/await ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async%5Ffunction) functions to build your application. In addition to writing Workers, you can use Workers to [build full applications](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/) using the same tooling and process as in this tutorial. In this tutorial, you will build a todo list application running on Workers that allows reading data from a [KV](https://developers.cloudflare.com/kv/) store and using the data to populate an HTML response to send to the client. The work needed to create this application is split into three tasks: 1. Write data to KV. 2. Rendering data from KV. 3. Adding todos from the application UI. For the remainder of this tutorial you will complete each task, iterating on your application, and then publish it to your own domain. ## 3\. Write data to KV To begin, you need to understand how to populate your todo list with actual data. To do this, use [Cloudflare Workers KV](https://developers.cloudflare.com/kv/) — a key-value store that you can access inside of your Worker to read and write data. To get started with KV, set up a namespace. All of your cached data will be stored inside that namespace and, with configuration, you can access that namespace inside the Worker with a predefined variable. Use Wrangler to create a new namespace called `TODOS` with the [kv namespace create command](https://developers.cloudflare.com/workers/wrangler/commands/kv/#kv-namespace-create) and get the associated namespace ID by running the following command in your terminal: ```sh npx wrangler kv namespace create "TODOS" --preview ``` The associated namespace can be combined with a `--preview` flag to interact with a preview namespace instead of a production namespace. Namespaces can be added to your application by defining them inside your Wrangler configuration. Copy your newly created namespace ID, and in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/), define a `kv_namespaces` key to set up your namespace: ```jsonc { "kv_namespaces": [ { "binding": "TODOS", "id": "", "preview_id": "" } ] } ``` ```toml [[kv_namespaces]] binding = "TODOS" id = "" preview_id = "" ``` The defined namespace, `TODOS`, will now be available inside of your codebase. With that, it is time to understand the [KV API](https://developers.cloudflare.com/kv/api/). A KV namespace has three primary methods you can use to interface with your cache: `get`, `put`, and `delete`. Start storing data by defining an initial set of data, which you will put inside of the cache using the `put` method. The following example defines a `defaultData` object instead of an array of todo items. You may want to store metadata and other information inside of this cache object later on. Given that data object, use `JSON.stringify` to add a string into the cache: ```js export default { async fetch(request, env, ctx) { const defaultData = { todos: [ { id: 1, name: "Finish the Cloudflare Workers blog post", completed: false, }, ], }; await env.TODOS.put("data", JSON.stringify(defaultData)); return new Response("Hello World!"); }, }; ``` Workers KV is an eventually consistent, global datastore. Any writes within a region are immediately reflected within that same region but will not be immediately available in other regions. However, those writes will eventually be available everywhere and, at that point, Workers KV guarantees that data within each region will be consistent. Given the presence of data in the cache and the assumption that your cache is eventually consistent, this code needs a slight adjustment: the application should check the cache and use its value, if the key exists. If it does not, you will use `defaultData` as the data source for now (it should be set in the future) and write it to the cache for future use. After breaking out the code into a few functions for simplicity, the result looks like this: ```js export default { async fetch(request, env, ctx) { const defaultData = { todos: [ { id: 1, name: "Finish the Cloudflare Workers blog post", completed: false, }, ], }; const setCache = (data) => env.TODOS.put("data", data); const getCache = () => env.TODOS.get("data"); let data; const cache = await getCache(); if (!cache) { await setCache(JSON.stringify(defaultData)); data = defaultData; } else { data = JSON.parse(cache); } return new Response(JSON.stringify(data)); }, }; ``` ## Render data from KV Given the presence of data in your code, which is the cached data object for your application, you should take this data and render it in a user interface. To do this, make a new `html` variable in your Workers script and use it to build up a static HTML template that you can serve to the client. In `fetch`, construct a new `Response` with a `Content-Type: text/html` header and serve it to the client: ```js const html = ` Todos

Todos

`; async fetch (request, env, ctx) { // previous code return new Response(html, { headers: { 'Content-Type': 'text/html' } }); } ``` You have a static HTML site being rendered and you can begin populating it with data. In the body, add a `div` tag with an `id` of `todos`: ```js const html = ` Todos

Todos

`; ``` Add a ` `; ``` Your static page can take in `window.todos` and render HTML based on it, but you have not actually passed in any data from KV. To do this, you will need to make a few changes. First, your `html` variable will change to a function. The function will take in a `todos` argument, which will populate the `window.todos` variable in the above code sample: ```js const html = (todos) => ` `; ``` This code updates the cache. Remember that the KV cache is eventually consistent — even if you were to update your Worker to read from the cache and return it, you have no guarantees it will actually be up to date. Instead, update the list of todos locally, by taking your original code for rendering the todo list, making it a reusable function called `populateTodos`, and calling it when the page loads and when the cache request has finished: ```js const html = (todos) => ` `; ``` With the client-side code in place, deploying the new version of the function should put all these pieces together. The result is an actual dynamic todo list. ## 5\. Update todos from the application UI For the final piece of your todo list, you need to be able to update todos — specifically, marking them as completed. Luckily, a great deal of the infrastructure for this work is already in place. You can update the todo list data in the cache, as evidenced by your `createTodo` function. Performing updates on a todo is more of a client-side task than a Worker-side one. To start, the `populateTodos` function can be updated to generate a `div` for each todo. In addition, move the name of the todo into a child element of that `div`: ```js const html = (todos) => ` `; ``` You have designed the client-side part of this code to handle an array of todos and render a list of HTML elements. There is a number of things that you have been doing that you have not quite had a use for yet – specifically, the inclusion of IDs and updating the todo's completed state. These things work well together to actually support updating todos in the application UI. To start, it would be useful to attach the ID of each todo in the HTML. By doing this, you can then refer to the element later in order to correspond it to the todo in the JavaScript part of your code. Data attributes and the corresponding `dataset` method in JavaScript are a perfect way to implement this. When you generate your `div` element for each todo, you can attach a data attribute called todo to each `div`: ```js const html = (todos) => ` `; ``` Inside your HTML, each `div` for a todo now has an attached data attribute, which looks like: ```html
``` You can now generate a checkbox for each todo element. This checkbox will default to unchecked for new todos but you can mark it as checked as the element is rendered in the window: ```js const html = (todos) => ` `; ``` The checkbox is set up to correctly reflect the value of completed on each todo but it does not yet update when you actually check the box. To do this, attach the `completeTodo` function as an event listener on the `click` event. Inside the function, inspect the checkbox element, find its parent (the todo `div`), and use its `todo` data attribute to find the corresponding todo in the data array. You can toggle the completed status, update its properties, and rerender the UI: ```js const html = (todos) => ` `; ``` The final result of your code is a system that checks the `todos` variable, updates your Cloudflare KV cache with that value, and then does a re-render of the UI based on the data it has locally. ## 6\. Conclusion and next steps By completing this tutorial, you have built a static HTML, CSS, and JavaScript application that is transparently powered by Workers and Workers KV, which take full advantage of Cloudflare's global network. If you would like to keep improving on your project, you can implement a better design (you can refer to a live version available at [todos.signalnerve.workers.dev ↗](https://todos.signalnerve.workers.dev/)), or make additional improvements to security and speed. You may also want to add user-specific caching. Right now, the cache key is always `data` – this means that any visitor to the site will share the same todo list with other visitors. Within your Worker, you could use values from the client request to create and maintain user-specific lists. For example, you may generate a cache key based on the requesting IP: ```js export default { async fetch(request, env, ctx) { const defaultData = { todos: [ { id: 1, name: "Finish the Cloudflare Workers blog post", completed: false, }, ], }; const setCache = (key, data) => env.TODOS.put(key, data); const getCache = (key) => env.TODOS.get(key); const ip = request.headers.get("CF-Connecting-IP"); const myKey = `data-${ip}`; if (request.method === "PUT") { const body = await request.text(); try { JSON.parse(body); await setCache(myKey, body); return new Response(body, { status: 200 }); } catch (err) { return new Response(err, { status: 500 }); } } let data; const cache = await getCache(); if (!cache) { await setCache(myKey, JSON.stringify(defaultData)); data = defaultData; } else { data = JSON.parse(cache); } const body = html(JSON.stringify(data.todos).replace(/ ` Todos

Todos

`; export default { async fetch(request, env, ctx) { const defaultData = { todos: [ { id: 1, name: "Finish the Cloudflare Workers blog post", completed: false, }, ], }; const setCache = (key, data) => env.TODOS.put(key, data); const getCache = (key) => env.TODOS.get(key); const ip = request.headers.get("CF-Connecting-IP"); const myKey = `data-${ip}`; if (request.method === "PUT") { const body = await request.text(); try { JSON.parse(body); await setCache(myKey, body); return new Response(body, { status: 200 }); } catch (err) { return new Response(err, { status: 500 }); } } let data; const cache = await getCache(); if (!cache) { await setCache(myKey, JSON.stringify(defaultData)); data = defaultData; } else { data = JSON.parse(cache); } const body = html(JSON.stringify(data.todos).replace(/ Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build a QR code generator Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will build and publish a Worker application that generates QR codes. If you would like to review the code for this tutorial, the final version of the codebase is [available on GitHub ↗](https://github.com/kristianfreeman/workers-qr-code-generator). You can take the code provided in the example repository, customize it, and deploy it for use in your own projects. ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## 1\. Create a new Workers project First, use the [create-cloudflare CLI](https://developers.cloudflare.com/pages/get-started/c3) to create a new Cloudflare Workers project. To do this, open a terminal window and run the following command: npmyarnpnpm ``` npm create cloudflare@latest -- qr-code-generator ``` ``` yarn create cloudflare qr-code-generator ``` ``` pnpm create cloudflare@latest qr-code-generator ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `JavaScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Then, move into your newly created directory: ```sh cd qr-code-generator ``` Inside of your new `qr-code-generator` Worker project directory, `index.js` represents the entry point to your Cloudflare Workers application. All Cloudflare Workers applications start by listening for `fetch` events, which are triggered when a client makes a request to a Workers route. After a request is received by the Worker, the response your application constructs will be returned to the user. This tutorial will guide you through understanding how the request/response pattern works and how you can use it to build fully featured applications. ```js export default { async fetch(request, env, ctx) { return new Response("Hello Worker!"); }, }; ``` In your default `index.js` file, you can see that request/response pattern in action. The `fetch` constructs a new `Response` with the body text `'Hello Worker!'`. When a Worker receives a `fetch` event, the Worker returns the newly constructed response to the client. Your Worker will serve new responses directly from [Cloudflare's global network ↗](https://www.cloudflare.com/network) instead of continuing to your origin server. A standard server would accept requests and return responses. Cloudflare Workers allows you to respond quickly by constructing responses directly on the Cloudflare global network. ## 2\. Handle Incoming Request Any project you publish to Cloudflare Workers can make use of modern JavaScript tooling like ES modules, `npm` packages, and [async/await ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async%5Ffunction) functions to build your application. In addition to writing Workers, you can use Workers to [build full applications](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/) using the same tooling and process as in this tutorial. The QR code generator you will build in this tutorial will be a Worker that runs on a single route and receives requests. Each request will contain a text message (a URL, for example), which the function will encode into a QR code. The function will then respond with the QR code in PNG image format. At this point in the tutorial, your Worker function can receive requests and return a simple response with the text `"Hello Worker!"`. To handle data coming into your Worker, check if the incoming request is a `POST` request: ```js export default { async fetch(request, env, ctx) { if (request.method === "POST") { return new Response("Hello Worker!"); } }, }; ``` Currently, if an incoming request is not a `POST`, the function will return `undefined`. However, a Worker always needs to return a `Response`. Since the function should only accept incoming `POST` requests, return a new `Response` with a [405 status code ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/405) if the incoming request is not a `POST`: ```js export default { async fetch(request, env, ctx) { if (request.method === "POST") { return new Response("Hello Worker!"); } return new Response("Expected POST request", { status: 405, }); }, }; ``` You have established the basic flow of the request. You will now set up a response to incoming valid requests. If a `POST` request comes in, the function should generate a QR code. To start, move the `"Hello Worker!"` response into a new function, `generateQRCode`, which will ultimately contain the bulk of your function’s logic: ```js export default { async fetch(request, env, ctx) { if (request.method === "POST") { } }, }; async function generateQRCode(request) { // TODO: Include QR code generation return new Response("Hello worker!"); } ``` With the `generateQRCode` function filled out, call it within `fetch` function and return its result directly to the client: ```js export default { async fetch(request, env, ctx) { if (request.method === "POST") { return generateQRCode(request); } }, }; ``` ## 3\. Build a QR code generator All projects deployed to Cloudflare Workers support npm packages. This support makes it easy to rapidly build out functionality in your Workers. The ['qrcode-svg' ↗](https://github.com/papnkukn/qrcode-svg) package is a great way to take text and encode it into a QR code. In the command line, install and save 'qrcode-svg' to your project’s 'package.json': npmyarnpnpmbun ``` npm i qrcode-svg ``` ``` yarn add qrcode-svg ``` ``` pnpm add qrcode-svg ``` ``` bun add qrcode-svg ``` In `index.js`, import the `qrcode-svg` package as the variable `QRCode`. In the `generateQRCode` function, parse the incoming request as JSON using `request.json`, and generate a new QR code using the `qrcode-svg` package. The QR code is generated as an SVG. Construct a new instance of `Response`, passing in the SVG data as the body, and a `Content-Type` header of `image/svg+xml`. This will allow browsers to properly parse the data coming back from your Worker as an image: ```js import QRCode from "qrcode-svg"; async function generateQRCode(request) { const { text } = await request.json(); const qr = new QRCode({ content: text || "https://workers.dev" }); return new Response(qr.svg(), { headers: { "Content-Type": "image/svg+xml" }, }); } ``` ## 4\. Test in an application UI The Worker will execute when a user sends a `POST` request to a route, but it is best practice to also provide a proper interface for testing the function. At this point in the tutorial, if any request is received by your function that is not a `POST`, a `405` response is returned. The new version of `fetch` should return a new `Response` with a static HTML document instead of the `405` error: ```js export default { async fetch(request, env, ctx) { if (request.method === "POST") { return generateQRCode(request); } return new Response(landing, { headers: { "Content-Type": "text/html", }, }); }, }; async function generateQRCode(request) { const { text } = await request.json(); const qr = new QRCode({ content: text || "https://workers.dev" }); return new Response(qr.svg(), { headers: { "Content-Type": "image/svg+xml" }, }); } const landing = `

QR Generator

Click the below button to generate a new QR code. This will make a request to your Worker.

Generated QR Code Image

`; ``` The `landing` variable, which is a static HTML string, sets up an `input` tag and a corresponding `button`, which calls the `generateQRCode` function. This function will make an HTTP `POST` request back to your Worker, allowing you to see the corresponding QR code image returned on the page. With the above steps complete, your Worker is ready. The full version of the code looks like this: ```js const QRCode = require("qrcode-svg"); export default { async fetch(request, env, ctx) { if (request.method === "POST") { return generateQRCode(request); } return new Response(landing, { headers: { "Content-Type": "text/html", }, }); }, }; async function generateQRCode(request) { const { text } = await request.json(); const qr = new QRCode({ content: text || "https://workers.dev" }); return new Response(qr.svg(), { headers: { "Content-Type": "image/svg+xml" }, }); } const landing = `

QR Generator

Click the below button to generate a new QR code. This will make a request to your Worker.

Generated QR Code Image

`; ``` ## 5\. Deploy your Worker With all the above steps complete, you have written the code for a QR code generator on Cloudflare Workers. Wrangler has built-in support for bundling, uploading, and releasing your Cloudflare Workers application. To do this, run `npx wrangler deploy`, which will build and deploy your code. ```sh npx wrangler deploy ``` ## Related resources In this tutorial, you built and deployed a Worker application for generating QR codes. If you would like to see the full source code for this application, you can find it [on GitHub ↗](https://github.com/kristianfreeman/workers-qr-code-generator). If you want to get started building your own projects, review the existing list of [Quickstart templates](https://developers.cloudflare.com/workers/get-started/quickstarts/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/#page","headline":"Build a QR code generator · Cloudflare Workers docs","description":"This tutorial shows you how to build and publish a Worker application that generates QR codes. The final version of the codebase is available on GitHub.","url":"https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript"]} ``` --- --- description: Learn how to build a Slackbot with Hono and TypeScript in Cloudflare Workers title: Build a Slackbot image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build a Slackbot Last updated Apr 8, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will build a [Slack ↗](https://slack.com) bot using [Cloudflare Workers](https://developers.cloudflare.com/workers/). Your bot will make use of GitHub webhooks to send messages to a Slack channel when issues are updated or created, and allow users to write a command to look up GitHub issues from inside Slack. ![After following this tutorial, you will be able to create a Slackbot like the one in this example. Continue reading to build your Slackbot.](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1222,height=384,format=webp/_astro/issue-command.BJRwbx5d.png) This tutorial is recommended for people who are familiar with writing web applications. You will use TypeScript as the programming language and [Hono ↗](https://hono.dev/) as the web framework. If you have built an application with tools like [Node ↗](https://nodejs.org) and [Express ↗](https://expressjs.com), this project will feel very familiar to you. If you are new to writing web applications or have wanted to build something like a Slack bot in the past, but were intimidated by deployment or configuration, Workers will be a way for you to focus on writing code and shipping projects. If you would like to review the code or how the bot works in an actual Slack channel before proceeding with this tutorial, you can access the final version of the codebase [on GitHub ↗](https://github.com/yusukebe/workers-slack-bot). From GitHub, you can add your own Slack API keys and deploy it to your own Slack channels for testing. --- ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## Set up Slack This tutorial assumes that you already have a Slack account, and the ability to create and manage Slack applications. ### Configure a Slack application To post messages from your Cloudflare Worker into a Slack channel, you will need to create an application in Slack’s UI. To do this, go to Slack’s API section, at [api.slack.com/apps ↗](https://api.slack.com/apps), and select **Create New App**. ![To create a Slackbot, first create a Slack App](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2020,height=1605,format=webp/_astro/create-a-slack-app.D5_bKo4M.png) Slack applications have many features. You will make use of two of them, Incoming Webhooks and Slash Commands, to build your Worker-powered Slack bot. #### Incoming Webhook Incoming Webhooks are URLs that you can use to send messages to your Slack channels. Your incoming webhook will be paired with GitHub’s webhook support to send messages to a Slack channel whenever there are updates to issues in a given repository. You will see the code in more detail as you build your application. First, create a Slack webhook: 1. On the sidebar of Slack's UI, select **Incoming Webhooks**. 2. In **Webhook URLs for your Workspace**, select **Add New Webhook to Workspace**. 3. On the following screen, select the channel that you want your webhook to send messages to (you can select a room, like #general or #code, or be messaged directly by your Slack bot when the webhook is called.) 4. Authorize the new webhook URL. After authorizing your webhook URL, you will be returned to the **Incoming Webhooks** page and can view your new webhook URL. You will add this into your Workers code later. Next, you will add the second component to your Slack bot: a Slash Command. ![Select Add New Webhook to Workspace to add a new Webhook URL in Slack's dashboard](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1354,height=902,format=webp/_astro/slack-incoming-webhook.DWpFxzq_.png) #### Slash Command A Slash Command in Slack is a custom-configured command that can be attached to a URL request. For example, if you configured `/weather `, Slack would make an HTTP POST request to a configured URL, passing the text `` to get the weather for a specified zip code. In your application, you will use the `/issue` command to look up GitHub issues using the [GitHub API ↗](https://developer.github.com). Typing `/issue cloudflare/wrangler#1` will send the text `cloudflare/wrangler#1` in a HTTP POST request to your application, which the application will use to find the [relevant GitHub issue ↗](https://github.com/cloudflare/wrangler-legacy/issues/1). 1. On the Slack sidebar, select **Slash Commands**. 2. Create your first slash command. For this tutorial, you will use the command `/issue`. The request URL should be the `/lookup` path on your application URL: for example, if your application will be hosted at `https://myworkerurl.com`, the Request URL should be `https://myworkerurl.com/lookup`. ![You must create a Slash Command in Slack's dashboard and attach it to a Request URL](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1332,height=810,format=webp/_astro/create-slack-command.CBy2ieO7.png) ### Configure your GitHub Webhooks Your Cloudflare Workers application will be able to handle incoming requests from Slack. It should also be able to receive events directly from GitHub. If a GitHub issue is created or updated, you can make use of GitHub webhooks to send that event to your Workers application and post a corresponding message in Slack. To configure a webhook: 1. Go to your GitHub repository's **Settings** \> **Webhooks** \> **Add webhook**. If you have a repository like `https://github.com/user/repo`, you can access the **Webhooks** page directly at `https://github.com/user/repo/settings/hooks`. 1. Set the Payload URL to the `/webhook` path on your Worker URL. For example, if your Worker will be hosted at `https://myworkerurl.com`, the Payload URL should be `https://myworkerurl.com/webhook`. 1. In the **Content type** dropdown, select **application/json**. The **Content type** for your payload can either be a URL-encoded payload (`application/x-www-form-urlencoded`) or JSON (`application/json`). For the purpose of this tutorial and to make parsing the payload sent to your application, select JSON. 1. In **Which events would you like to trigger this webhook?**, select **Let me select individual events**. GitHub webhooks allow you to specify which events you would like to have sent to your webhook. By default, the webhook will send `push` events from your repository. For the purpose of this tutorial, you will choose **Let me select individual events**. 1. Select the **Issues** event type. There are many different event types that can be enabled for your webhook. Selecting **Issues** will send every issue-related event to your webhook, including when issues are opened, edited, deleted, and more. If you would like to expand your Slack bot application in the future, you can select more of these events after the tutorial. 1. Select **Add webhook**. ![Create a GitHub Webhook in the GitHub dashboard](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2068,height=2408,format=webp/_astro/new-github-webhook.DtHDy8MC.png) When your webhook is created, it will attempt to send a test payload to your application. Since your application is not actually deployed yet, leave the configuration as it is. You will later return to your repository to create, edit, and close some issues to ensure that the webhook is working once your application is deployed. ## Init To initiate the project, use the command line interface [C3 (create-cloudflare-cli) ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare). npmyarnpnpm ``` npm create cloudflare@latest -- slack-bot ``` ``` yarn create cloudflare slack-bot ``` ``` pnpm create cloudflare@latest slack-bot ``` Follow these steps to create a Hono project. * For _What would you like to start with_?, select `Framework Starter`. * For _Which development framework do you want to use?_, select `Hono`. * For, _Do you want to deploy your application?_, select `No`. Go to the `slack-bot` directory: ```sh cd slack-bot ``` Open `src/index.ts` in an editor to find the following code. ```ts import { Hono } from "hono"; type Bindings = { [key in keyof CloudflareBindings]: CloudflareBindings[key]; }; const app = new Hono<{ Bindings: Bindings }>(); app.get("/", (c) => { return c.text("Hello Hono!"); }); export default app; ``` This is a minimal application using Hono. If a GET access comes in on the path `/`, it will return a response with the text `Hello Hono!`. It also returns a message `404 Not Found` with status code 404 if any other path or method is accessed. To run the application on your local machine, execute the following command. npmyarnpnpmbun ``` npm i -- dev ``` ``` yarn add dev ``` ``` pnpm add dev ``` ``` bun add dev ``` Access to `http://localhost:8787` in your browser after the server has been started, and you can see the message. Hono helps you to create your Workers application easily and quickly. ## Build Now, let's create a Slack bot on Cloudflare Workers. ### Separating files You can create your application in several files instead of writing all endpoints and functions in one file. With Hono, it is able to add routing of child applications to the parent application using the function `app.route()`. For example, imagine the following Web API application. ```ts import { Hono } from "hono"; const app = new Hono(); app.get("/posts", (c) => c.text("Posts!")); app.post("/posts", (c) => c.text("Created!", 201)); export default app; ``` You can add the routes under `/api/v1`. ```ts import { Hono } from "hono"; import api from "./api"; const app = new Hono(); app.route("/api/v1", api); export default app; ``` It will return `Posts!` when accessing `GET /api/v1/posts`. The Slack bot will have two child applications called "route" each. 1. `lookup` route will take requests from Slack (sent when a user uses the `/issue` command), and look up the corresponding issue using the GitHub API. This application will be added to `/lookup` in the main application. 2. `webhook` route will be called when an issue changes on GitHub, via a configured webhook. This application will be add to `/webhook` in the main application. Create the route files in a directory named `routes`. ```sh mkdir -p src/routes touch src/routes/lookup.ts touch src/routes/webhook.ts ``` Then update the main application. ```ts import { Hono } from "hono"; import lookup from "./routes/lookup"; import webhook from "./routes/webhook"; const app = new Hono(); app.route("/lookup", lookup); app.route("/webhook", webhook); export default app; ``` ### Defining TypeScript types Before implementing the actual functions, you need to define the TypeScript types you will use in this project. Create a new file in the application at `src/types.ts` and write the code. `Bindings` is a type that describes the Cloudflare Workers environment variables. `Issue` is a type for a GitHub issue and `User` is a type for a GitHub user. You will need these later. ```ts export type Bindings = { SLACK_WEBHOOK_URL: string; }; export type Issue = { html_url: string; title: string; body: string; state: string; created_at: string; number: number; user: User; }; type User = { html_url: string; login: string; avatar_url: string; }; ``` ### Creating the lookup route Start creating the lookup route in `src/routes/lookup.ts`. ```ts import { Hono } from "hono"; const app = new Hono(); export default app; ``` To understand how you should design this function, you need to understand how Slack slash commands send data to URLs. According to the [documentation for Slack slash commands ↗](https://api.slack.com/interactivity/slash-commands), Slack sends an HTTP POST request to your specified URL, with a `application/x-www-form-urlencoded` content type. For example, if someone were to type `/issue cloudflare/wrangler#1`, you could expect a data payload in the format: ```txt token=gIkuvaNzQIHg97ATvDxqgjtO &team_id=T0001 &team_domain=example &enterprise_id=E0001 &enterprise_name=Globular%20Construct%20Inc &channel_id=C2147483705 &channel_name=test &user_id=U2147483697 &user_name=Steve &command=/issue &text=cloudflare/wrangler#1 &response_url=https://hooks.slack.com/commands/1234/5678 &trigger_id=13345224609.738474920.8088930838d88f008e0 ``` Given this payload body, you need to parse it, and get the value of the `text` key. With that `text`, for example, `cloudflare/wrangler#1`, you can parse that string into known piece of data (`owner`, `repo`, and `issue_number`), and use it to make a request to GitHub’s API, to retrieve the issue data. With Slack slash commands, you can respond to a slash command by returning structured data as the response to the incoming slash command. In this case, you should use the response from GitHub’s API to present a formatted version of the GitHub issue, including pieces of data like the title of the issue, who created it, and the date it was created. Slack’s new [Block Kit ↗](https://api.slack.com/block-kit) framework will allow you to return a detailed message response, by constructing text and image blocks with the data from GitHub’s API. #### Parsing slash commands To begin, the `lookup` route should parse the messages coming from Slack. As previously mentioned, the Slack API sends an HTTP POST in URL Encoded format. You can get the variable `text` by parsing it with `c.req.json()`. ```ts import { Hono } from "hono"; const app = new Hono(); app.post("/", async (c) => { const { text } = await c.req.parseBody(); if (typeof text !== "string") { return c.notFound(); } }); export default app; ``` Given a `text` variable, that contains text like `cloudflare/wrangler#1`, you should parse that text, and get the individual parts from it for use with GitHub’s API: `owner`, `repo`, and `issue_number`. To do this, create a new file in your application, at `src/utils/github.ts`. This file will contain a number of “utility” functions for working with GitHub’s API. The first of these will be a string parser, called `parseGhIssueString`: ```ts const ghIssueRegex = /(?[\w.-]*)\/(?[\w.-]*)\#(?\d*)/; export const parseGhIssueString = (text: string) => { const match = text.match(ghIssueRegex); return match ? (match.groups ?? {}) : {}; }; ``` `parseGhIssueString` takes in a `text` input, matches it against `ghIssueRegex`, and if a match is found, returns the `groups` object from that match, making use of the `owner`, `repo`, and `issue_number` capture groups defined in the regex. By exporting this function from `src/utils/github.ts`, you can make use of it back in `src/handlers/lookup.ts`: ```ts import { Hono } from "hono"; import { parseGhIssueString } from "../utils/github"; const app = new Hono(); app.post("/", async (c) => { const { text } = await c.req.parseBody(); if (typeof text !== "string") { return c.notFound(); } const { owner, repo, issue_number } = parseGhIssueString(text); }); export default app; ``` #### Making requests to GitHub’s API With this data, you can make your first API lookup to GitHub. Again, make a new function in `src/utils/github.ts`, to make a `fetch` request to the GitHub API for the issue data: ```ts const ghIssueRegex = /(?[\w.-]*)\/(?[\w.-]*)\#(?\d*)/; export const parseGhIssueString = (text: string) => { const match = text.match(ghIssueRegex); return match ? (match.groups ?? {}) : {}; }; export const fetchGithubIssue = ( owner: string, repo: string, issue_number: string, ) => { const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`; const headers = { "User-Agent": "simple-worker-slack-bot" }; return fetch(url, { headers }); }; ``` Back in `src/handlers/lookup.ts`, use `fetchGitHubIssue` to make a request to GitHub’s API, and parse the response: ```ts import { Hono } from "hono"; import { fetchGithubIssue, parseGhIssueString } from "../utils/github"; import { Issue } from "../types"; const app = new Hono(); app.post("/", async (c) => { const { text } = await c.req.parseBody(); if (typeof text !== "string") { return c.notFound(); } const { owner, repo, issue_number } = parseGhIssueString(text); const response = await fetchGithubIssue(owner, repo, issue_number); const issue = await response.json(); }); export default app; ``` #### Constructing a Slack message After you have received a response back from GitHub’s API, the final step is to construct a Slack message with the issue data, and return it to the user. The final result will look something like this: ![A successful Slack Message will have the components listed below](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1376,height=832,format=webp/_astro/issue-slack-message.8mahQ-Ir.png) You can see four different pieces in the above screenshot: 1. The first line (bolded) links to the issue, and shows the issue title 2. The following lines (including code snippets) are the issue body 3. The last line of text shows the issue status, the issue creator (with a link to the user’s GitHub profile), and the creation date for the issue 4. The profile picture of the issue creator, on the right-hand side The previously mentioned [Block Kit ↗](https://api.slack.com/block-kit) framework will help take the issue data (in the structure lined out in [GitHub’s REST API documentation ↗](https://developer.github.com/v3/issues/)) and format it into something like the above screenshot. Create another file, `src/utils/slack.ts`, to contain the function `constructGhIssueSlackMessage`, a function for taking issue data, and turning it into a collection of blocks. Blocks are JavaScript objects that Slack will use to format the message: ```ts import { Issue } from "../types"; export const constructGhIssueSlackMessage = ( issue: Issue, issue_string: string, prefix_text?: string, ) => { const issue_link = `<${issue.html_url}|${issue_string}>`; const user_link = `<${issue.user.html_url}|${issue.user.login}>`; const date = new Date(Date.parse(issue.created_at)).toLocaleDateString(); const text_lines = [ prefix_text, `*${issue.title} - ${issue_link}*`, issue.body, `*${issue.state}* - Created by ${user_link} on ${date}`, ]; }; ``` Slack messages accept a variant of Markdown, which supports bold text via asterisks (`*bolded text*`), and links in the format ``. Given that format, construct `issue_link`, which takes the `html_url` property from the GitHub API `issue` data (in format `https://github.com/cloudflare/wrangler-legacy/issues/1`), and the `issue_string` sent from the Slack slash command, and combines them into a clickable link in the Slack message. `user_link` is similar, using `issue.user.html_url` (in the format `https://github.com/signalnerve`, a GitHub user) and the user’s GitHub username (`issue.user.login`), to construct a clickable link to the GitHub user. Finally, parse `issue.created_at`, an ISO 8601 string, convert it into an instance of a JavaScript `Date`, and turn it into a formatted string, in the format `MM/DD/YY`. With those variables in place, `text_lines` is an array of each line of text for the Slack message. The first line is the **issue title** and the **issue link**, the second is the **issue body**, and the final line is the **issue state** (for example, open or closed), the **user link**, and the **creation date**. With the text constructed, you can finally construct your Slack message, returning an array of blocks for Slack’s [Block Kit ↗](https://api.slack.com/block-kit). In this case, there is only have one block: a [section ↗](https://api.slack.com/reference/messaging/blocks#section) block with Markdown text, and an accessory image of the user who created the issue. Return that single block inside of an array, to complete the `constructGhIssueSlackMessage` function: ```ts import { Issue } from "../types"; export const constructGhIssueSlackMessage = ( issue: Issue, issue_string: string, prefix_text?: string, ) => { const issue_link = `<${issue.html_url}|${issue_string}>`; const user_link = `<${issue.user.html_url}|${issue.user.login}>`; const date = new Date(Date.parse(issue.created_at)).toLocaleDateString(); const text_lines = [ prefix_text, `*${issue.title} - ${issue_link}*`, issue.body, `*${issue.state}* - Created by ${user_link} on ${date}`, ]; return [ { type: "section", text: { type: "mrkdwn", text: text_lines.join("\n"), }, accessory: { type: "image", image_url: issue.user.avatar_url, alt_text: issue.user.login, }, }, ]; }; ``` #### Finishing the lookup route In `src/handlers/lookup.ts`, use `constructGhIssueSlackMessage` to construct `blocks`, and return them as a new response with `c.json()` when the slash command is called: ```ts import { Hono } from "hono"; import { fetchGithubIssue, parseGhIssueString } from "../utils/github"; import { constructGhIssueSlackMessage } from "../utils/slack"; import { Issue } from "../types"; const app = new Hono(); app.post("/", async (c) => { const { text } = await c.req.parseBody(); if (typeof text !== "string") { return c.notFound(); } const { owner, repo, issue_number } = parseGhIssueString(text); const response = await fetchGithubIssue(owner, repo, issue_number); const issue = await response.json(); const blocks = constructGhIssueSlackMessage(issue, text); return c.json({ blocks, response_type: "in_channel", }); }); export default app; ``` One additional parameter passed into the response is `response_type`. By default, responses to slash commands are ephemeral, meaning that they are only seen by the user who writes the slash command. Passing a `response_type` of `in_channel`, as seen above, will cause the response to appear for all users in the channel. If you would like the messages to remain private, remove the `response_type` line. This will cause `response_type` to default to `ephemeral`. #### Handling errors The `lookup` route is almost complete, but there are a number of errors that can occur in the route, such as parsing the body from Slack, getting the issue from GitHub, or constructing the Slack message itself. Although Hono applications can handle errors without having to do anything, you can customize the response returned in the following way. ```ts import { Hono } from "hono"; import { fetchGithubIssue, parseGhIssueString } from "../utils/github"; import { constructGhIssueSlackMessage } from "../utils/slack"; import { Issue } from "../types"; const app = new Hono(); app.post("/", async (c) => { const { text } = await c.req.parseBody(); if (typeof text !== "string") { return c.notFound(); } const { owner, repo, issue_number } = parseGhIssueString(text); const response = await fetchGithubIssue(owner, repo, issue_number); const issue = await response.json(); const blocks = constructGhIssueSlackMessage(issue, text); return c.json({ blocks, response_type: "in_channel", }); }); app.onError((_e, c) => { return c.text( "Uh-oh! We couldn't find the issue you provided. " + "We can only find public issues in the following format: `owner/repo#issue_number`.", ); }); export default app; ``` ### Creating the webhook route You are now halfway through implementing the routes for your Workers application. In implementing the next route, `src/routes/webhook.ts`, you will re-use a lot of the code that you have already written for the lookup route. At the beginning of this tutorial, you configured a GitHub webhook to track any events related to issues in your repository. When an issue is opened, for example, the function corresponding to the path `/webhook` on your Workers application should take the data sent to it from GitHub, and post a new message in the configured Slack channel. In `src/routes/webhook.ts`, define a blank Hono application. The difference from the `lookup` route is that the `Bindings` is passed as a generics for the `new Hono()`. This is necessary to give the appropriate TypeScript type to `SLACK_WEBHOOK_URL` which will be used later. ```ts import { Hono } from "hono"; import { Bindings } from "../types"; const app = new Hono<{ Bindings: Bindings }>(); export default app; ``` Much like with the `lookup` route, you will need to parse the incoming payload inside of `request`, get the relevant issue data from it (refer to [the GitHub API documentation on IssueEvent ↗](https://developer.github.com/v3/activity/events/types/#issuesevent) for the full payload schema), and send a formatted message to Slack to indicate what has changed. The final version will look something like this: ![A successful Webhook Message example](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1372,height=744,format=webp/_astro/webhook_example.EQJW9q2u.png) Compare this message format to the format returned when a user uses the `/issue` slash command. You will see that there is only one actual difference between the two: the addition of an action text on the first line, in the format `An issue was $action:`. This action, which is sent as part of the `IssueEvent` from GitHub, will be used as you construct a very familiar looking collection of blocks using Slack’s Block Kit. #### Parsing event data To start filling out the route, parse the request body formatted JSON into an object and construct some helper variables: ```ts import { Hono } from "hono"; import { constructGhIssueSlackMessage } from "../utils/slack"; const app = new Hono(); app.post("/", async (c) => { const { action, issue, repository } = await c.req.json(); const prefix_text = `An issue was ${action}:`; const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`; }); export default app; ``` An `IssueEvent`, the payload sent from GitHub as part of your webhook configuration, includes an `action` (what happened to the issue: for example, it was opened, closed, locked, etc.), the `issue` itself, and the `repository`, among other things. Use `c.req.json()` to convert the payload body of the request from JSON into a plain JS object. Use ES6 destructuring to set `action`, `issue` and `repository` as variables you can use in your code. `prefix_text` is a string indicating what happened to the issue, and `issue_string` is the familiar string `owner/repo#issue_number` that you have seen before: while the `lookup` route directly used the text sent from Slack to fill in `issue_string`, you will construct it directly based on the data passed in the JSON payload. #### Constructing and sending a Slack message The messages your Slack bot sends back to your Slack channel from the `lookup` and `webhook` routes are incredibly similar. Because of this, you can re-use the existing `constructGhIssueSlackMessage` to continue populating `src/handlers/webhook.ts`. Import the function from `src/utils/slack.ts`, and pass the issue data into it: ```ts import { Hono } from "hono"; import { constructGhIssueSlackMessage } from "../utils/slack"; const app = new Hono(); app.post("/", async (c) => { const { action, issue, repository } = await c.req.json(); const prefix_text = `An issue was ${action}:`; const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`; const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text); }); export default app; ``` Importantly, the usage of `constructGhIssueSlackMessage` in this handler adds one additional argument to the function, `prefix_text`. Update the corresponding function inside of `src/utils/slack.ts`, adding `prefix_text` to the collection of `text_lines` in the message block, if it has been passed in to the function. Add a utility function, `compact`, which takes an array, and filters out any `null` or `undefined` values from it. This function will be used to remove `prefix_text` from `text_lines` if it has not actually been passed in to the function, such as when called from `src/handlers/lookup.ts`. The full (and final) version of the `src/utils/slack.ts` looks like this: ```ts import { Issue } from "../types"; const compact = (array: unknown[]) => array.filter((el) => el); export const constructGhIssueSlackMessage = ( issue: Issue, issue_string: string, prefix_text?: string, ) => { const issue_link = `<${issue.html_url}|${issue_string}>`; const user_link = `<${issue.user.html_url}|${issue.user.login}>`; const date = new Date(Date.parse(issue.created_at)).toLocaleDateString(); const text_lines = [ prefix_text, `*${issue.title} - ${issue_link}*`, issue.body, `*${issue.state}* - Created by ${user_link} on ${date}`, ]; return [ { type: "section", text: { type: "mrkdwn", text: compact(text_lines).join("\n"), }, accessory: { type: "image", image_url: issue.user.avatar_url, alt_text: issue.user.login, }, }, ]; }; ``` Back in `src/handlers/webhook.ts`, the `blocks` that are returned from `constructGhIssueSlackMessage` become the body in a new `fetch` request, an HTTP POST request to a Slack webhook URL. Once that request completes, return a response with status code `200`, and the body text `"OK"`: ```ts import { Hono } from "hono"; import { constructGhIssueSlackMessage } from "../utils/slack"; import { Bindings } from "../types"; const app = new Hono<{ Bindings: Bindings }>(); app.post("/", async (c) => { const { action, issue, repository } = await c.req.json(); const prefix_text = `An issue was ${action}:`; const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`; const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text); const fetchResponse = await fetch(c.env.SLACK_WEBHOOK_URL, { body: JSON.stringify({ blocks }), method: "POST", headers: { "Content-Type": "application/json" }, }); return c.text("OK"); }); export default app; ``` The constant `SLACK_WEBHOOK_URL` represents the Slack Webhook URL that you created all the way back in the [Incoming Webhook](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/#incoming-webhook) section of this tutorial. Caution Since this webhook allows developers to post directly to your Slack channel, keep it secret. To use this constant inside of your codebase, use the [wrangler secret](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) command: ```sh npx wrangler secret put SLACK_WEBHOOK_URL ``` ```sh Enter a secret value: https://hooks.slack.com/services/abc123 ``` #### Handling errors Similarly to the `lookup` route, the `webhook` route should include some basic error handling. Unlike `lookup`, which sends responses directly back into Slack, if something goes wrong with your webhook, it may be useful to actually generate an erroneous response, and return it to GitHub. To do this, write the custom error handler with `app.onError()` and return a new response with a status code of `500`. The final version of `src/routes/webhook.ts` looks like this: ```ts import { Hono } from "hono"; import { constructGhIssueSlackMessage } from "../utils/slack"; import { Bindings } from "../types"; const app = new Hono<{ Bindings: Bindings }>(); app.post("/", async (c) => { const { action, issue, repository } = await c.req.json(); const prefix_text = `An issue was ${action}:`; const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`; const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text); const fetchResponse = await fetch(c.env.SLACK_WEBHOOK_URL, { body: JSON.stringify({ blocks }), method: "POST", headers: { "Content-Type": "application/json" }, }); if (!fetchResponse.ok) throw new Error(); return c.text("OK"); }); app.onError((_e, c) => { return c.json( { message: "Unable to handle webhook", }, 500, ); }); export default app; ``` ## Deploy By completing the preceding steps, you have finished writing the code for your Slack bot. You can now deploy your application. Wrangler has built-in support for bundling, uploading, and releasing your Cloudflare Workers application. To do this, run the following command which will build and deploy your code. npmyarnpnpmbun ``` npm i -- deploy ``` ``` yarn add deploy ``` ``` pnpm add deploy ``` ``` bun add deploy ``` Deploying your Workers application should now cause issue updates to start appearing in your Slack channel, as the GitHub webhook can now successfully reach your Workers webhook route: ![When you create new issue, a Slackbot will now appear in your Slack channel](https://developers.cloudflare.com/images/workers/tutorials/slackbot/create-new-issue.gif) ## Related resources In this tutorial, you built and deployed a Cloudflare Workers application that can respond to GitHub webhook events, and allow GitHub API lookups within Slack. If you would like to review the full source code for this application, you can find the repository [on GitHub ↗](https://github.com/yusukebe/workers-slack-bot). If you want to get started building your own projects, review the existing list of [Quickstart templates](https://developers.cloudflare.com/workers/get-started/quickstarts/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/#page","headline":"Build a Slackbot · Cloudflare Workers docs","description":"Learn how to build a Slackbot with Hono and TypeScript in Cloudflare Workers","url":"https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-08","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Hono","TypeScript"]} ``` --- --- description: This tutorial will guide you on how to build globally distributed applications with Cloudflare Workers, and Turso, an edge-hosted distributed database based on libSQL. title: Connect to and query your Turso database using Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Connect to and query your Turso database using Workers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/connect-to-turso-using-workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This tutorial will guide you on how to build globally distributed applications with Cloudflare Workers, and [Turso ↗](https://chiselstrike.com/), an edge-hosted distributed database based on libSQL. By using Workers and Turso, you can create applications that are close to your end users without having to maintain or operate infrastructure in tens or hundreds of regions. Note For a more seamless experience, refer to the [Turso Database Integration guide](https://developers.cloudflare.com/workers/databases/third-party-integrations/turso/). The Turso Database Integration will guide you through connecting your Worker to a Turso database by securely configuring your database credentials as [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) in your Worker. ## Prerequisites Before continuing with this tutorial, you should have: * Successfully [created up your first Cloudflare Worker](https://developers.cloudflare.com/workers/get-started/guide/) and/or have deployed a Cloudflare Worker before. * Installed [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), a command-line tool for building Cloudflare Workers. * A [GitHub account ↗](https://github.com/), required for authenticating to Turso. * A basic familiarity with installing and using command-line interface (CLI) applications. ## Install the Turso CLI You will need the Turso CLI to create and populate a database. Run either of the following two commands in your terminal to install the Turso CLI: ```sh # On macOS or Linux with Homebrew brew install chiselstrike/tap/turso # Manual scripted installation curl -sSfL | bash ``` After you have installed the Turso CLI, verify that the CLI is in your shell path: ```sh turso --version ``` ```sh # This should output your current Turso CLI version (your installed version may be higher): turso version v0.51.0 ``` ## Create and populate a database Before you create your first Turso database, you need to log in to the CLI using your GitHub account by running: ```sh turso auth login ``` ```sh Waiting for authentication... ✔ Success! Logged in as ``` `turso auth login` will open a browser window and ask you to sign into your GitHub account, if you are not already logged in. The first time you do this, you will need to give the Turso application permission to use your account. Select **Approve** to grant Turso the permissions needed. After you have authenticated, you can create a database by running `turso db create `. Turso will automatically choose a location closest to you. ```sh turso db create my-db ``` ```sh # Example: [===> ] Creating database my-db in Los Angeles, California (US) (lax) # Once succeeded: Created database my-db in Los Angeles, California (US) (lax) in 34 seconds. ``` With your first database created, you can now connect to it directly and execute SQL against it: ```sh turso db shell my-db ``` To get started with your database, create and define a schema for your first table. In this example, you will create a `example_users` table with one column: `email` (of type `text`) and then populate it with one email address. In the shell you just opened, paste in the following SQL: ```sql create table example_users (email text); insert into example_users values ('foo@bar.com'); ``` If the SQL statements succeeded, there will be no output. Note that the trailing semi-colons (`;`) are necessary to terminate each SQL statement. Type `.quit` to exit the shell. ## Use Wrangler to create a Workers project The Workers command-line interface, [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), allows you to create, locally develop, and deploy your Workers projects. To create a new Workers project (named `worker-turso-ts`), run the following: npmyarnpnpm ``` npm create cloudflare@latest -- worker-turso-ts ``` ``` yarn create cloudflare worker-turso-ts ``` ``` pnpm create cloudflare@latest worker-turso-ts ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). To start developing your Worker, `cd` into your new project directory: ```sh cd worker-turso-ts ``` In your project directory, you now have the following files: * `wrangler.json` / `wrangler.toml`: [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) * `src/index.ts`: A minimal Hello World Worker written in TypeScript * `package.json`: A minimal Node dependencies configuration file. * `tsconfig.json`: TypeScript configuration that includes Workers types. Only generated if indicated. For this tutorial, only the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) and `src/index.ts` file are relevant. You will not need to edit the other files, and they should be left as is. ## Configure your Worker for your Turso database The Turso client library requires two pieces of information to make a connection: 1. `LIBSQL_DB_URL` \- The connection string for your Turso database. 2. `LIBSQL_DB_AUTH_TOKEN` \- The authentication token for your Turso database. This should be kept a secret, and not committed to source code. To get the URL for your database, run the following Turso CLI command, and copy the result: ```sh turso db show my-db --url ``` ```sh libsql://my-db-.turso.io ``` Open the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) in your editor and at the bottom of the file, create a new `[vars]` section representing the [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) for your project: ```jsonc { "vars": { "LIBSQL_DB_URL": "paste-your-url-here" } } ``` ```toml [vars] LIBSQL_DB_URL = "paste-your-url-here" ``` Save the changes to the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). Next, create a long-lived authentication token for your Worker to use when connecting to your database. Run the following Turso CLI command, and copy the output to your clipboard: ```sh turso db tokens create my-db -e none # Will output a long text string (an encoded JSON Web Token) ``` To keep this token secret: 1. You will create a `.dev.vars` file for local development. Do not commit this file to source control. You should add `.dev.vars to your `.gitignore\` file if you are using Git. * You will also create a [secret](https://developers.cloudflare.com/workers/configuration/secrets/) to keep your authentication token confidential. First, create a new file called `.dev.vars` with the following structure. Paste your authentication token in the quotation marks: ```plaintext LIBSQL_DB_AUTH_TOKEN="" ``` Save your changes to `.dev.vars`. Next, store the authentication token as a secret for your production Worker to reference. Run the following `wrangler secret` command to create a Secret with your token: ```sh # Ensure you specify the secret name exactly: your Worker will need to reference it later. npx wrangler secret put LIBSQL_DB_AUTH_TOKEN ``` ```sh ? Enter a secret value: › ``` Select `` on your keyboard to save the token as a secret. Both `LIBSQL_DB_URL` and `LIBSQL_DB_AUTH_TOKEN` will be available in your Worker's environment at runtime. ## Install extra libraries Install the Turso client library and a router: npmyarnpnpmbun ``` npm i @libsql/client itty-router ``` ``` yarn add @libsql/client itty-router ``` ``` pnpm add @libsql/client itty-router ``` ``` bun add @libsql/client itty-router ``` The `@libsql/client` library allows you to query a Turso database. The `itty-router` library is a lightweight router you will use to help handle incoming requests to the worker. ## Write your Worker You will now write a Worker that will: 1. Handle an HTTP request. 2. Route it to a specific handler to either list all users in our database or add a new user. 3. Return the results and/or success. Open `src/index.ts` and delete the existing template. Copy the below code exactly as is and paste it into the file: ```ts import { Client as LibsqlClient, createClient } from "@libsql/client/web"; import { Router, RouterType } from "itty-router"; export interface Env { // The environment variable containing your the URL for your Turso database. LIBSQL_DB_URL?: string; // The Secret that contains the authentication token for your Turso database. LIBSQL_DB_AUTH_TOKEN?: string; // These objects are created before first use, then stashed here // for future use router?: RouterType; } export default { async fetch(request, env): Promise { if (env.router === undefined) { env.router = buildRouter(env); } return env.router.fetch(request); }, } satisfies ExportedHandler; function buildLibsqlClient(env: Env): LibsqlClient { const url = env.LIBSQL_DB_URL?.trim(); if (url === undefined) { throw new Error("LIBSQL_DB_URL env var is not defined"); } const authToken = env.LIBSQL_DB_AUTH_TOKEN?.trim(); if (authToken === undefined) { throw new Error("LIBSQL_DB_AUTH_TOKEN env var is not defined"); } return createClient({ url, authToken }); } function buildRouter(env: Env): RouterType { const router = Router(); router.get("/users", async () => { const client = buildLibsqlClient(env); const rs = await client.execute("select * from example_users"); return Response.json(rs); }); router.get("/add-user", async (request) => { const client = buildLibsqlClient(env); const email = request.query.email; if (email === undefined) { return new Response("Missing email", { status: 400 }); } if (typeof email !== "string") { return new Response("email must be a single string", { status: 400 }); } if (email.length === 0) { return new Response("email length must be > 0", { status: 400 }); } try { await client.execute({ sql: "insert into example_users values (?)", args: [email], }); } catch (e) { console.error(e); return new Response("database insert failed"); } return new Response("Added"); }); router.all("*", () => new Response("Not Found.", { status: 404 })); return router; } ``` Save your `src/index.ts` file with your changes. Note: * The libSQL client library import '@libsql/client/web' must be imported exactly as shown when working with Cloudflare workers. The non-web import will not work in the Workers environment. * The `Env` interface contains the environment variable and secret you defined earlier. * The `Env` interface also caches the libSQL client object and router, which are created on the first request to the Worker. * The `/users` route fetches all rows from the `example_users` table you created in the Turso shell. It simply serializes the `ResultSet` object as JSON directly to the caller. * The `/add-user` route inserts a new row using a value provided in the query string. With your environment configured and your code ready, you will now test your Worker locally before you deploy. ## Run the Worker locally with Wrangler To run a local instance of our Worker (entirely on your machine), run the following command: ```sh npx wrangler dev ``` You should be able to review output similar to the following: ```txt Your worker has access to the following bindings: - Vars: - LIBSQL_DB_URL: "your-url" ⎔ Starting a local server... ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ [b] open a browser, [d] open Devtools, [l] turn off local mode, [c] clear console, [x] to exit │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ Debugger listening on ws://127.0.0.1:61918/1064babd-bc9d-4bed-b171-b35dab3b7680 For help, see: https://nodejs.org/en/docs/inspector Debugger attached. [mf:inf] Worker reloaded! (40.25KiB) [mf:inf] Listening on 0.0.0.0:8787 [mf:inf] - http://127.0.0.1:8787 [mf:inf] - http://192.168.1.136:8787 [mf:inf] Updated `Request.cf` object cache! ``` The localhost address — the one with `127.0.0.1` in it — is a web-server running locally on your machine. Connect to it and validate your Worker returns the email address you inserted when you created your `example_users` table by visiting the `/users` route in your browser: [http://127.0.0.1:8787/users ↗](http://127.0.0.1:8787/users). You should see JSON similar to the following containing the data from the `example_users` table: ```json { "columns": ["email"], "rows": [{ "email": "foo@bar.com" }], "rowsAffected": 0 } ``` Caution If you see an error instead of a list of users, double check that: * You have entered the correct value for your `LIBSQL_DB_URL` in the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). * You have set a secret called `LIBSQL_DB_AUTH_TOKEN` with your database authentication token. Both of these need to be present and match the variable names in your Worker's code. Test the `/add-users` route and pass it an email address to insert: [http://127.0.0.1:8787/add-user?email=test@test.com ↗](http://127.0.0.1:8787/add-user?email=test@test.com.) You should see the text `“Added”`. If you load the first URL with the `/users` route again ([http://127.0.0.1:8787/users ↗](http://127.0.0.1:8787/users)), it will show the newly added row. You can repeat this as many times as you like. Note that due to its design, your application will not stop you from adding duplicate email addresses. Quit Wrangler by typing `q` into the shell where it was started. ## Deploy to Cloudflare After you have validated that your Worker can connect to your Turso database, deploy your Worker. Run the following Wrangler command to deploy your Worker to the Cloudflare global network: ```sh npx wrangler deploy ``` The first time you run this command, it will launch a browser, ask you to sign in with your Cloudflare account, and grant permissions to Wrangler. The `deploy` command will output the following: ```txt Your worker has access to the following bindings: - Vars: - LIBSQL_DB_URL: "your-url" ... Published worker-turso-ts (0.19 sec) https://worker-turso-ts..workers.dev Current Deployment ID: f9e6b48f-5aac-40bd-8f44-8a40be2212ff ``` You have now deployed a Worker that can connect to your Turso database, query it, and insert new data. ## Optional: Clean up To clean up the resources you created as part of this tutorial: * If you do not want to keep this Worker, run `npx wrangler delete worker-turso-ts` to delete the deployed Worker. * You can also delete your Turso database via `turso db destroy my-db`. ## Related resources * Find the [complete project source code on GitHub ↗](https://github.com/cloudflare/workers-sdk/tree/main/templates/worker-turso-ts/). * Understand how to [debug your Cloudflare Worker](https://developers.cloudflare.com/workers/observability/). * Join the [Cloudflare Developer Discord ↗](https://discord.cloudflare.com). * Join the [ChiselStrike (Turso) Discord ↗](https://discord.com/invite/4B5D7hYwub). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/connect-to-turso-using-workers/#page","headline":"Connect to and query your Turso database using Workers · Cloudflare Workers docs","description":"This tutorial will guide you on how to build globally distributed applications with Cloudflare Workers, and Turso, an edge-hosted distributed database based on libSQL.","url":"https://developers.cloudflare.com/workers/tutorials/connect-to-turso-using-workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["TypeScript","SQL"]} ``` --- --- description: In this tutorial, you will use the OpenAI API and Cloudflare R2 to create a fine-tuned model. title: Create a fine-tuned OpenAI model with R2 image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Create a fine-tuned OpenAI model with R2 Last updated Mar 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/create-finetuned-chatgpt-ai-models-with-r2/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will use the [OpenAI ↗](https://openai.com) API and [Cloudflare R2](https://developers.cloudflare.com/r2) to create a [fine-tuned model ↗](https://platform.openai.com/docs/guides/fine-tuning). This feature in OpenAI's API allows you to derive a custom model from OpenAI's various large language models based on a set of custom instructions and example answers. These instructions and example answers are written in a document, known as a fine-tune document. This document will be stored in R2 and dynamically provided to OpenAI's APIs when creating a new fine-tune model. In order to use this feature, you will do the following tasks: 1. Upload a fine-tune document to R2. 2. Read the R2 file and upload it to OpenAI. 3. Create a new fine-tuned model based on the document. ![Demo](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2978,height=1744,format=webp/_astro/finetune-example.Df8cOHyQ.png) To review the completed code for this application, refer to the [GitHub repository for this tutorial ↗](https://github.com/kristianfreeman/openai-finetune-r2-example). ## Prerequisites Before you start, make sure you have: * A Cloudflare account with access to R2\. If you do not have a Cloudflare account, [sign up ↗](https://dash.cloudflare.com/sign-up/workers-and-pages) before continuing. Then purchase R2 from your Cloudflare dashboard. * An OpenAI API key. * A fine-tune document, structured as [JSON Lines ↗](https://jsonlines.org/). Use the [example document ↗](https://github.com/kristianfreeman/openai-finetune-r2-example/blob/16ca53ca9c8589834abe317487eeedb8a24c7643/example%5Fdata.jsonl) in the source code. ## 1\. Create a Worker application First, use the `c3` CLI to create a new Cloudflare Workers project. npmyarnpnpm ``` npm create cloudflare@latest -- finetune-chatgpt-model ``` ``` yarn create cloudflare finetune-chatgpt-model ``` ``` pnpm create cloudflare@latest finetune-chatgpt-model ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). The above options will create the "Hello World" TypeScript project. Move into your newly created directory: ```sh cd finetune-chatgpt-model ``` ## 2\. Upload a fine-tune document to R2 Next, upload the fine-tune document to R2\. R2 is a key-value store that allows you to store and retrieve files from within your Workers application. You will use [Wrangler](https://developers.cloudflare.com/workers/wrangler) to create a new R2 bucket. To create a new R2 bucket use the [wrangler r2 bucket create](https://developers.cloudflare.com/workers/wrangler/commands/r2/#r2-bucket-create) command. Note that you are logged in with your Cloudflare account. If not logged in via Wrangler, use the [wrangler login](https://developers.cloudflare.com/workers/wrangler/commands/general/#login) command. ```sh npx wrangler r2 bucket create ``` Replace `` with your desired bucket name. Note that bucket names must be lowercase and can only contain dashes. Next, upload a file using the [wrangler r2 object put](https://developers.cloudflare.com/workers/wrangler/commands/r2/#r2-object-put) command. ```sh npx wrangler r2 object put -f ``` `` is the combined bucket and file path of the file you want to upload -- for example, `fine-tune-ai/finetune.jsonl`, where `fine-tune-ai` is the bucket name. Replace `` with the local filename of your fine-tune document. ## 3\. Bind your bucket to the Worker A binding is how your Worker interacts with external resources such as the R2 bucket. To bind the R2 bucket to your Worker, add the following to your Wrangler file. Update the binding property to a valid JavaScript variable identifier. Replace `` with the name of the bucket you created in [step 2](#2-upload-a-fine-tune-document-to-r2): ```jsonc { "r2_buckets": [ { "binding": "MY_BUCKET", // <~ valid JavaScript variable name "bucket_name": "" } ] } ``` ```toml [[r2_buckets]] binding = "MY_BUCKET" bucket_name = "" ``` ## 4\. Initialize your Worker application You will use [Hono ↗](https://hono.dev/), a lightweight framework for building Cloudflare Workers applications. Hono provides an interface for defining routes and middleware functions. Inside your project directory, run the following command to install Hono: npmyarnpnpmbun ``` npm i hono ``` ``` yarn add hono ``` ``` pnpm add hono ``` ``` bun add hono ``` You also need to install the [OpenAI Node API library ↗](https://www.npmjs.com/package/openai). This library provides convenient access to the OpenAI REST API in a Node.js project. To install the library, execute the following command: npmyarnpnpmbun ``` npm i openai ``` ``` yarn add openai ``` ``` pnpm add openai ``` ``` bun add openai ``` Next, open the `src/index.ts` file and replace the default code with the below code. Replace `` with the binding name you set in Wrangler file. ```typescript import { Context, Hono } from "hono"; import OpenAI from "openai"; type Bindings = { : R2Bucket OPENAI_API_KEY: string } type Variables = { openai: OpenAI } const app = new Hono<{ Bindings: Bindings, Variables: Variables }>() app.use('*', async (c, next) => { const openai = new OpenAI({ apiKey: c.env.OPENAI_API_KEY, }) c.set("openai", openai) await next() }) app.onError((err, c) => { return c.text(err.message, 500) }) export default app; ``` In the above code, you first import the required packages and define the types. Then, you initialize `app` as a new Hono instance. Using the `use` middleware function, you add the OpenAI API client to the context of all routes. This middleware function allows you to access the client from within any route handler. `onError()` defines an error handler to return any errors as a JSON response. ## 5\. Read R2 files and upload them to OpenAI In this section, you will define the route and function responsible for handling file uploads. In `createFile`, your Worker reads the file from R2 and converts it to a `File` object. Your Worker then uses the OpenAI API to upload the file and return the response. The `GET /files` route listens for `GET` requests with a query parameter `file`, representing a filename of an uploaded fine-tune document in R2\. The function uses the `createFile` function to manage the file upload process. Replace `` with the binding name you set in Wrangler file. ```typescript // New import added at beginning of file import { toFile } from 'openai/uploads' const createFile = async (c: Context, r2Object: R2ObjectBody) => { const openai: OpenAI = c.get("openai") const blob = await r2Object.blob() const file = await toFile(blob, r2Object.key) const uploadedFile = await openai.files.create({ file, purpose: "fine-tune", }) return uploadedFile } app.get('/files', async c => { const fileQueryParam = c.req.query("file") if (!fileQueryParam) return c.text("Missing file query param", 400) const file = await c.env..get(fileQueryParam) if (!file) return c.text("Couldn't find file", 400) const uploadedFile = await createFile(c, file) return c.json(uploadedFile) }) ``` ## 6\. Create fine-tuned models This section includes the `GET /models` route and the `createModel` function. The function `createModel` takes care of specifying the details and initiating the fine-tuning process with OpenAI. The route handles incoming requests for creating a new fine-tuned model. ```typescript const createModel = async (c: Context, fileId: string) => { const openai: OpenAI = c.get("openai"); const body = { training_file: fileId, model: "gpt-4o-mini", }; return openai.fineTuning.jobs.create(body); }; app.get("/models", async (c) => { const fileId = c.req.query("file_id"); if (!fileId) return c.text("Missing file ID query param", 400); const model = await createModel(c, fileId); return c.json(model); }); ``` ## 7\. List all fine-tune jobs This section describes the `GET /jobs` route and the corresponding `getJobs` function. The function interacts with OpenAI's API to fetch a list of all fine-tuning jobs. The route provides an interface for retrieving this information. ```typescript const getJobs = async (c: Context) => { const openai: OpenAI = c.get("openai"); const resp = await openai.fineTuning.jobs.list(); return resp.data; }; app.get("/jobs", async (c) => { const jobs = await getJobs(c); return c.json(jobs); }); ``` ## 8\. Deploy your application After you have created your Worker application and added the required functions, deploy the application. Before you deploy, you must set the `OPENAI_API_KEY` [secret](https://developers.cloudflare.com/workers/configuration/secrets/) for your application. Do this by running the [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret-put) command: ```sh npx wrangler secret put OPENAI_API_KEY ``` To deploy your Worker application to the Cloudflare global network: 1. Make sure you are in your Worker project's directory, then run the [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) command: ```sh npx wrangler deploy ``` 1. Wrangler will package and upload your code. 2. After your application is deployed, Wrangler will provide you with your Worker's URL. ## 9\. View the fine-tune job status and use the model To use your application, create a new fine-tune job by making a request to the `/files` with a `file` query param matching the filename you uploaded earlier: ```sh curl https://your-worker-url.com/files?file=finetune.jsonl ``` When the file is uploaded, issue another request to `/models`, passing the `file_id` query parameter. This should match the `id` returned as JSON from the `/files` route: ```sh curl https://your-worker-url.com/models?file_id=file-abc123 ``` Finally, visit `/jobs` to see the status of your fine-tune jobs in OpenAI. Once the fine-tune job has completed, you can see the `fine_tuned_model` value, indicating a fine-tuned model has been created. ![Jobs](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1606,height=1144,format=webp/_astro/finetune-jobs.BQ_jbiJu.png) Visit the [OpenAI Playground ↗](https://platform.openai.com/playground) in order to use your fine-tune model. Select your fine-tune model from the top-left dropdown of the interface. ![Demo](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2978,height=1744,format=webp/_astro/finetune-example.Df8cOHyQ.png) Use it in any API requests you make to OpenAI's chat completions endpoints. For instance, in the below code example: ```javascript openai.chat.completions.create({ messages: [{ role: "system", content: "You are a helpful assistant." }], model: "ft:gpt-4o-mini:my-org:custom_suffix:id", }); ``` ## Next steps To build more with Workers, refer to [Tutorials](https://developers.cloudflare.com/workers/tutorials). If you have any questions, need assistance, or would like to share your project, join the Cloudflare Developer community on [Discord ↗](https://discord.cloudflare.com) to connect with other developers and the Cloudflare team. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/create-finetuned-chatgpt-ai-models-with-r2/#page","headline":"Create a fine-tuned OpenAI model with R2 · Cloudflare Workers docs","description":"In this tutorial, you will use the OpenAI API and Cloudflare R2 to create a fine-tuned model.","url":"https://developers.cloudflare.com/workers/tutorials/create-finetuned-chatgpt-ai-models-with-r2/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-03-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI","Hono","TypeScript"]} ``` --- --- description: This tutorial shows how to deploy a serverless, real-time chat application. The chat application uses a Durable Object to control each chat room. title: Deploy a real-time chat application image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Deploy a real-time chat application Last updated Jan 29, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/deploy-a-realtime-chat-app/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will deploy a serverless, real-time chat application that runs using [Durable Objects](https://developers.cloudflare.com/durable-objects/). This chat application uses a Durable Object to control each chat room. Users connect to the Object using WebSockets. Messages from one user are broadcast to all the other users. The chat history is also stored in durable storage. Real-time messages are relayed directly from one user to others without going through the storage layer. ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## Clone the chat application repository Open your terminal and clone the [workers-chat-demo ↗](https://github.com/cloudflare/workers-chat-demo) repository: ```sh git clone https://github.com/cloudflare/workers-chat-demo.git ``` ## Authenticate Wrangler After you have cloned the repository, authenticate Wrangler by running: ```sh npx wrangler login ``` ## Deploy your project When you are ready to deploy your application, run: ```sh npx wrangler deploy ``` Your application will be deployed to your `*.workers.dev` subdomain. To deploy your application to a custom domain within the Cloudflare dashboard, go to your Worker > **Triggers** \> **Add Custom Domain**. To deploy your application to a custom domain using Wrangler, open your project's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). To configure a route in your Wrangler configuration file, add the following to your environment: ```jsonc { "routes": [ { "pattern": "example.com/about", "zone_id": "" } ] } ``` ```toml [[routes]] pattern = "example.com/about" zone_id = "" ``` If you have specified your zone ID in the environment of your Wrangler configuration file, you will not need to write it again in object form. To configure a subdomain in your Wrangler configuration file, add the following to your environment: ```jsonc { "routes": [ { "pattern": "subdomain.example.com", "custom_domain": true } ] } ``` ```toml [[routes]] pattern = "subdomain.example.com" custom_domain = true ``` To test your live application: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker > **Triggers** \> **Routes** \> Select the `edge-chat-demo..workers.dev` route. 3. Enter a name in the **your name** field. 4. Choose whether to enter a public room or create a private room. 5. Send the link to other participants. You will be able to view room participants on the right side of the screen. ## Uninstall your application To uninstall your chat application, modify your Wrangler file to remove the `durable_objects` bindings and add a `deleted_classes` migration: ```jsonc { "durable_objects": { "bindings": [] }, // Indicate that you want the ChatRoom and RateLimiter classes to be callable as Durable Objects. "migrations": [ { "tag": "v1", "new_sqlite_classes": [ "ChatRoom", "RateLimiter" ] }, { "tag": "v2", // Should be unique for each entry "deleted_classes": [ "ChatRoom", "RateLimiter" ] } ] } ``` ```toml [durable_objects] bindings = [ ] [[migrations]] tag = "v1" new_sqlite_classes = [ "ChatRoom", "RateLimiter" ] [[migrations]] tag = "v2" deleted_classes = [ "ChatRoom", "RateLimiter" ] ``` Then run `npx wrangler deploy`. To delete your Worker: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Select **Manage Service** \> **Delete**. For complete instructions on set up and deletion, refer to the `README.md` in your cloned repository. By completing this tutorial, you have deployed a real-time chat application with Durable Objects and Cloudflare Workers. ## Related resources Continue building with other Cloudflare Workers tutorials below. * [Build a Slackbot](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/) * [Create SMS notifications for your GitHub repository using Twilio](https://developers.cloudflare.com/workers/tutorials/github-sms-notifications-using-twilio/) * [Build a QR code generator](https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/deploy-a-realtime-chat-app/#page","headline":"Deploy a real-time chat application · Cloudflare Workers docs","description":"This tutorial shows how to deploy a serverless, real-time chat application. The chat application uses a Durable Object to control each chat room.","url":"https://developers.cloudflare.com/workers/tutorials/deploy-a-realtime-chat-app/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-01-29","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript"]} ``` --- --- description: Learn how to deploy an Express.js application on Cloudflare Workers. title: Deploy an Express.js application on Cloudflare Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Deploy an Express.js application on Cloudflare Workers Last updated Aug 10, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/deploy-an-express-app/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn how to deploy an [Express.js ↗](https://expressjs.com/) application on Cloudflare Workers using the [Cloudflare Workers platform](https://developers.cloudflare.com/workers/) and [D1 database](https://developers.cloudflare.com/d1/). You will build a Members Registry API with basic Create, Read, Update, and Delete (CRUD) operations. You will use D1 as the database for storing and retrieving member data. ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## Quick start If you want to skip the steps and get started quickly, select **Deploy to Cloudflare** below. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/docs-examples/tree/main/workers/express-on-workers) This creates a repository in your GitHub account and deploys the application to Cloudflare Workers. Use this option if you are familiar with Cloudflare Workers, and wish to skip the step-by-step guidance. You may wish to manually follow the steps if you are new to Cloudflare Workers. ## 1\. Create a new Cloudflare Workers project Use [C3 ↗](https://developers.cloudflare.com/learning-paths/workers/get-started/c3-and-wrangler/#c3), the command-line tool for Cloudflare's developer products, to create a new directory and initialize a new Worker project: npmyarnpnpm ``` npm create cloudflare@latest -- express-d1-app ``` ``` yarn create cloudflare express-d1-app ``` ``` pnpm create cloudflare@latest express-d1-app ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Change into your new project directory: ```sh cd express-d1-app ``` ## 2\. Install Express and dependencies In this tutorial, you will use [Express.js ↗](https://expressjs.com/), a popular web framework for Node.js. To use Express in a Cloudflare Workers environment, install Express along with the necessary TypeScript types: npmyarnpnpmbun ``` npm i express @types/express ``` ``` yarn add express @types/express ``` ``` pnpm add express @types/express ``` ``` bun add express @types/express ``` Express.js on Cloudflare Workers requires the `nodejs_compat` [compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/). This flag enables Node.js APIs and allows Express to run on the Workers runtime. Add the following to your Wrangler configuration file: ```jsonc { "compatibility_flags": [ "nodejs_compat" ] } ``` ```toml compatibility_flags = [ "nodejs_compat" ] ``` ## 3\. Create a D1 database You will now create a D1 database to store member information. Use the `wrangler d1 create` command to create a new database: ```sh npx wrangler d1 create members-db ``` The command will create a new D1 database and ask you the following questions: * **Would you like Wrangler to add it on your behalf?**: Type `Y`. * **What binding name would you like to use?**: Type `DB` and press Enter. * **For local dev, do you want to connect to the remote resource instead of a local resource?**: Type `N`. ```sh ⛅️ wrangler 4.44.0 ─────────────────── ✅ Successfully created DB 'members-db' in region WNAM Created your new D1 database. To access your new D1 Database in your Worker, add the following snippet to your configuration file: { "d1_databases": [ { "binding": "members_db", "database_name": "members-db", "database_id": "" } ] } ✔ Would you like Wrangler to add it on your behalf? … yes ✔ What binding name would you like to use? … DB ✔ For local dev, do you want to connect to the remote resource instead of a local resource? … no ``` The binding will be added to your Wrangler configuration file. ```jsonc { "d1_databases": [ { "binding": "DB", "database_name": "members-db", "database_id": "" } ] } ``` ```toml [[d1_databases]] binding = "DB" database_name = "members-db" database_id = "" ``` ## 4\. Create database schema Create a directory called `schemas` in your project root, and inside it, create a file called `schema.sql`: ```sql DROP TABLE IF EXISTS members; CREATE TABLE IF NOT EXISTS members ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, joined_date TEXT NOT NULL ); -- Insert sample data INSERT INTO members (name, email, joined_date) VALUES ('Alice Johnson', 'alice@example.com', '2024-01-15'), ('Bob Smith', 'bob@example.com', '2024-02-20'), ('Carol Williams', 'carol@example.com', '2024-03-10'); ``` This schema creates a `members` table with an auto-incrementing ID, name, email, and join date fields. It also inserts three sample members. Execute the schema file against your D1 database: ```sh npx wrangler d1 execute members-db --file=./schemas/schema.sql ``` The above command creates the table in your local development database. You will deploy the schema to production later. ## 5\. Initialize Express application Update your `src/index.ts` file to set up Express with TypeScript. Replace the file content with the following: ```ts import { env } from "cloudflare:workers"; import { httpServerHandler } from "cloudflare:node"; import express from "express"; const app = express(); // Middleware to parse JSON bodies app.use(express.json()); // Health check endpoint app.get("/", (req, res) => { res.json({ message: "Express.js running on Cloudflare Workers!" }); }); app.listen(3000); export default httpServerHandler({ port: 3000 }); ``` This code initializes Express and creates a basic health check endpoint. The key import `import { env } from "cloudflare:workers"` allows you to access [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) like your D1 database from anywhere in your code. The [httpServerHandler](https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/#httpserverhandler) integrates Express with the Workers runtime, enabling your application to handle HTTP requests on Cloudflare's network. Next, execute the typegen command to generate type definitions for your Worker environment: ```sh npm run cf-typegen ``` ## 6\. Implement read operations Add endpoints to retrieve members from the database. Update your `src/index.ts` file by adding the following routes after the health check endpoint: ```ts // GET all members app.get('/api/members', async (req, res) => { try { const { results } = await env.DB.prepare('SELECT * FROM members ORDER BY joined_date DESC').all(); res.json({ success: true, members: results }); } catch (error) { res.status(500).json({ success: false, error: 'Failed to fetch members' }); } }); // GET a single member by ID app.get('/api/members/:id', async (req, res) => { try { const { id } = req.params; const { results } = await env.DB.prepare('SELECT * FROM members WHERE id = ?').bind(id).all(); if (results.length === 0) { return res.status(404).json({ success: false, error: 'Member not found' }); } res.json({ success: true, member: results[0] }); } catch (error) { res.status(500).json({ success: false, error: 'Failed to fetch member' }); } }); ``` These routes use the D1 binding (`env.DB`) to prepare SQL statements and execute them. Since you imported `env` from `cloudflare:workers` at the top of the file, it is accessible throughout your application. The `prepare`, `bind`, and `all` methods on the D1 binding allow you to safely query the database. Refer to [D1 Workers Binding API](https://developers.cloudflare.com/d1/worker-api/) for all available methods. ## 7\. Implement create operation Add an endpoint to create new members. Add the following route to your `src/index.ts` file: ```ts // POST - Create a new member app.post("/api/members", async (req, res) => { try { const { name, email } = req.body; // Validate input if (!name || !email) { return res.status(400).json({ success: false, error: "Name and email are required", }); } // Basic email validation (simplified for tutorial purposes) // For production, consider using a validation library or more comprehensive checks if (!email.includes("@") || !email.includes(".")) { return res.status(400).json({ success: false, error: "Invalid email format", }); } const joined_date = new Date().toISOString().split("T")[0]; const result = await env.DB.prepare( "INSERT INTO members (name, email, joined_date) VALUES (?, ?, ?)" ) .bind(name, email, joined_date) .run(); if (result.success) { res.status(201).json({ success: true, message: "Member created successfully", id: result.meta.last_row_id, }); } else { res .status(500) .json({ success: false, error: "Failed to create member" }); } } catch (error: any) { // Handle unique constraint violation if (error.message?.includes("UNIQUE constraint failed")) { return res.status(409).json({ success: false, error: "Email already exists", }); } res.status(500).json({ success: false, error: "Failed to create member" }); } }); ``` This endpoint validates the input, checks the email format, and inserts a new member into the database. It also handles duplicate email addresses by checking for unique constraint violations. ## 8\. Implement update operation Add an endpoint to update existing members. Add the following route to your `src/index.ts` file: ```ts app.put("/api/members/:id", async (req, res) => { try { const { id } = req.params; const { name, email } = req.body; // Validate input if (!name && !email) { return res.status(400).json({ success: false, error: "At least one field (name or email) is required", }); } // Basic email validation if provided (simplified for tutorial purposes) // For production, consider using a validation library or more comprehensive checks if (email && (!email.includes("@") || !email.includes("."))) { return res.status(400).json({ success: false, error: "Invalid email format", }); } // Build dynamic update query const updates: string[] = []; const values: any[] = []; if (name) { updates.push("name = ?"); values.push(name); } if (email) { updates.push("email = ?"); values.push(email); } values.push(id); const result = await env.DB.prepare( `UPDATE members SET ${updates.join(", ")} WHERE id = ?` ) .bind(...values) .run(); if (result.meta.changes === 0) { return res .status(404) .json({ success: false, error: "Member not found" }); } res.json({ success: true, message: "Member updated successfully" }); } catch (error: any) { if (error.message?.includes("UNIQUE constraint failed")) { return res.status(409).json({ success: false, error: "Email already exists", }); } res.status(500).json({ success: false, error: "Failed to update member" }); } }); ``` This endpoint allows updating either the name, email, or both fields of an existing member. It builds a dynamic SQL query based on the provided fields. ## 9\. Implement delete operation Add an endpoint to delete members. Add the following route to your `src/index.ts` file: ```ts // DELETE - Delete a member app.delete("/api/members/:id", async (req, res) => { try { const { id } = req.params; const result = await env.DB.prepare("DELETE FROM members WHERE id = ?") .bind(id) .run(); if (result.meta.changes === 0) { return res .status(404) .json({ success: false, error: "Member not found" }); } res.json({ success: true, message: "Member deleted successfully" }); } catch (error) { res.status(500).json({ success: false, error: "Failed to delete member" }); } }); ``` This endpoint deletes a member by their ID and returns an error if the member does not exist. ## 10\. Test locally Start the development server to test your API locally: ```sh npm run dev ``` The development server will start, and you can access your API at `http://localhost:8787`. Open a new terminal window and test the endpoints using `curl`: ```sh curl http://localhost:8787/api/members ``` ```json { "success": true, "members": [ { "id": 1, "name": "Alice Johnson", "email": "alice@example.com", "joined_date": "2024-01-15" }, { "id": 2, "name": "Bob Smith", "email": "bob@example.com", "joined_date": "2024-02-20" }, { "id": 3, "name": "Carol Williams", "email": "carol@example.com", "joined_date": "2024-03-10" } ] } ``` Test creating a new member: ```sh curl -X POST http://localhost:8787/api/members \ -H "Content-Type: application/json" \ -d '{"name": "David Brown", "email": "david@example.com"}' ``` ```json { "success": true, "message": "Member created successfully", "id": 4 } ``` Test getting a single member: ```sh curl http://localhost:8787/api/members/1 ``` Test updating a member: ```sh curl -X PUT http://localhost:8787/api/members/1 \ -H "Content-Type: application/json" \ -d '{"name": "Alice Cooper"}' ``` Test deleting a member: ```sh curl -X DELETE http://localhost:8787/api/members/4 ``` ## 11\. Deploy to Cloudflare Workers Before deploying to production, execute the schema file against your remote (production) database: ```sh npx wrangler d1 execute members-db --remote --file=./schemas/schema.sql ``` Now deploy your application to the Cloudflare network: ```sh npm run deploy ``` ```sh ⛅️ wrangler 4.44.0 ─────────────────── Total Upload: 1743.64 KiB / gzip: 498.65 KiB Worker Startup Time: 48 ms Your Worker has access to the following bindings: Binding Resource env.DB (members-db) D1 Database Uploaded express-d1-app (2.99 sec) Deployed express-d1-app triggers (5.26 sec) https://.workers.dev Current Version ID: ``` After successful deployment, Wrangler will output your Worker's URL. ## 12\. Test production deployment Test your deployed API using the provided URL. Replace `` with your actual Worker URL: ```sh curl https:///api/members ``` You should see the same member data you created in the production database. Create a new member in production: ```sh curl -X POST https:///api/members \ -H "Content-Type: application/json" \ -d '{"name": "Eva Martinez", "email": "eva@example.com"}' ``` Your Express.js application with D1 database is now running on Cloudflare Workers. ## Conclusion In this tutorial, you built a Members Registry API using Express.js and D1 database, then deployed it to Cloudflare Workers. You implemented full CRUD operations (Create, Read, Update, Delete) and learned how to: * Set up an Express.js application for Cloudflare Workers * Create and configure a D1 database with bindings * Implement database operations using D1's prepared statements * Test your API locally and in production ## Next steps * Learn more about [D1 database features](https://developers.cloudflare.com/d1/) * Explore [Workers routing and middleware](https://developers.cloudflare.com/workers/runtime-apis/) * Add authentication to your API using [Workers authentication](https://developers.cloudflare.com/workers/runtime-apis/handlers/) * Implement pagination for large datasets using [D1 query optimization](https://developers.cloudflare.com/d1/worker-api/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/deploy-an-express-app/#page","headline":"Deploy an Express.js application on Cloudflare Workers · Cloudflare Workers docs","description":"Learn how to deploy an Express.js application on Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/tutorials/deploy-an-express-app/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-10","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["TypeScript"]} ``` --- --- description: This tutorial explains how to programmatically generate a custom YouTube thumbnail using Cloudflare Workers. You may want to customize the thumbnail's design, call-to-actions and images used to encourage more viewers to watch your video. title: Generate YouTube thumbnails with Workers and Cloudflare Image Resizing image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Generate YouTube thumbnails with Workers and Cloudflare Image Resizing Last updated Aug 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/generate-youtube-thumbnails-with-workers-and-images/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn how to programmatically generate a custom YouTube thumbnail using Cloudflare Workers and Cloudflare Image Resizing. You may want to generate a custom YouTube thumbnail to customize the thumbnail's design, call-to-actions and images used to encourage more viewers to watch your video. This tutorial will help you understand how to work with [Images](https://developers.cloudflare.com/images/),[Image Resizing](https://developers.cloudflare.com/images/optimization/transformations/overview/) and [Cloudflare Workers](https://developers.cloudflare.com/workers/). ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). To follow this tutorial, make sure you have Node, Cargo, and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed on your machine. ## Learning goals In this tutorial, you will learn how to: * Upload Images to Cloudflare with the Cloudflare dashboard or API. * Set up a Worker project with Wrangler. * Manipulate images with image transformations in your Worker. ## Upload your image To generate a custom thumbnail image, you first need to upload a background image to Cloudflare Images. This will serve as the image you use for transformations to generate the thumbnails. Cloudflare Images allows you to store, resize, optimize and deliver images in a fast and secure manner. To get started, upload your images to the Cloudflare dashboard or use the Upload API. ### Upload with the dashboard To upload an image using the Cloudflare dashboard: 1. In the Cloudflare dashboard, go to the **Transformations** page. [Go to **Transformations** ↗](https://dash.cloudflare.com/?to=/:account/images/transformations) 2. Use **Quick Upload** to either drag and drop an image or click to browse and choose a file from your local files. 3. After the image is uploaded, view it using the generated URL. ### Upload with the API To upload your image with the [Upload via URL](https://developers.cloudflare.com/images/storage/upload-images/upload-url/) API, refer to the example below: ```sh curl --request POST \ --url https://api.cloudflare.com/client/v4/accounts//images/v1 \ --header 'Authorization: Bearer ' \ --form 'url=' \ --form 'metadata={"key":"value"}' \ --form 'requireSignedURLs=false' ``` * `ACCOUNT_ID`: The current user's account id which can be found in your account settings. * `API_TOKEN`: Needs to be generated to scoping Images permission. * `PATH_TO_IMAGE`: Indicates the URL for the image you want to upload. You will then receive a response similar to this: ```json { "result": { "id": "2cdc28f0-017a-49c4-9ed7-87056c83901", "filename": "image.jpeg", "metadata": { "key": "value" }, "uploaded": "2022-01-31T16:39:28.458Z", "requireSignedURLs": false, "variants": [ "https://imagedelivery.net/Vi7wi5KSItxGFsWRG2Us6Q/2cdc28f0-017a-49c4-9ed7-87056c83901/public", "https://imagedelivery.net/Vi7wi5KSItxGFsWRG2Us6Q/2cdc28f0-017a-49c4-9ed7-87056c83901/thumbnail" ] }, "success": true, "errors": [], "messages": [] } ``` Now that you have uploaded your image, you will use it as the background image for your video's thumbnail. ## Create a Worker to transform text to image After uploading your image, create a Worker that will enable you to transform text to image. This image can be used as an overlay on the background image you uploaded. Use the [rustwasm-worker-template ↗](https://github.com/cloudflare/workers-sdk/tree/main/templates/worker-rust). You will need the following before you begin: * A recent version of [Rust ↗](https://rustup.rs/). * Access to the `cargo-generate` subcommand: ```sh cargo install cargo-generate ``` Create a new Worker project using the `worker-rust` template: ```sh cargo generate https://github.com/cloudflare/rustwasm-worker-template ``` You will now make a few changes to the files in your project directory. 1. In the `lib.rs` file, add the following code block: ```rs use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get("/", |_, _| Response::ok("Hello from Workers!")) .run(req, env) .await } ``` 1. Update the `Cargo.toml` file in your `worker-to-text` project directory to use [text-to-png ↗](https://github.com/RookAndPawn/text-to-png), a Rust package for rendering text to PNG. Add the package as a dependency by running: ```sh cargo add text-to-png@0.2.0 ``` 1. Import the `text_to_png` library into your `worker-to-text` project's `lib.rs` file. ```rs use text_to_png::{TextPng, TextRenderer}; use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get("/", |_, _| Response::ok("Hello from Workers!")) .run(req, env) .await } ``` 1. Update `lib.rs` to create a `handle-slash` function that will activate the image transformation based on the text passed to the URL as a query parameter. ```rs use text_to_png::{TextPng, TextRenderer}; use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get("/", |_, _| Response::ok("Hello from Workers!")) .run(req, env) .await } async fn handle_slash(text: String) -> Result {} ``` 1. In the `handle-slash` function, call the `TextRenderer` by assigning it to a renderer value, specifying that you want to use a custom font. Then, use the `render_text_to_png_data` method to transform the text into image format. In this example, the custom font (`Inter-Bold.ttf`) is located in an `/assets` folder at the root of the project which will be used for generating the thumbnail. You must update this portion of the code to point to your custom font file. ```rs use text_to_png::{TextPng, TextRenderer}; use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get("/", |_, _| Response::ok("Hello from Workers!")) .run(req, env) .await } async fn handle_slash(text: String) -> Result { let renderer = TextRenderer::try_new_with_ttf_font_data(include_bytes!("../assets/Inter-Bold.ttf")) .expect("Example font is definitely loadable"); let text_png: TextPng = renderer.render_text_to_png_data(text.replace("+", " "), 60, "003682").unwrap(); } ``` 1. Rewrite the `Router` function to call `handle_slash` when a query is passed in the URL, otherwise return the `"Hello Worker!"` as the response. ```rs use text_to_png::{TextPng, TextRenderer}; use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get_async("/", |req, _| async move { if let Some(text) = req.url()?.query() { handle_slash(text.into()).await } else { handle_slash("Hello Worker!".into()).await } }) .run(req, env) .await } async fn handle_slash(text: String) -> Result { let renderer = TextRenderer::try_new_with_ttf_font_data(include_bytes!("../assets/Inter-Bold.ttf")) .expect("Example font is definitely loadable"); let text_png: TextPng = renderer.render_text_to_png_data(text.replace("+", " "), 60, "003682").unwrap(); } ``` 1. In your `lib.rs` file, set the headers to `content-type: image/png` so that the response is correctly rendered as a PNG image. ```rs use text_to_png::{TextPng, TextRenderer}; use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get_async("/", |req, _| async move { if let Some(text) = req.url()?.query() { handle_slash(text.into()).await } else { handle_slash("Hello Worker!".into()).await } }) .run(req, env) .await } async fn handle_slash(text: String) -> Result { let renderer = TextRenderer::try_new_with_ttf_font_data(include_bytes!("../assets/Inter-Bold.ttf")) .expect("Example font is definitely loadable"); let text_png: TextPng = renderer.render_text_to_png_data(text.replace("+", " "), 60, "003682").unwrap(); let mut headers = Headers::new(); headers.set("content-type", "image/png")?; Ok(Response::from_bytes(text_png.data)?.with_headers(headers)) } ``` The final `lib.rs` file should look as follows. Find the full code as an example repository on [GitHub ↗](https://github.com/cloudflare/workers-sdk/tree/main/templates/examples/worker-to-text). ```rs use text_to_png::{TextPng, TextRenderer}; use worker::*; mod utils; #[event(fetch)] pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result { // Optionally, get more helpful error messages written to the console in the case of a panic. utils::set_panic_hook(); let router = Router::new(); router .get_async("/", |req, _| async move { if let Some(text) = req.url()?.query() { handle_slash(text.into()).await } else { handle_slash("Hello Worker!".into()).await } }) .run(req, env) .await } async fn handle_slash(text: String) -> Result { let renderer = TextRenderer::try_new_with_ttf_font_data(include_bytes!("../assets/Inter-Bold.ttf")) .expect("Example font is definitely loadable"); let text = if text.len() > 128 { "Nope".into() } else { text }; let text = urlencoding::decode(&text).map_err(|_| worker::Error::BadEncoding)?; let text_png: TextPng = renderer.render_text_to_png_data(text.replace("+", " "), 60, "003682").unwrap(); let mut headers = Headers::new(); headers.set("content-type", "image/png")?; Ok(Response::from_bytes(text_png.data)?.with_headers(headers)) } ``` After you have finished updating your project, start a local server for developing your Worker by running: ```sh npx wrangler dev ``` This should spin up a `localhost` instance with the image displayed: ![Run wrangler dev to start a local server for your Worker](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1089,height=804,format=webp/_astro/hello-worker.ot1qb0cF.png) Adding a query parameter with custom text, you should receive: ![Follow the instructions above to receive an output image](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1115,height=662,format=webp/_astro/build-serverles.BHasze4F.png) To deploy your Worker, open your Wrangler file and update the `name` key with your project's name. Below is an example with this tutorial's project name: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker-to-text" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker-to-text" ``` Then run the `npx wrangler deploy` command to deploy your Worker. ```sh npx wrangler deploy ``` A `.workers.dev` domain will be generated for your Worker after running `wrangler deploy`. You will use this domain in the main thumbnail image. ## Create a Worker to display the original image Create a Worker to serve the image you uploaded to Images by running: npmyarnpnpm ``` npm create cloudflare@latest -- thumbnail-image ``` ``` yarn create cloudflare thumbnail-image ``` ``` pnpm create cloudflare@latest thumbnail-image ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `JavaScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). To start developing your Worker, `cd` into your new project directory: ```sh cd thumbnail-image ``` This will create a new Worker project named `thumbnail-image`. In the `src/index.js` file, add the following code block: ```js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/original-image") { const image = await fetch( `https://imagedelivery.net/${env.CLOUDFLARE_ACCOUNT_HASH}/${IMAGE_ID}/public`, ); return image; } return new Response("Image Resizing with a Worker"); }, }; ``` Update `env.CLOUDFLARE_ACCOUNT_HASH` with your [Cloudflare account ID](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/). Update `env.IMAGE_ID` with your [image ID](https://developers.cloudflare.com/images/get-started/). Run your Worker and go to the `/original-image` route to review your image. ## Add custom text on your image You will now use [Cloudflare image transformations](https://developers.cloudflare.com/images/optimization/transformations/overview/), with the `fetch` method, to add your dynamic text image as an overlay on top of your background image. Start by displaying the resulting image on a different route. Call the new route `/thumbnail`. ```js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/original-image") { const image = await fetch( `https://imagedelivery.net/${env.CLOUDFLARE_ACCOUNT_HASH}/${IMAGE_ID}/public`, ); return image; } if (url.pathname === "/thumbnail") { } return new Response("Image Resizing with a Worker"); }, }; ``` Next, use the `fetch` method to apply the image transformation changes on top of the background image. The overlay options are nested in `options.cf.image`. ```js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/original-image") { const image = await fetch( `https://imagedelivery.net/${env.CLOUDFLARE_ACCOUNT_HASH}/${IMAGE_ID}/public`, ); return image; } if (url.pathname === "/thumbnail") { fetch(imageURL, { cf: { image: {}, }, }); } return new Response("Image Resizing with a Worker"); }, }; ``` The `imageURL` is the URL of the image you want to use as a background image. In the `cf.image` object, specify the options you want to apply to the background image. Note At time of publication, Cloudflare image transformations do not allow resizing images in a Worker that is stored in Cloudflare Images. Instead of using the image you served on the `/original-image` route, you will use the same image from a different source. Add your background image to an assets directory on GitHub and push your changes to GitHub. Copy the URL of the image upload by performing a left click on the image and selecting the **Copy Remote File Url** option. Replace the `imageURL` value with the copied remote URL. ```js if (url.pathname === "/thumbnail") { const imageURL = "https://github.com/lauragift21/social-image-demo/blob/1ed9044463b891561b7438ecdecbdd9da48cdb03/assets/cover.png?raw=true"; fetch(imageURL, { cf: { image: {}, }, }); } ``` Next, add overlay options in the image object. Resize the image to the preferred width and height for YouTube thumbnails and use the [draw](https://developers.cloudflare.com/images/optimization/draw-overlays/) option to add overlay text using the deployed URL of your `text-to-image` Worker. ```js fetch(imageURL, { cf: { image: { width: 1280, height: 720, draw: [ { url: "https://text-to-image.examples.workers.dev", left: 40, }, ], }, }, }); ``` Image transformations can only be tested when you deploy your Worker. To deploy your Worker, open your Wrangler file and update the `name` key with your project's name. Below is an example with this tutorial's project name: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "thumbnail-image" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "thumbnail-image" ``` Deploy your Worker by running: ```sh npx wrangler deploy ``` The command deploys your Worker to custom `workers.dev` subdomain. Go to your `.workers.dev` subdomain and go to the `/thumbnail` route. You should see the resized image with the text `Hello Workers!`. ![Follow the steps above to generate your resized image.](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1280,height=720,format=webp/_astro/thumbnail.z6EOGa1_.png) You will now make text applied dynamic. Making your text dynamic will allow you change the text and have it update on the image automatically. To add dynamic text, append any text attached to the `/thumbnail` URL using query parameters and pass it down to the `text-to-image` Worker URL as a parameter. ```js for (const title of url.searchParams.values()) { try { const editedImage = await fetch(imageURL, { cf: { image: { width: 1280, height: 720, draw: [ { url: `https://text-to-image.examples.workers.dev/?${title}`, left: 50, }, ], }, }, }); return editedImage; } catch (error) { console.log(error); } } ``` By completing this tutorial, you have successfully made a custom YouTube thumbnail generator. ## Related resources In this tutorial, you learned how to use Cloudflare Workers and Cloudflare image transformations to generate custom YouTube thumbnails. To learn more about Cloudflare Workers and image transformations, refer to [Resize an image with a Worker](https://developers.cloudflare.com/images/optimization/transformations/transform-via-workers/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/generate-youtube-thumbnails-with-workers-and-images/#page","headline":"Generate YouTube thumbnails with Workers and Cloudflare Image Resizing · Cloudflare Workers docs","description":"This tutorial explains how to programmatically generate a custom YouTube thumbnail using Cloudflare Workers. You may want to customize the thumbnail's design, call-to-actions and images used to encourage more viewers to watch your video.","url":"https://developers.cloudflare.com/workers/tutorials/generate-youtube-thumbnails-with-workers-and-images/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript","Rust"]} ``` --- --- description: This tutorial shows you how to build an SMS notification system on Workers to receive updates on a GitHub repository. Your Worker will send you a text update using Twilio when there is new activity on your repository. title: GitHub SMS notifications using Twilio image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # GitHub SMS notifications using Twilio Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/github-sms-notifications-using-twilio/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn to build an SMS notification system on Workers to receive updates on a GitHub repository. Your Worker will send you a text update using Twilio when there is new activity on your repository. You will learn how to: * Build webhooks using Workers. * Integrate Workers with GitHub and Twilio. * Use Worker secrets with Wrangler. ![Animated gif of receiving a text message on your phone after pushing changes to a repository](https://developers.cloudflare.com/images/workers/tutorials/github-sms/video-of-receiving-a-text-after-pushing-to-a-repo.gif) --- ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## Create a Worker project Start by using `npm create cloudflare@latest` to create a Worker project in the command line: npmyarnpnpm ``` npm create cloudflare@latest -- github-twilio-notifications ``` ``` yarn create cloudflare github-twilio-notifications ``` ``` pnpm create cloudflare@latest github-twilio-notifications ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `JavaScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Make note of the URL that your application was deployed to. You will be using it when you configure your GitHub webhook. ```sh cd github-twilio-notifications ``` Inside of your new `github-sms-notifications` directory, `src/index.js` represents the entry point to your Cloudflare Workers application. You will configure this file for most of the tutorial. You will also need a GitHub account and a repository for this tutorial. If you do not have either setup, [create a new GitHub account ↗](https://github.com/join) and [create a new repository ↗](https://docs.github.com/en/get-started/quickstart/create-a-repo) to continue with this tutorial. First, create a webhook for your repository to post updates to your Worker. Inside of your Worker, you will then parse the updates. Finally, you will send a `POST` request to Twilio to send a text message to you. You can reference the finished code at this [GitHub repository ↗](https://github.com/rickyrobinett/workers-sdk/tree/main/templates/examples/github-sms-notifications-using-twilio). --- ## Configure GitHub To start, configure a GitHub webhook to post to your Worker when there is an update to the repository: 1. Go to your GitHub repository's **Settings** \> **Webhooks** \> **Add webhook**. 2. Set the Payload URL to the `/webhook` path on the Worker URL that you made note of when your application was first deployed. 3. In the **Content type** dropdown, select _application/json_. 4. In the **Secret** field, input a secret key of your choice. 5. In **Which events would you like to trigger this webhook?**, select **Let me select individual events**. Select the events you want to get notifications for (such as **Pull requests**, **Pushes**, and **Branch or tag creation**). 6. Select **Add webhook** to finish configuration. ![Following instructions to set up your webhook in the GitHub webhooks settings dashboard](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1259,height=1064,format=webp/_astro/github-config-screenshot.BR7flpMR.png) --- ## Parsing the response With your local environment set up, parse the repository update with your Worker. Initially, your generated `index.js` should look like this: ```js export default { async fetch(request, env, ctx) { return new Response("Hello World!"); }, }; ``` Use the `request.method` property of [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) to check if the request coming to your application is a `POST` request, and send an error response if the request is not a `POST` request. ```js export default { async fetch(request, env, ctx) { if (request.method !== "POST") { return new Response("Please send a POST request!"); } }, }; ``` Next, validate that the request is sent with the right secret key. GitHub attaches a hash signature for [each payload using the secret key ↗](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). Use a helper function called `checkSignature` on the request to ensure the hash is correct. Then, you can access data from the webhook by parsing the request as JSON. ```js async fetch(request, env, ctx) { if(request.method !== 'POST') { return new Response('Please send a POST request!'); } try { const rawBody = await request.text(); if (!checkSignature(rawBody, request.headers, env.GITHUB_SECRET_TOKEN)) { return new Response("Wrong password, try again", {status: 403}); } } catch (e) { return new Response(`Error: ${e}`); } }, ``` The `checkSignature` function will use the Node.js crypto library to hash the received payload with your known secret key to ensure it matches the request hash. GitHub uses an HMAC hexdigest to compute the hash in the SHA-256 format. You will place this function at the top of your `index.js` file, before your export. ```js import { createHmac, timingSafeEqual } from "node:crypto"; import { Buffer } from "node:buffer"; function checkSignature(text, headers, githubSecretToken) { const hmac = createHmac("sha256", githubSecretToken); hmac.update(text); const expectedSignature = hmac.digest("hex"); const actualSignature = headers.get("x-hub-signature-256"); const trusted = Buffer.from(`sha256=${expectedSignature}`, "ascii"); const untrusted = Buffer.from(actualSignature, "ascii"); return ( trusted.byteLength == untrusted.byteLength && timingSafeEqual(trusted, untrusted) ); } ``` To make this work, you need to use [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret-put) to set your `GITHUB_SECRET_TOKEN`. This token is the secret you picked earlier when configuring you GitHub webhook: ```sh npx wrangler secret put GITHUB_SECRET_TOKEN ``` Add the nodejs\_compat flag to your Wrangler file: ```jsonc { "compatibility_flags": [ "nodejs_compat" ] } ``` ```toml compatibility_flags = [ "nodejs_compat" ] ``` --- ## Sending a text with Twilio You will send a text message to you about your repository activity using Twilio. You need a Twilio account and a phone number that can receive text messages. [Refer to the Twilio guide to get set up ↗](https://www.twilio.com/messaging/sms). (If you are new to Twilio, they have [an interactive game ↗](https://www.twilio.com/quest) where you can learn how to use their platform and get some free credits for beginners to the service.) You can then create a helper function to send text messages by sending a `POST` request to the Twilio API endpoint. [Refer to the Twilio reference ↗](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource) to learn more about this endpoint. Create a new function called `sendText()` that will handle making the request to Twilio: ```js async function sendText(accountSid, authToken, message) { const endpoint = `https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`; const encoded = new URLSearchParams({ To: "%YOUR_PHONE_NUMBER%", From: "%YOUR_TWILIO_NUMBER%", Body: message, }); const token = btoa(`${accountSid}:${authToken}`); const request = { body: encoded, method: "POST", headers: { Authorization: `Basic ${token}`, "Content-Type": "application/x-www-form-urlencoded", }, }; const response = await fetch(endpoint, request); const result = await response.json(); return Response.json(result); } ``` To make this work, you need to set some secrets to hide your `ACCOUNT_SID` and `AUTH_TOKEN` from the source code. You can set secrets with [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret-put) in your command line. ```sh npx wrangler secret put TWILIO_ACCOUNT_SID npx wrangler secret put TWILIO_AUTH_TOKEN ``` Modify your `githubWebhookHandler` to send a text message using the `sendText` function you just made. ```js async fetch(request, env, ctx) { if(request.method !== 'POST') { return new Response('Please send a POST request!'); } try { const rawBody = await request.text(); if (!checkSignature(rawBody, request.headers, env.GITHUB_SECRET_TOKEN)) { return new Response('Wrong password, try again', {status: 403}); } const action = request.headers.get('X-GitHub-Event'); const json = JSON.parse(rawBody); const repoName = json.repository.full_name; const senderName = json.sender.login; return await sendText( env.TWILIO_ACCOUNT_SID, env.TWILIO_AUTH_TOKEN, `${senderName} completed ${action} onto your repo ${repoName}` ); } catch (e) { return new Response(`Error: ${e}`); } }; ``` Run the `npx wrangler deploy` command to redeploy your Worker project: ```sh npx wrangler deploy ``` ![Video of receiving a text after pushing to a repo](https://developers.cloudflare.com/images/workers/tutorials/github-sms/video-of-receiving-a-text-after-pushing-to-a-repo.gif) Now when you make an update (that you configured in the GitHub **Webhook** settings) to your repository, you will get a text soon after. If you have never used Git before, refer to the [GIT Push and Pull Tutorial ↗](https://www.datacamp.com/tutorial/git-push-pull) for pushing to your repository. Reference the finished code [on GitHub ↗](https://github.com/rickyrobinett/workers-sdk/tree/main/templates/examples/github-sms-notifications-using-twilio). By completing this tutorial, you have learned how to build webhooks using Workers, integrate Workers with GitHub and Twilio, and use Worker secrets with Wrangler. ## Related resources * [Build a JAMStack app](https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app/) * [Build a QR code generator](https://developers.cloudflare.com/workers/tutorials/build-a-qr-code-generator/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/github-sms-notifications-using-twilio/#page","headline":"GitHub SMS notifications using Twilio · Cloudflare Workers docs","description":"This tutorial shows you how to build an SMS notification system on Workers to receive updates on a GitHub repository. Your Worker will send you a text update using Twilio when there is new activity on your repository.","url":"https://developers.cloudflare.com/workers/tutorials/github-sms-notifications-using-twilio/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript"]} ``` --- --- description: Use Cloudflare Workers and Airtable to persist form submissions from a front-end user interface. Workers will handle incoming form submissions and use Airtables REST API to asynchronously persist the data in an Airtable base. title: Handle form submissions with Airtable image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Handle form submissions with Airtable Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/handle-form-submissions-with-airtable/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will use [Cloudflare Workers](https://developers.cloudflare.com/workers/) and [Airtable ↗](https://airtable.com) to persist form submissions from a front-end user interface. Airtable is a free-to-use spreadsheet solution that has an approachable API for developers. Workers will handle incoming form submissions and use Airtable's [REST API ↗](https://airtable.com/api) to asynchronously persist the data in an Airtable base (Airtable's term for a spreadsheet) for later reference. ![GIF of a complete Airtable and serverless function integration](https://developers.cloudflare.com/images/workers/tutorials/airtable/example.gif) ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## 1\. Create a form For this tutorial, you will be building a Workers function that handles input from a contact form. The form this tutorial references will collect a first name, last name, email address, phone number, message subject, and a message. Build a form If this is your first time building a form and you would like to follow a tutorial to create a form with Cloudflare Pages, refer to the [HTML forms](https://developers.cloudflare.com/pages/tutorials/forms) tutorial. Review a simplified example of the form used in this tuttorial. Note that the `action` parameter of the `
` tag should point to the deployed Workers application that you will build in this tutorial. ```html
``` ## 2\. Create a Worker project To handle the form submission, create and deploy a Worker that parses the incoming form data and prepares it for submission to Airtable. Create a new `airtable-form-handler` Worker project: npmyarnpnpm ``` npm create cloudflare@latest -- airtable-form-handler ``` ``` yarn create cloudflare airtable-form-handler ``` ``` pnpm create cloudflare@latest airtable-form-handler ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `JavaScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Then, move into the newly created directory: ```sh cd airtable-form-handler ``` ## 3\. Configure an Airtable base When your Worker is complete, it will send data up to an Airtable base via Airtable's REST API. If you do not have an Airtable account, create one (the free plan is sufficient to complete this tutorial). In Airtable's dashboard, create a new base by selecting **Start from scratch**. After you have created a new base, set it up for use with the front-end form. Delete the existing columns, and create six columns, with the following field types: | Field name | Airtable field type | | ------------ | ------------------- | | First Name | "Single line text" | | Last Name | "Single line text" | | Email | "Email" | | Phone Number | "Phone number" | | Subject | "Single line text" | | Message | "Long text" | Note that the field names are case-sensitive. If you change the field names, you will need to exactly match your new field names in the API request you make to Airtable later in the tutorial. Finally, you can optionally rename your table -- by defaulte it will have a name like Table 1\. In the below code, we assume the table has been renamed with a more descriptive name, like `Form Submissions`. Next, navigate to [Airtable's API page ↗](https://airtable.com/api) and select your new base. Note that you must be logged into Airtable to see your base information. In the API documentation page, find your **Airtable base ID**. You will also need to create a **Personal access token** that you'll use to access your Airtable base. You can do so by visiting the [Personal access tokens ↗](https://airtable.com/create/tokens) page on Airtable's website and creating a new token. Make sure that you configure the token in the following way: * Scope: the `data.records:write` scope must be set on the token * Access: access should be granted to the base you have been working with in this tutorial The results access token should now be set in your application. To make the token available in your codebase, use the [wrangler secret](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) command. The `secret` command encrypts and stores environment variables for use in your function, without revealing them to users. Run `wrangler secret put`, passing `AIRTABLE_ACCESS_TOKEN` as the name of your secret: ```sh npx wrangler secret put AIRTABLE_ACCESS_TOKEN ``` ```sh Enter the secret text you would like assigned to the variable AIRTABLE_ACCESS_TOKEN on the script named airtable-form-handler: ****** 🌀 Creating the secret for script name airtable-form-handler ✨ Success! Uploaded secret AIRTABLE_ACCESS_TOKEN. ``` Before you continue, review the keys that you should have from Airtable: 1. **Airtable Table Name**: The name for your table, like Form Submissions. 2. **Airtable Base ID**: The alphanumeric base ID found at the top of your base's API page. 3. **Airtable Access Token**: A Personal Access Token created by the user to access information about your new Airtable base. ## 4\. Submit data to Airtable With your Airtable base set up, and the keys and IDs you need to communicate with the API ready, you will now set up your Worker to persist data from your form into Airtable. In your Worker project's `index.js` file, replace the default code with a Workers fetch handler that can respond to requests. When the URL requested has a pathname of `/submit`, you will handle a new form submission, otherwise, you will return a `404 Not Found` response. ```js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/submit") { await submitHandler(request, env); } return new Response("Not found", { status: 404 }); }, }; ``` The `submitHandler` has two functions. First, it will parse the form data coming from your HTML5 form. Once the data is parsed, use the Airtable API to persist a new row (a new form submission) to your table: ```js async function submitHandler(request, env) { if (request.method !== "POST") { return new Response("Method Not Allowed", { status: 405, }); } const body = await request.formData(); const { first_name, last_name, email, phone, subject, message } = Object.fromEntries(body); // The keys in "fields" are case-sensitive, and // should exactly match the field names you set up // in your Airtable table, such as "First Name". const reqBody = { fields: { "First Name": first_name, "Last Name": last_name, Email: email, "Phone Number": phone, Subject: subject, Message: message, }, }; await createAirtableRecord(env, reqBody); } // Existing code // export default ... ``` Prevent potential errors when accessing request.body The body of a [Request ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request) can only be accessed once. If you previously used `request.formData()` in the same request, you may encounter a TypeError when attempting to access `request.body`. To avoid errors, create a clone of the Request object with `request.clone()` for each subsequent attempt to access a Request's body. Keep in mind that Workers have a [memory limit of 128 MB per Worker](https://developers.cloudflare.com/workers/platform/limits/#memory) and loading particularly large files into a Worker's memory multiple times may reach this limit. To ensure memory usage does not reach this limit, consider using [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/). While the majority of this function is concerned with parsing the request body (the data being sent as part of the request), there are two important things to note. First, if the HTTP method sent to this function is not `POST`, you will return a new response with the status code of [405 Method Not Allowed ↗](https://httpstatuses.com/405). The variable `reqBody` represents a collection of fields, which are key-value pairs for each column in your Airtable table. By formatting `reqBody` as an object with a collection of fields, you are creating a new record in your table with a value for each field. Then you call `createAirtableRecord` (the function you will define next). The `createAirtableRecord` function accepts a `body` parameter, which conforms to the Airtable API's required format — namely, a JavaScript object containing key-value pairs under `fields`, representing a single record to be created on your table: ```js async function createAirtableRecord(env, body) { try { const result = fetch( `https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent(env.AIRTABLE_TABLE_NAME)}`, { method: "POST", body: JSON.stringify(body), headers: { Authorization: `Bearer ${env.AIRTABLE_ACCESS_TOKEN}`, "Content-Type": "application/json", }, }, ); return result; } catch (error) { console.error(error); } } // Existing code // async function submitHandler // export default ... ``` To make an authenticated request to Airtable, you need to provide four constants that represent data about your Airtable account, base, and table name. You have already set `AIRTABLE_ACCESS_TOKEN` using `wrangler secret`, since it is a value that should be encrypted. The **Airtable base ID** and **table name**, and `FORM_URL` are values that can be publicly shared in places like GitHub. Use Wrangler's [vars](https://developers.cloudflare.com/workers/wrangler/migration/v1-to-v2/wrangler-legacy/configuration/#vars) feature to pass public environment variables from your Wrangler file. Add a `vars` table at the end of your Wrangler file: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "workers-airtable-form", "main": "src/index.js", // Set this to today's date "compatibility_date": "2026-08-14", "vars": { "AIRTABLE_BASE_ID": "exampleBaseId", "AIRTABLE_TABLE_NAME": "Form Submissions" } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "workers-airtable-form" main = "src/index.js" # Set this to today's date compatibility_date = "2026-08-14" [vars] AIRTABLE_BASE_ID = "exampleBaseId" AIRTABLE_TABLE_NAME = "Form Submissions" ``` With all these fields submitted, it is time to deploy your Workers serverless function and get your form communicating with it. First, publish your Worker: ```sh npx wrangler deploy ``` Your Worker project will deploy to a unique URL — for example, `https://workers-airtable-form.cloudflare.workers.dev`. This represents the first part of your front-end form's `action` attribute — the second part is the path for your form handler, which is `/submit`. In your front-end UI, configure your `form` tag as seen below: ```html
``` After you have deployed your new form (refer to the [HTML forms](https://developers.cloudflare.com/pages/tutorials/forms) tutorial if you need help creating a form), you should be able to submit a new form submission and see the value show up immediately in Airtable: ![Example GIF of complete Airtable and serverless function integration](https://developers.cloudflare.com/images/workers/tutorials/airtable/example.gif) ## Conclusion With this tutorial completed, you have created a Worker that can accept form submissions and persist them to Airtable. You have learned how to parse form data, set up environment variables, and use the `fetch` API to make requests to external services outside of your Worker. ## Related resources * [Build a Slackbot](https://developers.cloudflare.com/workers/tutorials/build-a-slackbot) * [Build a To-Do List Jamstack App](https://developers.cloudflare.com/workers/tutorials/build-a-jamstack-app) * [Build a blog using Nuxt.js and Sanity.io on Cloudflare Pages](https://developers.cloudflare.com/pages/tutorials/build-a-blog-using-nuxt-and-sanity) * [James Quick's video on building a Cloudflare Workers + Airtable integration ↗](https://www.youtube.com/watch?v=tFQ2kbiu1K4) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/handle-form-submissions-with-airtable/#page","headline":"Handle form submissions with Airtable · Cloudflare Workers docs","description":"Use Cloudflare Workers and Airtable to persist form submissions from a front-end user interface. Workers will handle incoming form submissions and use Airtables REST API to asynchronously persist the data in an Airtable base.","url":"https://developers.cloudflare.com/workers/tutorials/handle-form-submissions-with-airtable/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Forms","JavaScript"]} ``` --- --- description: This tutorial explains how to connect to a Cloudflare database using TCP Sockets and Hyperdrive. The Workers application you create in this tutorial will interact with a product database inside of MySQL. title: Connect to a MySQL database with Cloudflare Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Connect to a MySQL database with Cloudflare Workers Last updated Feb 2, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/mysql/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn how to create a Cloudflare Workers application and connect it to a MySQL database using [TCP Sockets](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) and [Hyperdrive](https://developers.cloudflare.com/hyperdrive/). The Workers application you create in this tutorial will interact with a product database inside of MySQL. Note We recommend using [Hyperdrive](https://developers.cloudflare.com/hyperdrive/) to connect to your MySQL database. Hyperdrive provides optimal performance and will ensure secure connectivity between your Worker and your MySQL database. When connecting directly to your MySQL database (without Hyperdrive), the MySQL drivers rely on unsupported Node.js APIs to create secure connections, which prevents connections. ## Prerequisites To continue: 1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages) if you have not already. 2. Install [npm ↗](https://docs.npmjs.com/getting-started). 3. Install [Node.js ↗](https://nodejs.org/en/). Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) requires a Node version of `16.17.0` or later. 4. Make sure you have access to a MySQL database. ## 1\. Create a Worker application First, use the [create-cloudflare CLI ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare) to create a new Worker application. To do this, open a terminal window and run the following command: npmyarnpnpm ``` npm create cloudflare@latest -- mysql-tutorial ``` ``` yarn create cloudflare mysql-tutorial ``` ``` pnpm create cloudflare@latest mysql-tutorial ``` This will prompt you to install the [create-cloudflare ↗](https://www.npmjs.com/package/create-cloudflare) package and lead you through a setup wizard. For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). If you choose to deploy, you will be asked to authenticate (if not logged in already), and your project will be deployed. If you deploy, you can still modify your Worker code and deploy again at the end of this tutorial. Now, move into the newly created directory: ```sh cd mysql-tutorial ``` ## 2\. Enable Node.js compatibility [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is required for database drivers, including mysql2, and needs to be configured for your Workers project. For compatibility dates of `2026-08-04` or later, Workers and Pages projects enable both `nodejs_compat` and `nodejs_compat_v2` by default. Built-in runtime APIs and polyfills are available without additional configuration. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. If your compatibility date is before `2026-08-04`, add the [nodejs\_compat](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag) [compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag) to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in: ```jsonc { "compatibility_flags": [ "nodejs_compat" ] } ``` ```toml compatibility_flags = [ "nodejs_compat" ] ``` To turn off [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) completely for a compatibility date of `2026-08-04` or later, remove the positive flags if present. Then add both `no_nodejs_compat` and `no_nodejs_compat_v2`. For configuration examples, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## 3\. Create a Hyperdrive configuration Create a Hyperdrive configuration using the connection string for your MySQL database. ```bash npx wrangler hyperdrive create --connection-string="mysql://user:password@HOSTNAME_OR_IP_ADDRESS:PORT/database_name" ``` This command outputs the Hyperdrive configuration `id` that will be used for your Hyperdrive [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/). Set up your binding by specifying the `id` in the Wrangler file. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "hyperdrive-example", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "compatibility_flags": [ "nodejs_compat" ], // Pasted from the output of `wrangler hyperdrive create --connection-string=[...]` above. "hyperdrive": [ { "binding": "HYPERDRIVE", "id": "" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "hyperdrive-example" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" compatibility_flags = [ "nodejs_compat" ] [[hyperdrive]] binding = "HYPERDRIVE" id = "" ``` ## 4\. Query your database from your Worker Install the [mysql2 ↗](https://github.com/sidorares/node-mysql2) driver: npmyarnpnpmbun ``` npm i mysql2@>3.13.0 ``` ``` yarn add mysql2@>3.13.0 ``` ``` pnpm add mysql2@>3.13.0 ``` ``` bun add mysql2@>3.13.0 ``` Note `mysql2` v3.13.0 or later is required Add the required Node.js compatibility flags and Hyperdrive binding to your `wrangler.jsonc` file: ```jsonc { // required for database drivers to function "compatibility_flags": [ "nodejs_compat" ], // Set this to today's date "compatibility_date": "2026-08-14", "hyperdrive": [ { "binding": "HYPERDRIVE", "id": "" } ] } ``` ```toml compatibility_flags = [ "nodejs_compat" ] # Set this to today's date compatibility_date = "2026-08-14" [[hyperdrive]] binding = "HYPERDRIVE" id = "" ``` Create a new `connection` instance and pass the Hyperdrive parameters: ```ts // mysql2 v3.13.0 or later is required import { createConnection } from "mysql2/promise"; export default { async fetch(request, env, ctx): Promise { // Create a new connection on each request. Hyperdrive maintains the underlying // database connection pool, so creating a new connection is fast. const connection = await createConnection({ host: env.HYPERDRIVE.host, user: env.HYPERDRIVE.user, password: env.HYPERDRIVE.password, database: env.HYPERDRIVE.database, port: env.HYPERDRIVE.port, // Required to enable mysql2 compatibility for Workers disableEval: true, }); try { // Sample query const [results, fields] = await connection.query("SHOW tables;"); // Return result rows as JSON return Response.json({ results, fields }); } catch (e) { console.error(e); return Response.json( { error: e instanceof Error ? e.message : e }, { status: 500 }, ); } }, } satisfies ExportedHandler; ``` Note The minimum version of `mysql2` required for Hyperdrive is `3.13.0`. ## 5\. Deploy your Worker Run the following command to deploy your Worker: ```sh npx wrangler deploy ``` Your application is now live and accessible at `..workers.dev`. ## Next steps To build more with databases and Workers, refer to [Tutorials](https://developers.cloudflare.com/workers/tutorials) and explore the [Databases documentation](https://developers.cloudflare.com/workers/databases). If you have any questions, need assistance, or would like to share your project, join the Cloudflare Developer community on [Discord ↗](https://discord.cloudflare.com) to connect with fellow developers and the Cloudflare team. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/mysql/#page","headline":"Connect to a MySQL database with Cloudflare Workers · Cloudflare Workers docs","description":"This tutorial explains how to connect to a Cloudflare database using TCP Sockets and Hyperdrive. The Workers application you create in this tutorial will interact with a product database inside of MySQL.","url":"https://developers.cloudflare.com/workers/tutorials/mysql/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-02-02","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["MySQL","TypeScript","SQL"]} ``` --- --- description: Build a project that leverages OpenAI's function calling feature, available in OpenAI's latest Chat Completions API models. title: OpenAI GPT function calling with JavaScript and Cloudflare Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # OpenAI GPT function calling with JavaScript and Cloudflare Workers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/openai-function-calls-workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will build a project that leverages [OpenAI's function calling ↗](https://platform.openai.com/docs/guides/function-calling) feature, available in OpenAI's latest Chat Completions API models. The function calling feature allows the AI model to intelligently decide when to call a function based on the input, and respond in JSON format to match the function's signature. You will use the function calling feature to request for the model to determine a website URL which contains information relevant to a message from the user, retrieve the text content of the site, and, finally, return a final response from the model informed by real-time web data. ## What you will learn * How to use OpenAI's function calling feature. * Integrating OpenAI's API in a Cloudflare Worker. * Fetching and processing website content using Cheerio. * Handling API responses and function calls in JavaScript. * Storing API keys as secrets with Wrangler. --- ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## 1\. Create a new Worker project Create a Worker project in the command line: npmyarnpnpm ``` npm create cloudflare@latest -- openai-function-calling-workers ``` ``` yarn create cloudflare openai-function-calling-workers ``` ``` pnpm create cloudflare@latest openai-function-calling-workers ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `JavaScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Go to your new `openai-function-calling-workers` Worker project: ```sh cd openai-function-calling-workers ``` Inside of your new `openai-function-calling-workers` directory, find the `src/index.js` file. You will configure this file for most of the tutorial. You will also need an OpenAI account and API key for this tutorial. If you do not have one, [create a new OpenAI account ↗](https://platform.openai.com/signup) and [create an API key ↗](https://platform.openai.com/account/api-keys) to continue with this tutorial. Make sure to store you API key somewhere safe so you can use it later. ## 2\. Make a request to OpenAI With your Worker project created, make your first request to OpenAI. You will use the OpenAI node library to interact with the OpenAI API. In this project, you will also use the Cheerio library to handle processing the HTML content of websites npmyarnpnpmbun ``` npm i openai cheerio ``` ``` yarn add openai cheerio ``` ``` pnpm add openai cheerio ``` ``` bun add openai cheerio ``` Now, define the structure of your Worker in `index.js`: ```js export default { async fetch(request, env, ctx) { // Initialize OpenAI API // Handle incoming requests return new Response("Hello World!"); }, }; ``` Above `export default`, add the imports for `openai` and `cheerio`: ```js import OpenAI from "openai"; import * as cheerio from "cheerio"; ``` Within your `fetch` function, instantiate your `OpenAI` client: ```js async fetch(request, env, ctx) { const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY, }); // Handle incoming requests return new Response('Hello World!'); }, ``` Use [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret-put) to set `OPENAI_API_KEY`. This [secret's](https://developers.cloudflare.com/workers/configuration/secrets/) value is the API key you created earlier in the OpenAI dashboard: ```sh npx wrangler secret put ``` For local development, create a new file `.dev.vars` in your Worker project and add this line. Make sure to replace `OPENAI_API_KEY` with your own OpenAI API key: ```txt OPENAI_API_KEY = "" ``` Now, make a request to the OpenAI [Chat Completions API ↗](https://platform.openai.com/docs/guides/gpt/chat-completions-api): ```js export default { async fetch(request, env, ctx) { const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY, }); const url = new URL(request.url); const message = url.searchParams.get("message"); const messages = [ { role: "user", content: message ? message : "What's in the news today?", }, ]; const tools = [ { type: "function", function: { name: "read_website_content", description: "Read the content on a given website", parameters: { type: "object", properties: { url: { type: "string", description: "The URL to the website to read", }, }, required: ["url"], }, }, }, ]; const chatCompletion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: messages, tools: tools, tool_choice: "auto", }); const assistantMessage = chatCompletion.choices[0].message; console.log(assistantMessage); //Later you will continue handling the assistant's response here return new Response(assistantMessage.content); }, }; ``` Review the arguments you are passing to OpenAI: * **model**: This is the model you want OpenAI to use for your request. In this case, you are using `gpt-4o-mini`. * **messages**: This is an array containing all messages that are part of the conversation. Initially you provide a message from the user, and we later add the response from the model. The content of the user message is either the `message` query parameter from the request URL or the default "What's in the news today?". * **tools**: An array containing the actions available to the AI model. In this example you only have one tool, `read_website_content`, which reads the content on a given website. * **name**: The name of your function. In this case, it is `read_website_content`. * **description**: A short description that lets the model know the purpose of the function. This is optional but helps the model know when to select the tool. * **parameters**: A JSON Schema object which describes the function. In this case we request a response containing an object with the required property `url`. * **tool\_choice**: This argument is technically optional as `auto` is the default. This argument indicates that either a function call or a normal message response can be returned by OpenAI. ## 3\. Building your `read_website_content()` function You will now need to define the `read_website_content` function, which is referenced in the `tools` array. The `read_website_content` function fetches the content of a given URL and extracts the text from `

` tags using the `cheerio` library: Add this code above the `export default` block in your `index.js` file: ```js async function read_website_content(url) { console.log("reading website content"); const response = await fetch(url); const body = await response.text(); let cheerioBody = cheerio.load(body); const resp = { website_body: cheerioBody("p").text(), url: url, }; return JSON.stringify(resp); } ``` In this function, you take the URL that you received from OpenAI and use JavaScript's [Fetch API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI/Using%5FFetch) to pull the content of the website and extract the paragraph text. Now we need to determine when to call this function. ## 4\. Process the Assistant's Messages Next, we need to process the response from the OpenAI API to check if it includes any function calls. If a function call is present, you should execute the corresponding function in your Worker. Note that the assistant may request multiple function calls. Modify the fetch method within the `export default` block as follows: ```js // ... your previous code ... if (assistantMessage.tool_calls) { for (const toolCall of assistantMessage.tool_calls) { if (toolCall.function.name === "read_website_content") { const url = JSON.parse(toolCall.function.arguments).url; const websiteContent = await read_website_content(url); messages.push({ role: "tool", tool_call_id: toolCall.id, name: toolCall.function.name, content: websiteContent, }); } } const secondChatCompletion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: messages, }); return new Response(secondChatCompletion.choices[0].message.content); } else { // this is your existing return statement return new Response(assistantMessage.content); } ``` Check if the assistant message contains any function calls by checking for the `tool_calls` property. Because the AI model can call multiple functions by default, you need to loop through any potential function calls and add them to the `messages` array. Each `read_website_content` call will invoke the `read_website_content` function you defined earlier and pass the URL generated by OpenAI as an argument. \` The `secondChatCompletion` is needed to provide a response informed by the data you retrieved from each function call. Now, the last step is to deploy your Worker. Test your code by running `npx wrangler dev` and open the provided url in your browser. This will now show you OpenAI’s response using real-time information from the retrieved web data. ## 5\. Deploy your Worker application To deploy your application, run the `npx wrangler deploy` command to deploy your Worker application: ```sh npx wrangler deploy ``` You can now preview your Worker at `..workers.dev`. Going to this URL will display the response from OpenAI. Optionally, add the `message` URL parameter to write a custom message: for example, `https://..workers.dev/?message=What is the weather in NYC today?`. ## 6\. Next steps Reference the [finished code for this tutorial on GitHub ↗](https://github.com/LoganGrasby/Cloudflare-OpenAI-Functions-Demo/blob/main/src/worker.js). To continue working with Workers and AI, refer to [the guide on using LangChain and Cloudflare Workers together ↗](https://blog.cloudflare.com/langchain-and-cloudflare/) or [how to build a ChatGPT plugin with Cloudflare Workers ↗](https://blog.cloudflare.com/magic-in-minutes-how-to-build-a-chatgpt-plugin-with-cloudflare-workers/). If you have any questions, need assistance, or would like to share your project, join the Cloudflare Developer community on [Discord ↗](https://discord.cloudflare.com) to connect with fellow developers and the Cloudflare team. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/openai-function-calls-workers/#page","headline":"OpenAI GPT function calling with JavaScript and Cloudflare Workers · Cloudflare Workers docs","description":"Build a project that leverages OpenAI's function calling feature, available in OpenAI's latest Chat Completions API models.","url":"https://developers.cloudflare.com/workers/tutorials/openai-function-calls-workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI","JavaScript"]} ``` --- --- description: This tutorial explains how to connect to a Postgres database with Cloudflare Workers. The Workers application you create in this tutorial will interact with a product database inside of Postgres. title: Connect to a PostgreSQL database with Cloudflare Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Connect to a PostgreSQL database with Cloudflare Workers Last updated Mar 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/postgres/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn how to create a Cloudflare Workers application and connect it to a PostgreSQL database using [TCP Sockets](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) and [Hyperdrive](https://developers.cloudflare.com/hyperdrive/). The Workers application you create in this tutorial will interact with a product database inside of PostgreSQL. ## Prerequisites To continue: 1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages) if you have not already. 2. Install [npm ↗](https://docs.npmjs.com/getting-started). 3. Install [Node.js ↗](https://nodejs.org/en/). Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) requires a Node version of `16.17.0` or later. 4. Make sure you have access to a PostgreSQL database. ## 1\. Create a Worker application First, use the [create-cloudflare CLI ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare) to create a new Worker application. To do this, open a terminal window and run the following command: npmyarnpnpm ``` npm create cloudflare@latest -- postgres-tutorial ``` ``` yarn create cloudflare postgres-tutorial ``` ``` pnpm create cloudflare@latest postgres-tutorial ``` This will prompt you to install the [create-cloudflare ↗](https://www.npmjs.com/package/create-cloudflare) package and lead you through a setup wizard. For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). If you choose to deploy, you will be asked to authenticate (if not logged in already), and your project will be deployed. If you deploy, you can still modify your Worker code and deploy again at the end of this tutorial. Now, move into the newly created directory: ```sh cd postgres-tutorial ``` ### Enable Node.js compatibility [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is required for database drivers, including Postgres.js, and needs to be configured for your Workers project. For compatibility dates of `2026-08-04` or later, Workers and Pages projects enable both `nodejs_compat` and `nodejs_compat_v2` by default. Built-in runtime APIs and polyfills are available without additional configuration. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. If your compatibility date is before `2026-08-04`, add the [nodejs\_compat](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag) [compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag) to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in: ```jsonc { "compatibility_flags": [ "nodejs_compat" ] } ``` ```toml compatibility_flags = [ "nodejs_compat" ] ``` To turn off [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) completely for a compatibility date of `2026-08-04` or later, remove the positive flags if present. Then add both `no_nodejs_compat` and `no_nodejs_compat_v2`. For configuration examples, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## 2\. Add the PostgreSQL connection library To connect to a PostgreSQL database, you will need the `pg` library. In your Worker application directory, run the following command to install the library: npmyarnpnpmbun ``` npm i pg ``` ``` yarn add pg ``` ``` pnpm add pg ``` ``` bun add pg ``` Next, install the TypeScript types for the `pg` library to enable type checking and autocompletion in your TypeScript code: npmyarnpnpmbun ``` npm i -D @types/pg ``` ``` yarn add -D @types/pg ``` ``` pnpm add -D @types/pg ``` ``` bun add -d @types/pg ``` Note Make sure you are using `pg` (`node-postgres`) version `8.16.3` or higher. ## 3\. Configure the connection to the PostgreSQL database Choose one of the two methods to connect to your PostgreSQL database: 1. [Use a connection string](#use-a-connection-string). 2. [Set explicit parameters](#set-explicit-parameters). ### Use a connection string A connection string contains all the information needed to connect to a database. It is a URL that contains the following information: ```plaintext postgresql://username:password@host:port/database ``` Replace `username`, `password`, `host`, `port`, and `database` with the appropriate values for your PostgreSQL database. Set your connection string as a [secret](https://developers.cloudflare.com/workers/configuration/secrets/) so that it is not stored as plain text. Use [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) with the example variable name `DB_URL`: ```sh npx wrangler secret put DB_URL ``` ```sh ➜ wrangler secret put DB_URL ------------------------------------------------------- ? Enter a secret value: › ******************** ✨ Success! Uploaded secret DB_URL ``` Set your `DB_URL` secret locally in a `.dev.vars` file as documented in [Local Development with Secrets](https://developers.cloudflare.com/workers/configuration/secrets/). ```toml DB_URL="" ``` ### Set explicit parameters Configure each database parameter as an [environment variable](https://developers.cloudflare.com/workers/configuration/environment-variables/) via the [Cloudflare dashboard](https://developers.cloudflare.com/workers/configuration/environment-variables/#add-environment-variables-via-the-dashboard) or in your Wrangler file. Refer to an example of a Wrangler file configuration: ```jsonc { "vars": { "DB_USERNAME": "postgres", // Set your password by creating a secret so it is not stored as plain text "DB_HOST": "ep-aged-sound-175961.us-east-2.aws.neon.tech", "DB_PORT": 5432, "DB_NAME": "productsdb" } } ``` ```toml [vars] DB_USERNAME = "postgres" DB_HOST = "ep-aged-sound-175961.us-east-2.aws.neon.tech" DB_PORT = 5_432 DB_NAME = "productsdb" ``` To set your password as a [secret](https://developers.cloudflare.com/workers/configuration/secrets/) so that it is not stored as plain text, use [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret). `DB_PASSWORD` is an example variable name for this secret to be accessed in your Worker: ```sh npx wrangler secret put DB_PASSWORD ``` ```sh ------------------------------------------------------- ? Enter a secret value: › ******************** ✨ Success! Uploaded secret DB_PASSWORD ``` ## 4\. Connect to the PostgreSQL database in the Worker Open your Worker's main file (for example, `worker.ts`) and import the `Client` class from the `pg` library: ```typescript import { Client } from "pg"; ``` In the `fetch` event handler, connect to the PostgreSQL database using your chosen method, either the connection string or the explicit parameters. ### Use a connection string ```typescript // create a new Client instance using the connection string const sql = new Client({ connectionString: env.DB_URL }); // connect to the PostgreSQL database await sql.connect(); ``` ### Set explicit parameters ```typescript // create a new Client instance using explicit parameters const sql = new Client({ username: env.DB_USERNAME, password: env.DB_PASSWORD, host: env.DB_HOST, port: env.DB_PORT, database: env.DB_NAME, ssl: true, // Enable SSL for secure connections }); // connect to the PostgreSQL database await sql.connect(); ``` ## 5\. Interact with the products database To demonstrate how to interact with the products database, you will fetch data from the `products` table by querying the table when a request is received. Note If you are following along in your own PostgreSQL instance, set up the `products` using the following SQL `CREATE TABLE` statement. This statement defines the columns and their respective data types for the `products` table: ```sql CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL ); ``` Replace the existing code in your `worker.ts` file with the following code: ```typescript import { Client } from "pg"; export default { async fetch(request, env, ctx): Promise { // Create a new Client instance using the connection string // or explicit parameters as shown in the previous steps. // Here, we are using the connection string method. const sql = new Client({ connectionString: env.DB_URL, }); // Connect to the PostgreSQL database await sql.connect(); // Query the products table const result = await sql.query("SELECT * FROM products"); // Return the result as JSON return new Response(JSON.stringify(result.rows), { headers: { "Content-Type": "application/json", }, }); }, } satisfies ExportedHandler; ``` This code establishes a connection to the PostgreSQL database within your Worker application and queries the `products` table, returning the results as a JSON response. ## 6\. Deploy your Worker Run the following command to deploy your Worker: ```sh npx wrangler deploy ``` Your application is now live and accessible at `..workers.dev`. After deploying, you can interact with your PostgreSQL products database using your Cloudflare Worker. Whenever a request is made to your Worker's URL, it will fetch data from the `products` table and return it as a JSON response. You can modify the query as needed to retrieve the desired data from your products database. ## 7\. Insert a new row into the products database To insert a new row into the `products` table, create a new API endpoint in your Worker that handles a `POST` request. When a `POST` request is received with a JSON payload, the Worker will insert a new row into the `products` table with the provided data. Assume the `products` table has the following columns: `id`, `name`, `description`, and `price`. Add the following code snippet inside the `fetch` event handler in your `worker.ts` file, before the existing query code: ```typescript import { Client } from "pg"; export default { async fetch(request, env, ctx): Promise { // Create a new Client instance using the connection string // or explicit parameters as shown in the previous steps. // Here, we are using the connection string method. const sql = new Client({ connectionString: env.DB_URL, }); // Connect to the PostgreSQL database await sql.connect(); const url = new URL(request.url); if (request.method === "POST" && url.pathname === "/products") { // Parse the request's JSON payload const productData = (await request.json()) as { name: string; description: string; price: number; }; const name = productData.name, description = productData.description, price = productData.price; // Insert the new product into the products table const insertResult = await sql.query( `INSERT INTO products(name, description, price) VALUES($1, $2, $3) RETURNING *`, [name, description, price], ); // Return the inserted row as JSON return new Response(JSON.stringify(insertResult.rows), { headers: { "Content-Type": "application/json" }, }); } // Query the products table const result = await sql.query("SELECT * FROM products"); // Return the result as JSON return new Response(JSON.stringify(result.rows), { headers: { "Content-Type": "application/json", }, }); }, } satisfies ExportedHandler; ``` This code snippet does the following: 1. Checks if the request is a `POST` request and the URL path is `/products`. 2. Parses the JSON payload from the request. 3. Constructs an `INSERT` SQL query using the provided product data. 4. Executes the query, inserting the new row into the `products` table. 5. Returns the inserted row as a JSON response. Now, when you send a `POST` request to your Worker's URL with the `/products` path and a JSON payload, the Worker will insert a new row into the `products` table with the provided data. When a request to `/` is made, the Worker will return all products in the database. After making these changes, deploy the Worker again by running: ```sh npx wrangler deploy ``` You can now use your Cloudflare Worker to insert new rows into the `products` table. To test this functionality, send a `POST` request to your Worker's URL with the `/products` path, along with a JSON payload containing the new product data: ```json { "name": "Sample Product", "description": "This is a sample product", "price": 19.99 } ``` You have successfully created a Cloudflare Worker that connects to a PostgreSQL database and handles fetching data and inserting new rows into a products table. ## 8\. Use Hyperdrive to accelerate queries Create a Hyperdrive configuration using the connection string for your PostgreSQL database. ```bash npx wrangler hyperdrive create --connection-string="postgres://user:password@HOSTNAME_OR_IP_ADDRESS:PORT/database_name" --caching-disabled ``` This command outputs the Hyperdrive configuration `id` that will be used for your Hyperdrive [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/). Set up your binding by specifying the `id` in the Wrangler file. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "hyperdrive-example", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "compatibility_flags": [ "nodejs_compat" ], // Pasted from the output of `wrangler hyperdrive create --connection-string=[...]` above. "hyperdrive": [ { "binding": "HYPERDRIVE", "id": "" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "hyperdrive-example" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" compatibility_flags = [ "nodejs_compat" ] [[hyperdrive]] binding = "HYPERDRIVE" id = "" ``` Create the types for your Hyperdrive binding using the following command: ```bash npx wrangler types ``` Replace your existing connection string in your Worker code with the Hyperdrive connection string. ```js export default { async fetch(request, env, ctx): Promise { const sql = new Client({connectionString: env.HYPERDRIVE.connectionString}) const url = new URL(request.url); //rest of the routes and database queries }, } satisfies ExportedHandler; ``` ## 9\. Redeploy your Worker Run the following command to deploy your Worker: ```sh npx wrangler deploy ``` Your Worker application is now live and accessible at `..workers.dev`, using Hyperdrive. Hyperdrive accelerates database queries by pooling your connections and caching your requests across the globe. ## Next steps To build more with databases and Workers, refer to [Tutorials](https://developers.cloudflare.com/workers/tutorials) and explore the [Databases documentation](https://developers.cloudflare.com/workers/databases). If you have any questions, need assistance, or would like to share your project, join the Cloudflare Developer community on [Discord ↗](https://discord.cloudflare.com) to connect with fellow developers and the Cloudflare team. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/postgres/#page","headline":"Connect to a PostgreSQL database with Cloudflare Workers · Cloudflare Workers docs","description":"This tutorial explains how to connect to a Postgres database with Cloudflare Workers. The Workers application you create in this tutorial will interact with a product database inside of Postgres.","url":"https://developers.cloudflare.com/workers/tutorials/postgres/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-03-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["PostgreSQL","TypeScript","SQL"]} ``` --- --- description: This tutorial explains how to send transactional emails from Workers using Postmark. title: Send Emails With Postmark image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Send Emails With Postmark Last updated Jun 9, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/send-emails-with-postmark/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn how to send transactional emails from Workers using [Postmark ↗](https://postmarkapp.com/). At the end of this tutorial, you’ll be able to: * Create a Worker to send emails. * Sign up and add a Cloudflare domain to Postmark. * Send emails from your Worker using Postmark. * Store API keys securely with secrets. ## Prerequisites To continue with this tutorial, you’ll need: * A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages), if you don’t already have one. * A [registered](https://developers.cloudflare.com/registrar/get-started/register-domain/) domain. * Installed [npm ↗](https://docs.npmjs.com/getting-started). * A [Postmark account ↗](https://account.postmarkapp.com/sign%5Fup). ## Create a Worker project Start by using [C3](https://developers.cloudflare.com/pages/get-started/c3/) to create a Worker project in the command line, then, answer the prompts: ```sh npm create cloudflare@latest ``` Alternatively, you can use CLI arguments to speed things up: ```sh npm create cloudflare@latest email-with-postmark -- --type=hello-world --ts=false --git=true --deploy=false ``` This creates a simple hello-world Worker having the following content: ```js export default { async fetch(request, env, ctx) { return new Response("Hello World!"); }, }; ``` ## Add your domain to Postmark If you don’t already have a Postmark account, you can sign up for a [free account here ↗](https://account.postmarkapp.com/sign%5Fup). After signing up, check your inbox for a link to confirm your sender signature. This verifies and enables you to send emails from your registered email address. To enable email sending from other addresses on your domain, navigate to `Sender Signatures` on the Postmark dashboard, `Add Domain or Signature` \> `Add Domain`, then type in your domain and click on `Verify Domain`. Next, you’re presented with a list of DNS records to add to your Cloudflare domain. On your Cloudflare dashboard, select the domain you entered earlier and navigate to `DNS` \> `Records`. Copy/paste the DNS records (DKIM, and Return-Path) from Postmark to your Cloudflare domain. ![Image of adding DNS records to a Cloudflare domain](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1922,height=790,format=webp/_astro/add_dns_records.CuwqhmEV.png) Note If you need more help adding DNS records in Cloudflare, refer to [Manage DNS records](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/). When that’s done, head back to Postmark and click on the `Verify` buttons. If all records are properly configured, your domain status should be updated to `Verified`. ![Image of domain verification on the Postmark dashboard](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2338,height=1222,format=webp/_astro/verified_domain.CSwUI8xQ.png) To grab your API token, navigate to the `Servers` tab, then `My First Server` \> `API Tokens`, then copy your API key to a safe place. ## Send emails from your Worker The final step is putting it all together in a Worker. In your Worker, make a post request with `fetch` to Postmark’s email API and include your token and message body: Note [Postmark’s JavaScript library ↗](https://www.npmjs.com/package/postmark) is currently not supported on Workers. Use the [email API ↗](https://postmarkapp.com/developer/user-guide/send-email-with-api) instead. ```jsx export default { async fetch(request, env, ctx) { return await fetch("https://api.postmarkapp.com/email", { method: "POST", headers: { "Content-Type": "application/json", "X-Postmark-Server-Token": "your_postmark_api_token_here", }, body: JSON.stringify({ From: "hello@example.com", To: "someone@example.com", Subject: "Hello World", HtmlBody: "

Hello from Workers

", }), }); }, }; ``` To test your code locally, run the following command and navigate to [http://localhost:8787/ ↗](http://localhost:8787/) in a browser: ```sh npm start ``` Deploy your Worker with `npm run deploy`. ## Move API token to Secrets Sensitive information such as API keys and token should always be stored in secrets. All secrets are encrypted to add an extra layer of protection. That said, it’s a good idea to move your API token to a secret and access it from the environment of your Worker. To add secrets for local development, create a `.dev.vars` file which works exactly like a `.env` file: ```txt POSTMARK_API_TOKEN=your_postmark_api_token_here ``` Also ensure the secret is added to your deployed worker by running: ```sh npx wrangler secret put POSTMARK_API_TOKEN ``` The added secret can be accessed on via the `env` parameter passed to your Worker’s fetch event handler: ```jsx export default { async fetch(request, env, ctx) { return await fetch("https://api.postmarkapp.com/email", { method: "POST", headers: { "Content-Type": "application/json", "X-Postmark-Server-Token": env.POSTMARK_API_TOKEN, }, body: JSON.stringify({ From: "hello@example.com", To: "someone@example.com", Subject: "Hello World", HtmlBody: "

Hello from Workers

", }), }); }, }; ``` And finally, deploy this update with `npm run deploy`. ## Related resources * [Storing API keys and tokens with Secrets](https://developers.cloudflare.com/workers/configuration/secrets/). * [Transferring your domain to Cloudflare](https://developers.cloudflare.com/registrar/get-started/transfer-domain-to-cloudflare/). * [Send emails from Workers](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/send-emails-with-postmark/#page","headline":"Send Emails With Postmark · Cloudflare Workers docs","description":"This tutorial explains how to send transactional emails from Workers using Postmark.","url":"https://developers.cloudflare.com/workers/tutorials/send-emails-with-postmark/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-09","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript"]} ``` --- --- description: This tutorial explains how to send emails from Cloudflare Workers using Resend. title: Send Emails With Resend image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Send Emails With Resend Last updated Jun 9, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/send-emails-with-resend/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) In this tutorial, you will learn how to send transactional emails from Workers using [Resend ↗](https://resend.com/). At the end of this tutorial, you’ll be able to: * Create a Worker to send emails. * Sign up and add a Cloudflare domain to Resend. * Send emails from your Worker using Resend. * Store API keys securely with secrets. ## Prerequisites To continue with this tutorial, you’ll need: * A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages), if you don’t already have one. * A [registered](https://developers.cloudflare.com/registrar/get-started/register-domain/) domain. * Installed [npm ↗](https://docs.npmjs.com/getting-started). * A [Resend account ↗](https://resend.com/signup). ## Create a Worker project Start by using [C3](https://developers.cloudflare.com/pages/get-started/c3/) to create a Worker project in the command line, then, answer the prompts: ```sh npm create cloudflare@latest ``` Alternatively, you can use CLI arguments to speed things up: ```sh npm create cloudflare@latest email-with-resend -- --type=hello-world --ts=false --git=true --deploy=false ``` This creates a simple hello-world Worker having the following content: ```js export default { async fetch(request, env, ctx) { return new Response("Hello World!"); }, }; ``` ## Add your domain to Resend If you don’t already have a Resend account, you can sign up for a [free account here ↗](https://resend.com/signup). After signing up, go to `Domains` using the side menu, and click the button to add a new domain. On the modal, enter the domain you want to add and then select a region. Next, you’re presented with a list of DNS records to add to your Cloudflare domain. On your Cloudflare dashboard, select the domain you entered earlier and navigate to `DNS` \> `Records`. Copy/paste the DNS records (DKIM, SPF, and DMARC records) from Resend to your Cloudflare domain. ![Image of adding DNS records to a Cloudflare domain](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2012,height=936,format=webp/_astro/add_dns_records.Brij3X2H.png) Note If you need more help adding DNS records in Cloudflare, refer to [Manage DNS records](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/). When that’s done, head back to Resend and click on the `Verify DNS Records` button. If all records are properly configured, your domain status should be updated to `Verified`. ![Image of domain verification on the Resend dashboard](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1944,height=1116,format=webp/_astro/verified_domain.ouYLJaQl.png) Lastly, navigate to `API Keys` with the side menu, to create an API key. Give your key a descriptive name and the appropriate permissions. Click the button to add your key and then copy your API key to a safe location. ## Send emails from your Worker The final step is putting it all together in a Worker. Open up a terminal in the directory of the Worker you created earlier. Then, install the Resend SDK: ```sh npm i resend ``` In your Worker, import and use the Resend library like so: ```jsx import { Resend } from "resend"; export default { async fetch(request, env, ctx) { const resend = new Resend("your_resend_api_key"); const { data, error } = await resend.emails.send({ from: "hello@example.com", to: "someone@example.com", subject: "Hello World", html: "

Hello from Workers

", }); return Response.json({ data, error }); }, }; ``` To test your code locally, run the following command and navigate to [http://localhost:8787/ ↗](http://localhost:8787/) in a browser: ```sh npm start ``` Deploy your Worker with `npm run deploy`. ## Move API keys to Secrets Sensitive information such as API keys and token should always be stored in secrets. All secrets are encrypted to add an extra layer of protection. That said, it’s a good idea to move your API key to a secret and access it from the environment of your Worker. To add secrets for local development, create a `.dev.vars` file which works exactly like a `.env` file: ```txt RESEND_API_KEY=your_resend_api_key ``` Also ensure the secret is added to your deployed worker by running: ```sh npx wrangler secret put RESEND_API_KEY ``` The added secret can be accessed on via the `env` parameter passed to your Worker’s fetch event handler: ```jsx import { Resend } from "resend"; export default { async fetch(request, env, ctx) { const resend = new Resend(env.RESEND_API_KEY); const { data, error } = await resend.emails.send({ from: "hello@example.com", to: "someone@example.com", subject: "Hello World", html: "

Hello from Workers

", }); return Response.json({ data, error }); }, }; ``` And finally, deploy this update with `npm run deploy`. ## Related resources * [Storing API keys and tokens with Secrets](https://developers.cloudflare.com/workers/configuration/secrets/). * [Transferring your domain to Cloudflare](https://developers.cloudflare.com/registrar/get-started/transfer-domain-to-cloudflare/). * [Send emails from Workers](https://developers.cloudflare.com/email-service/api/send-emails/workers-api/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/send-emails-with-resend/#page","headline":"Send Emails With Resend · Cloudflare Workers docs","description":"This tutorial explains how to send emails from Cloudflare Workers using Resend.","url":"https://developers.cloudflare.com/workers/tutorials/send-emails-with-resend/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-09","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["JavaScript"]} ``` --- --- description: This tutorial explains how to create a TypeScript-based Cloudflare Workers project that can securely access files from and upload files to a CloudFlare R2 bucket. title: Securely access and upload assets with Cloudflare R2 image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Securely access and upload assets with Cloudflare R2 Last updated Mar 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/upload-assets-with-r2/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This tutorial explains how to create a TypeScript-based Cloudflare Workers project that can securely access files from and upload files to a [Cloudflare R2](https://developers.cloudflare.com/r2/) bucket. Cloudflare R2 allows developers to store large amounts of unstructured data without the costly egress bandwidth fees associated with typical cloud storage services. ## Prerequisites To continue: 1. Sign up for a [Cloudflare account ↗](https://dash.cloudflare.com/sign-up/workers-and-pages) if you have not already. 2. Install [npm ↗](https://docs.npmjs.com/getting-started). 3. Install [Node.js ↗](https://nodejs.org/en/). Use a Node version manager like [Volta ↗](https://volta.sh/) or [nvm ↗](https://github.com/nvm-sh/nvm) to avoid permission issues and change Node.js versions. [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) requires a Node version of `16.17.0` or later. ## Create a Worker application First, use the [create-cloudflare CLI ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare) to create a new Worker. To do this, open a terminal window and run the following command: npmyarnpnpm ``` npm create cloudflare@latest -- upload-r2-assets ``` ``` yarn create cloudflare upload-r2-assets ``` ``` pnpm create cloudflare@latest upload-r2-assets ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Worker only`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). Move into your newly created directory: ```sh cd upload-r2-assets ``` ## Create an R2 bucket Before you integrate R2 bucket access into your Worker application, an R2 bucket must be created: ```sh npx wrangler r2 bucket create ``` Replace `` with the name you want to assign to your bucket. List your account's R2 buckets to verify that a new bucket has been added: ```sh npx wrangler r2 bucket list ``` ## Configure access to an R2 bucket After your new R2 bucket is ready, use it inside your Worker application. Use your R2 bucket inside your Worker project by modifying the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to include an R2 bucket [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/). Add the following R2 bucket binding to your Wrangler file: ```jsonc { "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "" } ] } ``` ```toml [[r2_buckets]] binding = "MY_BUCKET" bucket_name = "" ``` Give your R2 bucket binding name. Replace `` with the name of the R2 bucket you created earlier. Your Worker application can now access your R2 bucket using the `MY_BUCKET` variable. You can now perform CRUD (Create, Read, Update, Delete) operations on the contents of the bucket. ## Fetch from an R2 bucket After setting up an R2 bucket binding, you will implement the functionalities for the Worker to interact with the R2 bucket, such as, fetching files from the bucket and uploading files to the bucket. To fetch files from the R2 bucket, use the `BINDING.get` function. In the below example, the R2 bucket binding is called `MY_BUCKET`. Using `.get(key)`, you can retrieve an asset based on the URL pathname as the key. In this example, the URL pathname is `/image.png`, and the asset key is `image.png`. ```ts interface Env { MY_BUCKET: R2Bucket; } export default { async fetch(request, env): Promise { // For example, the request URL my-worker.account.workers.dev/image.png const url = new URL(request.url); const key = url.pathname.slice(1); // Retrieve the key "image.png" const object = await env.MY_BUCKET.get(key); if (object === null) { return new Response("Object Not Found", { status: 404 }); } const headers = new Headers(); object.writeHttpMetadata(headers); headers.set("etag", object.httpEtag); return new Response(object.body, { headers, }); }, } satisfies ExportedHandler; ``` The code written above fetches and returns data from the R2 bucket when a `GET` request is made to the Worker application using a specific URL path. ## Upload securely to an R2 bucket Next, you will add the ability to upload to your R2 bucket using authentication. To securely authenticate your upload requests, use [Wrangler's secret capability](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret). Wrangler was installed when you ran the `create cloudflare@latest` command. Create a secret value of your choice -- for instance, a random string or password. Using the Wrangler CLI, add the secret to your project as `AUTH_SECRET`: ```sh npx wrangler secret put AUTH_SECRET ``` Now, add a new code path that handles a `PUT` HTTP request. This new code will check that the previously uploaded secret is correctly used for authentication, and then upload to R2 using `MY_BUCKET.put(key, data)`: ```ts interface Env { MY_BUCKET: R2Bucket; AUTH_SECRET: string; } export default { async fetch(request, env): Promise { if (request.method === "PUT") { // Note that you could require authentication for all requests // by moving this code to the top of the fetch function. const auth = request.headers.get("Authorization"); const expectedAuth = `Bearer ${env.AUTH_SECRET}`; if (!auth || auth !== expectedAuth) { return new Response("Unauthorized", { status: 401 }); } const url = new URL(request.url); const key = url.pathname.slice(1); await env.MY_BUCKET.put(key, request.body); return new Response(`Object ${key} uploaded successfully!`); } // include the previous code here... }, } satisfies ExportedHandler; ``` This approach ensures that only clients who provide a valid bearer token, via the `Authorization` header equal to the `AUTH_SECRET` value, will be permitted to upload to the R2 bucket. If you used a different binding name than `AUTH_SECRET`, replace it in the code above. ## Deploy your Worker application After completing your Cloudflare Worker project, deploy it to Cloudflare. Make sure you are in your Worker application directory that you created for this tutorial, then run: ```sh npx wrangler deploy ``` Your application is now live and accessible at `..workers.dev`. You have successfully created a Cloudflare Worker that allows you to interact with an R2 bucket to accomplish tasks such as uploading and downloading files. You can now use this as a starting point for your own projects. ## Next steps To build more with R2 and Workers, refer to [Tutorials](https://developers.cloudflare.com/workers/tutorials/) and the [R2 documentation](https://developers.cloudflare.com/r2/). If you have any questions, need assistance, or would like to share your project, join the Cloudflare Developer community on [Discord ↗](https://discord.cloudflare.com) to connect with fellow developers and the Cloudflare team. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/upload-assets-with-r2/#page","headline":"Securely access and upload assets with Cloudflare R2 · Cloudflare Workers docs","description":"This tutorial explains how to create a TypeScript-based Cloudflare Workers project that can securely access files from and upload files to a CloudFlare R2 bucket.","url":"https://developers.cloudflare.com/workers/tutorials/upload-assets-with-r2/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-03-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["TypeScript"]} ``` --- --- description: This tutorial shows you how to set up a Cloudflare Workers project with Prisma ORM. title: Set up and use a Prisma Postgres database image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Set up and use a Prisma Postgres database Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/using-prisma-postgres-with-workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) [Prisma Postgres ↗](https://www.prisma.io/postgres) is a managed, serverless PostgreSQL database. It supports features like connection pooling, caching, real-time subscriptions, and query optimization recommendations. In this tutorial, you will learn how to: * Set up a Cloudflare Workers project with [Prisma ORM ↗](https://www.prisma.io/docs). * Create a Prisma Postgres instance from the Prisma CLI. * Model data and run migrations with Prisma Postgres. * Query the database from Workers. * Deploy the Worker to Cloudflare. ## Prerequisites To follow this guide, ensure you have the following: * Node.js `v18.18` or higher installed. * An active [Cloudflare account ↗](https://dash.cloudflare.com/). * A basic familiarity with installing and using command-line interface (CLI) applications. ## 1\. Create a new Worker project Begin by using [C3](https://developers.cloudflare.com/pages/get-started/c3/) to create a Worker project in the command line: ```sh npm create cloudflare@latest prisma-postgres-worker -- --type=hello-world --ts=true --git=true --deploy=false ``` Then navigate into your project: ```sh cd ./prisma-postgres-worker ``` Your initial `src/index.ts` file currently contains a simple request handler: ```ts export default { async fetch(request, env, ctx): Promise { return new Response("Hello World!"); }, } satisfies ExportedHandler; ``` ## 2\. Setup Prisma in your project In this step, you will set up Prisma ORM with a Prisma Postgres database using the CLI. Then you will create and execute helper scripts to create tables in the database and generate a Prisma client to query it. ### 2.1\. Install required dependencies Install Prisma CLI as a dev dependency: npmyarnpnpmbun ``` npm i -D prisma ``` ``` yarn add -D prisma ``` ``` pnpm add -D prisma ``` ``` bun add -d prisma ``` Install the [Prisma Accelerate client extension ↗](https://www.npmjs.com/package/@prisma/extension-accelerate) as it is required for Prisma Postgres: npmyarnpnpmbun ``` npm i @prisma/extension-accelerate ``` ``` yarn add @prisma/extension-accelerate ``` ``` pnpm add @prisma/extension-accelerate ``` ``` bun add @prisma/extension-accelerate ``` Install the [dotenv-cli package ↗](https://www.npmjs.com/package/dotenv-cli) to load environment variables from `.dev.vars`: npmyarnpnpmbun ``` npm i -D dotenv-cli ``` ``` yarn add -D dotenv-cli ``` ``` pnpm add -D dotenv-cli ``` ``` bun add -d dotenv-cli ``` ### 2.2\. Create a Prisma Postgres database and initialize Prisma Initialize Prisma in your application: npmyarnpnpm ``` npx prisma@latest init --db ``` ``` yarn dlx prisma@latest init --db ``` ``` pnpx prisma@latest init --db ``` If you do not have a [Prisma Data Platform ↗](https://console.prisma.io/) account yet, or if you are not logged in, the command will prompt you to log in using one of the available authentication providers. A browser window will open so you can log in or create an account. Return to the CLI after you have completed this step. Once logged in (or if you were already logged in), the CLI will prompt you to select a project name and a database region. Once the command has terminated, it will have created: * A project in your [Platform Console ↗](https://console.prisma.io/) containing a Prisma Postgres database instance. * A `prisma` folder containing `schema.prisma`, where you will define your database schema. * An `.env` file in the project root, which will contain the Prisma Postgres database url `DATABASE_URL=`. Note that Cloudflare Workers do not support `.env` files. You will use a file called `.dev.vars` instead of the `.env` file that was just created. ### 2.3\. Prepare environment variables Rename the `.env` file in the root of your application to `.dev.vars` file: ```sh mv .env .dev.vars ``` ### 2.4\. Apply database schema changes Open the `schema.prisma` file in the `prisma` folder and add the following `User` model to your database: ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String name String } ``` Next, add the following helper scripts to the `scripts` section of your `package.json`: ```json "scripts": { "migrate": "dotenv -e .dev.vars -- npx prisma migrate dev", "generate": "dotenv -e .dev.vars -- npx prisma generate --no-engine", "studio": "dotenv -e .dev.vars -- npx prisma studio", // Additional worker scripts... } ``` Run the migration script to apply changes to the database: ```sh npm run migrate ``` When prompted, provide a name for the migration (for example, `init`). After these steps are complete, Prisma ORM is fully set up and connected to your Prisma Postgres database. ## 3\. Develop the application Modify the `src/index.ts` file and replace its contents with the following code: ```ts import { PrismaClient } from "@prisma/client/edge"; import { withAccelerate } from "@prisma/extension-accelerate"; export interface Env { DATABASE_URL: string; } export default { async fetch(request, env, ctx): Promise { const path = new URL(request.url).pathname; if (path === "/favicon.ico") return new Response("Resource not found", { status: 404, headers: { "Content-Type": "text/plain", }, }); const prisma = new PrismaClient({ datasourceUrl: env.DATABASE_URL, }).$extends(withAccelerate()); const user = await prisma.user.create({ data: { email: `Jon${Math.ceil(Math.random() * 1000)}@gmail.com`, name: "Jon Doe", }, }); const userCount = await prisma.user.count(); return new Response(`\ Created new user: ${user.name} (${user.email}). Number of users in the database: ${userCount}. `); }, } satisfies ExportedHandler; ``` Run the development server: ```sh npm run dev ``` Visit [https://localhost:8787 ↗](https://localhost:8787) to see your app display the following output: ```sh Number of users in the database: 1 ``` Every time you refresh the page, a new user is created. The number displayed will increment by `1` with each refresh as it returns the total number of users in your database. ## 4\. Deploy the application to Cloudflare When the application is deployed to Cloudflare, it needs access to the `DATABASE_URL` environment variable that is defined locally in `.dev.vars`. You can use the [npx wrangler secret put](https://developers.cloudflare.com/workers/configuration/secrets/#adding-secrets-to-your-project) command to upload the `DATABASE_URL` to the deployment environment: ```sh npx wrangler secret put DATABASE_URL ``` When prompted, paste the `DATABASE_URL` value (from `.dev.vars`). If you are logged in via the Wrangler CLI, you will see a prompt asking if you'd like to create a new Worker. Confirm by choosing "yes": ```sh ✔ There doesn't seem to be a Worker called "prisma-postgres-worker". Do you want to create a new Worker with that name and add secrets to it? … yes ``` Then execute the following command to deploy your project to Cloudflare Workers: ```sh npm run deploy ``` The `wrangler` CLI will bundle and upload your application. If you are not already logged in, the `wrangler` CLI will open a browser window prompting you to log in to the Cloudflare dashboard. Note If you belong to multiple accounts, select the account where you want to deploy the project. Once the deployment completes, verify the deployment by visiting the live URL provided in the deployment output, such as `https://{PROJECT_NAME}.workers.dev`. If you encounter any issues, ensure the secrets were added correctly and check the deployment logs for errors. ## Next steps Congratulations on building and deploying a simple application with Prisma Postgres and Cloudflare Workers! To enhance your application further: * Add [caching ↗](https://www.prisma.io/docs/postgres/caching) to your queries. * Explore the [Prisma Postgres documentation ↗](https://www.prisma.io/docs/postgres/getting-started). To see how to build a real-time application with Cloudflare Workers and Prisma Postgres, read [this ↗](https://www.prisma.io/docs/guides/prisma-postgres-realtime-on-cloudflare) guide. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/using-prisma-postgres-with-workers/#page","headline":"Set up and use a Prisma Postgres database · Cloudflare Workers docs","description":"This tutorial shows you how to set up a Cloudflare Workers project with Prisma ORM.","url":"https://developers.cloudflare.com/workers/tutorials/using-prisma-postgres-with-workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["TypeScript","SQL","Prisma ORM","PostgreSQL"]} ``` --- --- description: This tutorial will teach you how to read and write to KV directly from Rust using workers-rs. You will use Workers KV from Rust to build an app to store and retrieve cities. title: Use Workers KV directly from Rust image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Use Workers KV directly from Rust Last updated Jan 29, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/tutorials/workers-kv-from-rust/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This tutorial will teach you how to read and write to KV directly from Rust using [workers-rs ↗](https://github.com/cloudflare/workers-rs). ## Before you start All of the tutorials assume you have already completed the [Get started guide](https://developers.cloudflare.com/workers/get-started/guide/), which gets you set up with a Cloudflare Workers account, [C3 ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare), and [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). ## Prerequisites To complete this tutorial, you will need: * [Git ↗](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). * [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI. * The [Rust ↗](https://www.rust-lang.org/tools/install) toolchain. * And `cargo-generate` sub-command by running: ```sh cargo install cargo-generate ``` ## 1\. Create your Worker project in Rust Open a terminal window, and run the following command to generate a Worker project template in Rust: ```sh cargo generate cloudflare/workers-rs ``` Then select `template/hello-world-http` template, give your project a descriptive name and select enter. A new project should be created in your directory. Open the project in your editor and run `npx wrangler dev` to compile and run your project. In this tutorial, you will use Workers KV from Rust to build an app to store and retrieve cities by a given country name. ## 2\. Create a KV namespace In the terminal, use Wrangler to create a KV namespace for `cities`. This generates a configuration to be added to the project: ```sh npx wrangler kv namespace create cities ``` To add this configuration to your project, open the Wrangler file and create an entry for `kv_namespaces` above the build command: ```jsonc { "kv_namespaces": [ { "binding": "cities", "id": "e29b263ab50e42ce9b637fa8370175e8" } ] } ``` ```toml [[kv_namespaces]] binding = "cities" id = "e29b263ab50e42ce9b637fa8370175e8" ``` With this configured, you can access the KV namespace with the binding `"cities"` from Rust. ## 3\. Write data to KV For this app, you will create two routes: A `POST` route to receive and store the city in KV, and a `GET` route to retrieve the city of a given country. For example, a `POST` request to `/France` with a body of `{"city": "Paris"}` should create an entry of Paris as a city in France. A `GET` request to `/France` should retrieve from KV and respond with Paris. Install [Serde ↗](https://serde.rs/) as a project dependency to handle JSON `cargo add serde`. Then create an app router and a struct for `Country` in `src/lib.rs`: ```rust use serde::{Deserialize, Serialize}; use worker::*; #[event(fetch)] async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { let router = Router::new(); #[derive(Serialize, Deserialize, Debug)] struct Country { city: String, } router // TODO: .post_async("/:country", |_, _| async move { Response::empty() }) // TODO: .get_async("/:country", |_, _| async move { Response::empty() }) .run(req, env) .await } ``` For the post handler, you will retrieve the country name from the path and the city name from the request body. Then, you will save this in KV with the country as key and the city as value. Finally, the app will respond with the city name: ```rust .post_async("/:country", |mut req, ctx| async move { let country = ctx.param("country").unwrap(); let city = match req.json::().await { Ok(c) => c.city, Err(_) => String::from(""), }; if city.is_empty() { return Response::error("Bad Request", 400); }; return match ctx.kv("cities")?.put(country, &city)?.execute().await { Ok(_) => Response::ok(city), Err(_) => Response::error("Bad Request", 400), }; }) ``` Save the file and make a `POST` request to test this endpoint: ```sh curl --json '{"city": "Paris"}' http://localhost:8787/France ``` ## 4\. Read data from KV To retrieve cities stored in KV, write a `GET` route that pulls the country name from the path and searches KV. You also need some error handling if the country is not found: ```rust .get_async("/:country", |_req, ctx| async move { if let Some(country) = ctx.param("country") { return match ctx.kv("cities")?.get(country).text().await? { Some(city) => Response::ok(city), None => Response::error("Country not found", 404), }; } Response::error("Bad Request", 400) }) ``` Save and make a curl request to test the endpoint: ```sh curl http://localhost:8787/France ``` ## 5\. Deploy your project The source code for the completed app should include the following: ```rust use serde::{Deserialize, Serialize}; use worker::*; #[event(fetch)] async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { let router = Router::new(); #[derive(Serialize, Deserialize, Debug)] struct Country { city: String, } router .post_async("/:country", |mut req, ctx| async move { let country = ctx.param("country").unwrap(); let city = match req.json::().await { Ok(c) => c.city, Err(_) => String::from(""), }; if city.is_empty() { return Response::error("Bad Request", 400); }; return match ctx.kv("cities")?.put(country, &city)?.execute().await { Ok(_) => Response::ok(city), Err(_) => Response::error("Bad Request", 400), }; }) .get_async("/:country", |_req, ctx| async move { if let Some(country) = ctx.param("country") { return match ctx.kv("cities")?.get(country).text().await? { Some(city) => Response::ok(city), None => Response::error("Country not found", 404), }; } Response::error("Bad Request", 400) }) .run(req, env) .await } ``` To deploy your Worker, run the following command: ```sh npx wrangler deploy ``` ## Related resources * [Rust support in Workers](https://developers.cloudflare.com/workers/languages/rust/). * [Using KV in Workers](https://developers.cloudflare.com/kv/get-started/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/workers-kv-from-rust/#page","headline":"Use Workers KV directly from Rust · Cloudflare Workers docs","description":"This tutorial will teach you how to read and write to KV directly from Rust using workers-rs. You will use Workers KV from Rust to build an app to store and retrieve cities.","url":"https://developers.cloudflare.com/workers/tutorials/workers-kv-from-rust/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-01-29","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Rust"]} ``` --- --- description: Explore demo applications and reference architectures built with Cloudflare Workers. title: Demos and architectures image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Demos and architectures Last updated May 19, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/demos/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Learn how you can use Workers within your existing application and architecture. ## Reference architectures Explore the following reference architectures that use Workers: [**Fullstack applications**A practical example of how these services come together in a real fullstack application architecture.](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/fullstack-application/) [**Storing user generated content**Store user-generated content in R2 for fast, secure, and cost-effective architecture.](https://developers.cloudflare.com/reference-architecture/diagrams/storage/storing-user-generated-content/) [**Optimizing and securing connected transportation systems**This diagram showcases Cloudflare components optimizing connected transportation systems. It illustrates how their technologies minimize latency, ensure reliability, and strengthen security for critical data flow.](https://developers.cloudflare.com/reference-architecture/diagrams/iot/optimizing-and-securing-connected-transportation-systems/) [**Ingesting BigQuery Data into Workers AI**You can connect a Cloudflare Worker to get data from Google BigQuery and pass it to Workers AI, to run AI Models, powered by serverless GPUs.](https://developers.cloudflare.com/reference-architecture/diagrams/ai/bigquery-workers-ai/) [**Event notifications for storage**Use Cloudflare Workers or an external service to monitor for notifications about data changes and then handle them appropriately.](https://developers.cloudflare.com/reference-architecture/diagrams/storage/event-notifications-for-storage/) [**Extend ZTNA with external authorization and serverless computing**Cloudflare's ZTNA enhances access policies using external API calls and Workers for robust security. It verifies user authentication and authorization, ensuring only legitimate access to protected resources.](https://developers.cloudflare.com/reference-architecture/diagrams/sase/augment-access-with-serverless/) [**Cloudflare Security Architecture**This document provides insight into how this network and platform are architected from a security perspective, how they are operated, and what services are available for businesses to address their own security challenges.](https://developers.cloudflare.com/reference-architecture/architectures/security/) [**Composable AI architecture**The architecture diagram illustrates how AI applications can be built end-to-end on Cloudflare, or single services can be integrated with external infrastructure and services.](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-composable/) [**A/B-testing using Workers**Cloudflare's low-latency, fully serverless compute platform, Workers offers powerful capabilities to enable A/B testing using a server-side implementation.](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/a-b-testing-using-workers/) [**Serverless global APIs**An example architecture of a serverless API on Cloudflare and aims to illustrate how different compute and data products could interact with each other.](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/serverless-global-apis/) [**Serverless ETL pipelines**Cloudflare enables fully serverless ETL pipelines, significantly reducing complexity, accelerating time to production, and lowering overall costs.](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/serverless-etl/) [**Egress-free object storage in multi-cloud setups**Learn how to use R2 to get egress-free object storage in multi-cloud setups.](https://developers.cloudflare.com/reference-architecture/diagrams/storage/egress-free-storage-multi-cloud/) [**Retrieval Augmented Generation (RAG)**RAG combines retrieval with generative models for better text. It uses external knowledge to create factual, relevant responses, improving coherence and accuracy in NLP tasks like chatbots.](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-rag/) [**Automatic captioning for video uploads**By integrating automatic speech recognition technology into video platforms, content creators, publishers, and distributors can reach a broader audience, including individuals with hearing impairments or those who prefer to consume content in different languages.](https://developers.cloudflare.com/reference-architecture/diagrams/ai/ai-video-caption/) [**Serverless image content management**Leverage various components of Cloudflare's ecosystem to construct a scalable image management solution](https://developers.cloudflare.com/reference-architecture/diagrams/serverless/serverless-image-content-management/) [**Enterprise AI agent workspace**Reference architecture for building governed, stateful enterprise AI agent workspaces on Cloudflare.](https://developers.cloudflare.com/reference-architecture/diagrams/ai/enterprise-ai-agent-workspace/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/demos/#page","headline":"Demos and architectures · Cloudflare Workers docs","description":"Explore demo applications and reference architectures built with Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/demos/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-19","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Build and deploy an interactive ChatGPT App on Cloudflare Workers with real-time multiplayer state using MCP. title: Build an Interactive ChatGPT App image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build an Interactive ChatGPT App Last updated Jun 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/demos/chatgpt-app/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Deploy your first ChatGPT App This guide will show you how to build and deploy an interactive ChatGPT App on Cloudflare Workers that can: * Render rich, interactive UI widgets directly in ChatGPT conversations * Maintain real-time, multi-user state using Durable Objects * Enable bidirectional communication between your app and ChatGPT * Build multiplayer experiences that run entirely within ChatGPT You will build a real-time multiplayer chess game that demonstrates these capabilities. Players can start or join games, make moves on an interactive chessboard, and even ask ChatGPT for strategic advice—all without leaving the conversation. Your ChatGPT App will use the **Model Context Protocol (MCP)** to expose tools and UI resources that ChatGPT can invoke on your behalf. You can view the full code for this example [here ↗](https://github.com/cloudflare/agents/tree/main/openai-sdk/chess-app). ## Prerequisites Before you begin, you will need: * A [Cloudflare account ↗](https://dash.cloudflare.com/sign-up) * [Node.js ↗](https://nodejs.org/) installed (v18 or later) * A [ChatGPT Plus or Team account ↗](https://chat.openai.com/) with developer mode enabled * Basic knowledge of React and TypeScript ## 1\. Enable ChatGPT Developer Mode To use ChatGPT Apps (also called connectors), you need to enable developer mode: 1. Open [ChatGPT ↗](https://chat.openai.com/). 2. Go to **Settings** \> **Apps & Connectors** \> **Advanced Settings** 3. Toggle **Developer mode ON** Once enabled, you will be able to install custom apps during development and testing. ## 2\. Create your ChatGPT App project 1. Create a new project for your Chess App: npmyarnpnpm ``` npm create cloudflare@latest -- my-chess-app ``` ``` yarn create cloudflare my-chess-app ``` ``` pnpm create cloudflare@latest my-chess-app ``` 1. Navigate into your project: ```sh cd my-chess-app ``` 1. Install the required dependencies: ```sh npm install agents @modelcontextprotocol/sdk chess.js react react-dom react-chessboard ``` 1. Install development dependencies: ```sh npm install -D @cloudflare/vite-plugin @vitejs/plugin-react vite vite-plugin-singlefile @types/react @types/react-dom ``` ## 3\. Configure your project 1. Update your `wrangler.jsonc` to configure Durable Objects and assets: ```jsonc { "name": "my-chess-app", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ { "name": "CHESS", "class_name": "ChessGame", }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["ChessGame"], }, ], "assets": { "directory": "dist", "binding": "ASSETS", }, } ``` ```toml name = "my-chess-app" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" compatibility_flags = [ "nodejs_compat" ] [[durable_objects.bindings]] name = "CHESS" class_name = "ChessGame" [[migrations]] tag = "v1" new_sqlite_classes = [ "ChessGame" ] [assets] directory = "dist" binding = "ASSETS" ``` 1. Create a `vite.config.ts` for building your React UI: ```ts import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import { viteSingleFile } from "vite-plugin-singlefile"; export default defineConfig({ plugins: [react(), cloudflare(), viteSingleFile()], build: { minify: false, }, }); ``` 1. Update your `package.json` scripts: ```json { "scripts": { "dev": "vite", "build": "vite build", "deploy": "vite build && wrangler deploy" } } ``` ## 4\. Create the Chess game engine 1. Create the game logic using Durable Objects at `src/chess.tsx`: ```tsx import { Agent, callable, getCurrentAgent } from "agents"; import { Chess } from "chess.js"; type Color = "w" | "b"; type ConnectionState = { playerId: string; }; export type State = { board: string; players: { w?: string; b?: string }; status: "waiting" | "active" | "mate" | "draw" | "resigned"; winner?: Color; lastSan?: string; }; export class ChessGame extends Agent { initialState: State = { board: new Chess().fen(), players: {}, status: "waiting", }; game = new Chess(); constructor( ctx: DurableObjectState, public env: Env, ) { super(ctx, env); this.game.load(this.state.board); } private colorOf(playerId: string): Color | undefined { const { players } = this.state; if (players.w === playerId) return "w"; if (players.b === playerId) return "b"; return undefined; } @callable() join(params: { playerId: string; preferred?: Color | "any" }) { const { playerId, preferred = "any" } = params; const { connection } = getCurrentAgent(); if (!connection) throw new Error("Not connected"); connection.setState({ playerId }); const s = this.state; // Already seated? Return seat const already = this.colorOf(playerId); if (already) { return { ok: true, role: already as Color, state: s }; } // Choose a seat const free: Color[] = (["w", "b"] as const).filter((c) => !s.players[c]); if (free.length === 0) { return { ok: true, role: "spectator" as const, state: s }; } let seat: Color = free[0]; if (preferred === "w" && free.includes("w")) seat = "w"; if (preferred === "b" && free.includes("b")) seat = "b"; s.players[seat] = playerId; s.status = s.players.w && s.players.b ? "active" : "waiting"; this.setState(s); return { ok: true, role: seat, state: s }; } @callable() move( move: { from: string; to: string; promotion?: string }, expectedFen?: string, ) { if (this.state.status === "waiting") { return { ok: false, reason: "not-in-game", fen: this.game.fen(), status: this.state.status, }; } const { connection } = getCurrentAgent(); if (!connection) throw new Error("Not connected"); const { playerId } = connection.state as ConnectionState; const seat = this.colorOf(playerId); if (!seat) { return { ok: false, reason: "not-in-game", fen: this.game.fen(), status: this.state.status, }; } if (seat !== this.game.turn()) { return { ok: false, reason: "not-your-turn", fen: this.game.fen(), status: this.state.status, }; } // Optimistic sync guard if (expectedFen && expectedFen !== this.game.fen()) { return { ok: false, reason: "stale", fen: this.game.fen(), status: this.state.status, }; } const res = this.game.move(move); if (!res) { return { ok: false, reason: "illegal", fen: this.game.fen(), status: this.state.status, }; } const fen = this.game.fen(); let status: State["status"] = "active"; if (this.game.isCheckmate()) status = "mate"; else if (this.game.isDraw()) status = "draw"; this.setState({ ...this.state, board: fen, lastSan: res.san, status, winner: status === "mate" ? (this.game.turn() === "w" ? "b" : "w") : undefined, }); return { ok: true, fen, san: res.san, status }; } @callable() resign() { const { connection } = getCurrentAgent(); if (!connection) throw new Error("Not connected"); const { playerId } = connection.state as ConnectionState; const seat = this.colorOf(playerId); if (!seat) return { ok: false, reason: "not-in-game", state: this.state }; const winner = seat === "w" ? "b" : "w"; this.setState({ ...this.state, status: "resigned", winner }); return { ok: true, state: this.state }; } } ``` ## 5\. Create the MCP server and UI resource 1. Create your main worker at `src/index.ts`: ```ts import { createMcpHandler } from "agents/mcp"; import { routeAgentRequest } from "agents"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { env } from "cloudflare:workers"; const getWidgetHtml = async (host: string) => { let html = await (await env.ASSETS.fetch("http://localhost/")).text(); html = html.replace( "", ``, ); return html; }; function createServer() { const server = new McpServer({ name: "Chess", version: "v1.0.0" }); // Register a UI resource that ChatGPT can render server.registerResource( "chess", "ui://widget/index.html", {}, async (_uri, extra) => { return { contents: [ { uri: "ui://widget/index.html", mimeType: "text/html+skybridge", text: await getWidgetHtml( extra.requestInfo?.headers.host as string, ), }, ], }; }, ); // Register a tool that ChatGPT can call to render the UI server.registerTool( "playChess", { title: "Renders a chess game menu, ready to start or join a game.", annotations: { readOnlyHint: true }, _meta: { "openai/outputTemplate": "ui://widget/index.html", "openai/toolInvocation/invoking": "Opening chess widget", "openai/toolInvocation/invoked": "Chess widget opened", }, }, async (_, _extra) => { return { content: [ { type: "text", text: "Successfully rendered chess game menu" }, ], }; }, ); return server; } export default { async fetch(req: Request, env: Env, ctx: ExecutionContext) { const url = new URL(req.url); if (url.pathname.startsWith("/mcp")) { // Create a new server instance per request const server = createServer(); return createMcpHandler(server)(req, env, ctx); } return ( (await routeAgentRequest(req, env)) ?? new Response("Not found", { status: 404 }) ); }, } satisfies ExportedHandler; export { ChessGame } from "./chess"; ``` ## 6\. Build the React UI 1. Create the HTML entry point at `index.html`: ```html
``` 1. Create the React app at `src/app.tsx`: ```tsx import { useEffect, useRef, useState } from "react"; import { useAgent } from "agents/react"; import { createRoot } from "react-dom/client"; import { Chess, type Square } from "chess.js"; import { Chessboard, type PieceDropHandlerArgs } from "react-chessboard"; import type { State as ServerState } from "./chess"; function usePlayerId() { const [pid] = useState(() => { const existing = localStorage.getItem("playerId"); if (existing) return existing; const id = crypto.randomUUID(); localStorage.setItem("playerId", id); return id; }); return pid; } function App() { const playerId = usePlayerId(); const [gameId, setGameId] = useState(null); const [gameIdInput, setGameIdInput] = useState(""); const [menuError, setMenuError] = useState(null); const gameRef = useRef(new Chess()); const [fen, setFen] = useState(gameRef.current.fen()); const [myColor, setMyColor] = useState<"w" | "b" | "spectator">("spectator"); const [pending, setPending] = useState(false); const [serverState, setServerState] = useState(null); const [joined, setJoined] = useState(false); const host = window.HOST ?? "http://localhost:5173/"; const { stub } = useAgent({ host, name: gameId ?? "__lobby__", agent: "chess", onStateUpdate: (s) => { if (!gameId) return; gameRef.current.load(s.board); setFen(s.board); setServerState(s); }, }); useEffect(() => { if (!gameId || joined) return; (async () => { try { const res = await stub.join({ playerId, preferred: "any" }); if (!res?.ok) return; setMyColor(res.role); gameRef.current.load(res.state.board); setFen(res.state.board); setServerState(res.state); setJoined(true); } catch (error) { console.error("Failed to join game", error); } })(); }, [playerId, gameId, stub, joined]); async function handleStartNewGame() { const newId = crypto.randomUUID(); setGameId(newId); setGameIdInput(newId); setMenuError(null); setJoined(false); } async function handleJoinGame() { const trimmed = gameIdInput.trim(); if (!trimmed) { setMenuError("Enter a game ID to join."); return; } setGameId(trimmed); setMenuError(null); setJoined(false); } const handleHelpClick = () => { window.openai?.sendFollowUpMessage?.({ prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice.`, }); }; function onPieceDrop({ sourceSquare, targetSquare }: PieceDropHandlerArgs) { if (!gameId || !sourceSquare || !targetSquare || pending) return false; const game = gameRef.current; if (myColor === "spectator" || game.turn() !== myColor) return false; const piece = game.get(sourceSquare as Square); if (!piece || piece.color !== myColor) return false; const prevFen = game.fen(); try { const local = game.move({ from: sourceSquare, to: targetSquare, promotion: "q", }); if (!local) return false; } catch { return false; } const nextFen = game.fen(); setFen(nextFen); setPending(true); stub .move({ from: sourceSquare, to: targetSquare, promotion: "q" }, prevFen) .then((r) => { if (!r.ok) { game.load(r.fen); setFen(r.fen); } }) .finally(() => setPending(false)); return true; } return (
{!gameId ? (

Ready to play?

Start a new match or join an existing game.

setGameIdInput(e.target.value)} style={{ width: "100%", padding: "10px", borderRadius: "8px", border: "1px solid #ccc", }} /> {menuError && (

{menuError}

)}
) : (

Game {gameId}

Status: {serverState?.status}

)}
); } const root = createRoot(document.getElementById("root")!); root.render(); ``` Note This is a simplified version of the UI. For the complete implementation with player slots, better styling, and game state management, check out the [full example on GitHub ↗](https://github.com/cloudflare/agents/tree/main/openai-sdk/chess-app/src/app.tsx). ## 7\. Build and deploy 1. Build your React UI: ```sh npm run build ``` This compiles your React app into a single HTML file in the `dist` directory. 1. Deploy to Cloudflare: ```sh npx wrangler deploy ``` After deployment, you will see your app URL: ```plaintext https://my-chess-app.YOUR_SUBDOMAIN.workers.dev ``` ## 8\. Connect to ChatGPT Now connect your deployed app to ChatGPT: 1. Open [ChatGPT ↗](https://chat.openai.com/). 2. Go to **Settings** \> **Apps & Connectors** \> **Create** 3. Give your app a **name**, and optionally a **description** and **icon**. 4. Enter your MCP endpoint: `https://my-chess-app.YOUR_SUBDOMAIN.workers.dev/mcp`. 5. Select **"No authentication"**. 6. Select **"Create"**. ## 9\. Play chess in ChatGPT Try it out: 1. In your ChatGPT conversation, type: "Let's play chess". 2. ChatGPT will call the `playChess` tool and render your interactive chess widget. 3. Select **"Start a new game"** to create a game. 4. Share the game ID with a friend who can join via their own ChatGPT conversation. 5. Make moves by dragging pieces on the board. 6. Select **"Ask for help"** to get strategic advice from ChatGPT Note You might need to manually select the connector in the prompt box the first time you use it. Select **"+"** \> **"More"** \> **\[App name\]**. ## Key concepts ### MCP Server The Model Context Protocol (MCP) server defines tools and resources that ChatGPT can access. Note that we create a new server instance per request to prevent cross-client response leakage: ```ts function createServer() { const server = new McpServer({ name: "Chess", version: "v1.0.0" }); // Register a UI resource that ChatGPT can render server.registerResource( "chess", "ui://widget/index.html", {}, async (_uri, extra) => { return { contents: [ { uri: "ui://widget/index.html", mimeType: "text/html+skybridge", text: await getWidgetHtml( extra.requestInfo?.headers.host as string, ), }, ], }; }, ); // Register a tool that ChatGPT can call to render the UI server.registerTool( "playChess", { title: "Renders a chess game menu, ready to start or join a game.", annotations: { readOnlyHint: true }, _meta: { "openai/outputTemplate": "ui://widget/index.html", "openai/toolInvocation/invoking": "Opening chess widget", "openai/toolInvocation/invoked": "Chess widget opened", }, }, async (_, _extra) => { return { content: [ { type: "text", text: "Successfully rendered chess game menu" }, ], }; }, ); return server; } ``` ### Game Engine with Agents The `ChessGame` class extends `Agent` to create a stateful game engine: ```tsx export class ChessGame extends Agent { initialState: State = { board: new Chess().fen(), players: {}, status: "waiting" }; game = new Chess(); constructor( ctx: DurableObjectState, public env: Env ) { super(ctx, env); this.game.load(this.state.board); } ``` Each game gets its own Agent instance, enabling: * **Isolated state** per game * **Real-time synchronization** across players * **Persistent storage** that survives worker restarts ### Callable methods Use the `@callable()` decorator to expose methods that clients can invoke: ```ts @callable() join(params: { playerId: string; preferred?: Color | "any" }) { const { playerId, preferred = "any" } = params; const { connection } = getCurrentAgent(); if (!connection) throw new Error("Not connected"); connection.setState({ playerId }); const s = this.state; // Already seated? Return seat const already = this.colorOf(playerId); if (already) { return { ok: true, role: already as Color, state: s }; } // Choose a seat const free: Color[] = (["w", "b"] as const).filter((c) => !s.players[c]); if (free.length === 0) { return { ok: true, role: "spectator" as const, state: s }; } let seat: Color = free[0]; if (preferred === "w" && free.includes("w")) seat = "w"; if (preferred === "b" && free.includes("b")) seat = "b"; s.players[seat] = playerId; s.status = s.players.w && s.players.b ? "active" : "waiting"; this.setState(s); return { ok: true, role: seat, state: s }; } ``` ### React integration The `useAgent` hook connects your React app to the Durable Object: ```tsx const { stub } = useAgent({ host, name: gameId ?? "__lobby__", agent: "chess", onStateUpdate: (s) => { gameRef.current.load(s.board); setFen(s.board); setServerState(s); }, }); ``` Call methods on the agent: ```tsx const res = await stub.join({ playerId, preferred: "any" }); await stub.move({ from: "e2", to: "e4" }); ``` ### Bidirectional communication Your app can send messages to ChatGPT: ```ts const handleHelpClick = () => { window.openai?.sendFollowUpMessage?.({ prompt: `Help me with my chess game. I am playing as ${myColor} and the board is: ${fen}. Please only offer written advice as there are no tools for you to use.`, }); }; ``` This creates a new message in the ChatGPT conversation with context about the current game state. ## Next steps Now that you have a working ChatGPT App, you can: * Add more tools: Expose additional capabilities and UIs through MCP tools and resources. * Enhance the UI: Build more sophisticated interfaces with React. ## Related resources ### [Agents API](https://developers.cloudflare.com/agents/runtime/agents-api/) Complete API reference for the Agents SDK. ### [Durable Objects](https://developers.cloudflare.com/durable-objects/) Learn about the underlying stateful infrastructure. ### [Model Context Protocol](https://modelcontextprotocol.io/) MCP specification and documentation. ### [OpenAI Apps SDK](https://developers.openai.com/apps-sdk/) Official OpenAI Apps SDK reference. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/demos/chatgpt-app/#page","headline":"Build an Interactive ChatGPT App · Cloudflare Workers docs","description":"Build and deploy an interactive ChatGPT App on Cloudflare Workers with real-time multiplayer state using MCP.","url":"https://developers.cloudflare.com/workers/demos/chatgpt-app/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Develop and test your Workers locally. title: Local development image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Local development Last updated Jun 25, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can build, run, and test your Worker code on your own local machine before deploying it to Cloudflare's network. This is made possible through [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/), a simulator that executes your Worker code using the same runtime used in production, [workerd ↗](https://github.com/cloudflare/workerd). [By default](https://developers.cloudflare.com/workers/local-development/#defaults), your Worker's bindings [connect to locally simulated resources](https://developers.cloudflare.com/workers/local-development/#bindings-during-local-development), but can be configured to interact with the real, production resource with [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings). ## Core concepts ### Worker execution vs Bindings When developing Workers, it's important to understand two distinct concepts: * **Worker execution**: Where your Worker code actually runs (on your local machine vs on Cloudflare's infrastructure). * [**Bindings**](https://developers.cloudflare.com/workers/runtime-apis/bindings/): How your Worker interacts with Cloudflare resources (like [KV namespaces](https://developers.cloudflare.com/kv), [R2 buckets](https://developers.cloudflare.com/r2), [D1 databases](https://developers.cloudflare.com/d1), [Queues](https://developers.cloudflare.com/queues/), [Durable Objects](https://developers.cloudflare.com/durable-objects/), etc). In your Worker code, these are accessed via the `env` object (such as `env.MY_KV`). ## Start a local development server You can start a local development server using: 1. The Cloudflare Workers CLI [**Wrangler**](https://developers.cloudflare.com/workers/wrangler/), using the built-in [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) command. npmyarnpnpm ``` npx wrangler dev ``` ``` yarn wrangler dev ``` ``` pnpm wrangler dev ``` 1. [**Vite** ↗](https://vite.dev/), using the [**Cloudflare Vite plugin**](https://developers.cloudflare.com/workers/vite-plugin/). npmyarnpnpm ``` npx vite dev ``` ``` yarn vite dev ``` ``` pnpm vite dev ``` Both Wrangler and the Cloudflare Vite plugin use [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/) under the hood, and are developed and maintained by the Cloudflare team. For guidance on choosing when to use Wrangler versus Vite, see our guide [Choosing between Wrangler & Vite](https://developers.cloudflare.com/workers/local-development/wrangler-vs-vite/). * [Get started with Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) * [Get started with the Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/get-started/) ### Defaults By default, running `wrangler dev` / `vite dev` (when using the [Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/get-started/)) means that: * Your Worker code runs on your local machine. * All resources your Worker is bound to in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) are simulated locally. * The local `workerd` runtime runs with `TZ=UTC` so that `Date` and `Intl` APIs inside your Worker observe UTC, matching the production Cloudflare runtime regardless of your machine's timezone. ### Bindings during local development [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) are interfaces that allow your Worker to interact with various Cloudflare resources (like [KV namespaces](https://developers.cloudflare.com/kv), [R2 buckets](https://developers.cloudflare.com/r2), [D1 databases](https://developers.cloudflare.com/d1), [Queues](https://developers.cloudflare.com/queues/), [Durable Objects](https://developers.cloudflare.com/durable-objects/), etc). In your Worker code, these are accessed via the `env` object (such as `env.MY_KV`). During local development, your Worker code interacts with these bindings using the exact same API calls (such as `env.MY_KV.put()`) as it would in a deployed environment. These local resources are initially empty, but you can populate them with data, as documented in [Adding local data](https://developers.cloudflare.com/workers/local-development/local-data/). * By default, bindings connect to **local resource simulations** (except for [AI bindings](https://developers.cloudflare.com/workers-ai/configuration/bindings/), as AI models always run remotely). * You can override this default behavior and **connect to the remote resource** on a per-binding basis with [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings). This lets you connect to real, production resources while still running your Worker code locally. * When using `wrangler dev`, you can temporarily disable all [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings) (and connect only to local resources) by providing the `--local` flag (i.e. `wrangler dev --local`) ## Remote bindings **Remote bindings** are bindings that are configured to connect to the deployed, remote resource during local development _instead_ of the locally simulated resource. Remote bindings are supported by [**Wrangler**](https://developers.cloudflare.com/workers/wrangler/), the [**Cloudflare Vite plugin**](https://developers.cloudflare.com/workers/vite-plugin/), and the `@cloudflare/vitest-pool-workers` package. You can configure remote bindings by setting `remote: true` in the binding definition. ### Example configuration ```jsonc { "name": "my-worker", // Set this to today's date "compatibility_date": "2026-08-14", "r2_buckets": [ { "bucket_name": "screenshots-bucket", "binding": "screenshots_bucket", "remote": true, }, ], } ``` ```toml name = "my-worker" # Set this to today's date compatibility_date = "2026-08-14" [[r2_buckets]] bucket_name = "screenshots-bucket" binding = "screenshots_bucket" remote = true ``` When remote bindings are configured, your Worker still **executes locally**, only the underlying resources your bindings connect to change. For all bindings marked with `remote: true`, Miniflare will route its operations (such as `env.MY_KV.put()`) to the deployed resource. All other bindings not explicitly configured with `remote: true` continue to use their default local simulations. ### Integration with environments Remote Bindings work well together with [Workers Environments](https://developers.cloudflare.com/workers/wrangler/environments). To protect production data, you can create a development or staging environment and specify different resources in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) than you would use for production. **For example:** ```jsonc { "name": "my-worker", // Set this to today's date "compatibility_date": "2026-08-14", "env": { "production": { "r2_buckets": [ { "bucket_name": "screenshots-bucket", "binding": "screenshots_bucket", }, ], }, "staging": { "r2_buckets": [ { "bucket_name": "preview-screenshots-bucket", "binding": "screenshots_bucket", "remote": true, }, ], }, }, } ``` ```toml name = "my-worker" # Set this to today's date compatibility_date = "2026-08-14" [[env.production.r2_buckets]] bucket_name = "screenshots-bucket" binding = "screenshots_bucket" [[env.staging.r2_buckets]] bucket_name = "preview-screenshots-bucket" binding = "screenshots_bucket" remote = true ``` Running `wrangler dev -e staging` (or `CLOUDFLARE_ENV=staging vite dev`) with the above configuration means that: * Your Worker code runs locally * All calls made to `env.screenshots_bucket` will use the `preview-screenshots-bucket` resource, rather than the production `screenshots-bucket`. ### Recommended remote bindings We recommend configuring specific bindings to connect to their remote counterparts. These services often rely on Cloudflare's network infrastructure or have complex backends that are not fully simulated locally. The following bindings are recommended to have `remote: true` in your Wrangler configuration: #### [Browser Run](https://developers.cloudflare.com/workers/wrangler/configuration/#browser-run): To interact with a real headless browser for rendering. There is no current local simulation for Browser Run. ```jsonc { "browser": { "binding": "MY_BROWSER", "remote": true }, } ``` ```toml [browser] binding = "MY_BROWSER" remote = true ``` #### [Workers AI](https://developers.cloudflare.com/workers/wrangler/configuration/#workers-ai): To utilize actual AI models deployed on Cloudflare's network for inference. There is no current local simulation for Workers AI. ```jsonc { "ai": { "binding": "AI", "remote": true }, } ``` ```toml [ai] binding = "AI" remote = true ``` #### [Vectorize](https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes): To connect to your production Vectorize indexes for accurate vector search and similarity operations. There is no current local simulation for Vectorize. ```jsonc { "vectorize": [ { "binding": "MY_VECTORIZE_INDEX", "index_name": "my-prod-index", "remote": true } ], } ``` ```toml [[vectorize]] binding = "MY_VECTORIZE_INDEX" index_name = "my-prod-index" remote = true ``` #### [mTLS](https://developers.cloudflare.com/workers/wrangler/configuration/#mtls-certificates): To verify that the certificate exchange and validation process work as expected. There is no current local simulation for mTLS bindings. ```jsonc { "mtls_certificates": [ { "binding": "MY_CLIENT_CERT_FETCHER", "certificate_id": "", "remote": true } ] } ``` ```toml [[mtls_certificates]] binding = "MY_CLIENT_CERT_FETCHER" certificate_id = "" remote = true ``` #### [Images](https://developers.cloudflare.com/workers/wrangler/configuration/#images): To connect to a high-fidelity version of the Images API, and verify that all transformations work as expected. Local simulation for Cloudflare Images is [limited with only a subset of features](https://developers.cloudflare.com/images/optimization/binding/#interact-with-your-images-binding-locally). ```jsonc { "images": { "binding": "IMAGES" , "remote": true } } ``` ```toml [images] binding = "IMAGES" remote = true ``` Note If `remote: true` is not specified for Browser Run, Vectorize, mTLS, or Images, Cloudflare **will issue a warning**. This prompts you to consider enabling it for a more production-like testing experience. If a Workers AI binding has `remote` set to `false`, Cloudflare will **produce an error**. If the property is omitted, Cloudflare will connect to the remote resource and issue a warning to add the property to configuration. #### [Dispatch Namespaces](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/reference/local-development/): Workers for Platforms users can configure `remote: true` in dispatch namespace binding definitions: ```jsonc { "dispatch_namespaces": [ { "binding": "DISPATCH_NAMESPACE", "namespace": "testing", "remote":true } ] } ``` ```toml [[dispatch_namespaces]] binding = "DISPATCH_NAMESPACE" namespace = "testing" remote = true ``` This allows you to run your [dynamic dispatch Worker](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/how-workers-for-platforms-works/#dynamic-dispatch-worker) locally, while connecting it to your remote dispatch namespace binding. This allows you to test changes to your core dispatching logic against real, deployed [user Workers](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/how-workers-for-platforms-works/#user-workers). ### Unsupported remote bindings Certain bindings are not supported for remote connections (i.e. with `remote: true`) during local development. These will always use local simulations or local values. If `remote: true` is specified in Wrangler configuration for any of the following unsupported binding types, Cloudflare **will issue an error**. See [all supported and unsupported bindings for remote bindings](https://developers.cloudflare.com/workers/local-development/bindings-per-env/). * [**Durable Objects**](https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects): Enabling remote connections for Durable Objects may be supported in the future, but currently will always run locally. However, using Durable Objects in combination with remote bindings is possible. Refer to [Using remote resources with Durable Objects and Workflows](#using-remote-resources-with-durable-objects-and-workflows) below. * [**Workflows**](https://developers.cloudflare.com/workflows/): Enabling remote connections for Workflows may be supported in the future, but currently will only run locally. However, using Workflows in combination with remote bindings is possible. Refer to [Using remote resources with Durable Objects and Workflows](#using-remote-resources-with-durable-objects-and-workflows) below. * [**Environment Variables (vars)**](https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables): Environment variables are intended to be distinct between local development and deployed environments. They are easily configurable locally (such as in a `.dev.vars` file or directly in Wrangler configuration). * [**Secrets**](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets): Like environment variables, secrets are expected to have different values in local development versus deployed environments for security reasons. Use `.dev.vars` for local secret management. * [**Static Assets**](https://developers.cloudflare.com/workers/wrangler/configuration/#assets) Static assets are always served from your local disk during development for speed and direct feedback on changes. * [**Version Metadata**](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/): Since your Worker code is running locally, version metadata (like commit hash, version tags) associated with a specific deployed version is not applicable or accurate. * [**Analytics Engine**](https://developers.cloudflare.com/analytics/analytics-engine/): Local development sessions typically don't contribute data directly to production Analytics Engine. * [**Hyperdrive**](https://developers.cloudflare.com/workers/wrangler/configuration/#hyperdrive): This is being actively worked on, but is currently unsupported. * [**Rate Limiting**](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/#configuration): Local development sessions typically should not share or affect rate limits of your deployed Workers. Rate limiting logic should be tested against local simulations. Note If you have use-cases for connecting to any of the remote resources above, please [open a feature request ↗](https://github.com/cloudflare/workers-sdk/issues) in our [workers-sdk repository ↗](https://github.com/cloudflare/workers-sdk). #### Using remote resources with Durable Objects and Workflows While Durable Object and Workflow bindings cannot currently be remote, you can still use them during local development and have them interact with remote resources. There are two recommended patterns for this: * **Local Durable Objects/Workflows with remote bindings:** When you enable remote bindings in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration), your locally running Durable Objects and Workflows can access remote resources. This allows such bindings, although run locally, to interact with remote resources during local development. * **Accessing remote Durable Objects/Workflows via service bindings:** To interact with remote Durable Object or Workflow instances, deploy a Worker that defines those. Then, in your local Worker, configure a remote [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) pointing to the deployed Worker. Your local Worker will be then able to interact with the remote deployed Worker, which in turn can communicate with the remote Durable Objects/Workflows. Using this method, you can create a communication channel via the remote service binding, effectively using the deployed Worker as a proxy interface to the remote bindings during local development. ### Important Considerations * **Cloudflare Access**: If your Worker is protected by [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/), Wrangler must authenticate with Access when connecting to remote bindings. Refer to [Connect to Access-protected Workers](#connect-to-access-protected-workers). * **Data modification**: Operations (writes, deletes, updates) on bindings connected remotely will affect your actual data in the targeted Cloudflare resource (be it preview or production). * **Billing**: Interactions with remote Cloudflare services through these connections will incur standard operational costs for those services (such as KV operations, R2 storage/operations, AI requests, D1 usage). * **Network latency**: Expect network latency for operations on these remotely connected bindings, as they involve communication over the internet. ### Connect to Access-protected Workers If your Worker is deployed behind a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/) application — for example, if the `*.workers.dev` subdomain on your account is protected by Access, or if you have placed an Access policy on the custom route of the Worker — Wrangler must authenticate with Access when connecting to your remote bindings. There are two ways you can authenticate against Access: * **Interactive login** (local development): If you have a policy defined that accepts user login, then Wrangler launches the interactive `cloudflared access login` flow in your browser. No additional setup is required beyond being signed in to the correct account. If the policy only allows service token authentication, Wrangler will skip the interactive flow and throw an error indicating that service token credentials are required. * **Service token** (CI / non-interactive environments): In CI/CD pipelines and other non-interactive contexts, or where the policy only allows service token authentication, Wrangler cannot trigger the interactive flow via the browser. Authentication must be via a [Cloudflare Access service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/) instead. If you do not configure a service token in a non-interactive environment, Wrangler will throw an error rather than attempting the interactive flow. To set up service token authentication: 1. **Create a service token.** In the Cloudflare dashboard, go to **Zero Trust** \> **Access** \> **Service Auth** \> **Service Tokens** and create a new token. Refer to [Service tokens](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/) for the full reference. You will be shown a Client ID and a Client Secret — save them somewhere safe, as the secret is not shown again. 2. **Add a Service Auth policy to the Access application that protects your Worker.** Open the _existing_ Access application that already covers the hostname of the Worker — typically the wildcard application for `*..workers.dev`, or the application that protects your custom domain — and attach a new policy with: * **Action**: Service Auth * **Include**: The service token you created, or "Any Access Service Token" if you want to allow any service token to access the Worker. Caution Do not create a _separate_ Access application scoped only to the Worker's hostname. Doing so has been observed to block requests even when the existing wildcard application is left in place — refer to [opennextjs-cloudflare#1171 ↗](https://github.com/opennextjs/opennextjs-cloudflare/issues/1171). Attach the Service Auth policy to the existing application that already protects the hostname. 3. **Expose the credentials to Wrangler.** Set the `CLOUDFLARE_ACCESS_CLIENT_ID` and `CLOUDFLARE_ACCESS_CLIENT_SECRET` [system environment variables](https://developers.cloudflare.com/workers/wrangler/system-environment-variables/) in the environment that runs Wrangler: ```sh export CLOUDFLARE_ACCESS_CLIENT_ID= export CLOUDFLARE_ACCESS_CLIENT_SECRET= ``` In CI, store the values as secrets and expose them as environment variables to the step that runs Wrangler. ### API Wrangler provides programmatic utilities to help tooling authors support remote binding connections when running Workers code with [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/). **Key APIs include:** * [startRemoteProxySession](#startRemoteProxySession): Starts a proxy session that allows interaction with remote bindings. * [unstable\_convertConfigBindingsToStartWorkerBindings](#unstable%5Fconvertconfigbindingstostartworkerbindings): Utility for converting binding definitions. * [experimental\_maybeStartOrUpdateProxySession](#experimental%5Fmaybestartorupdatemixedmodesession): Convenience function to easily start or update a proxy session. #### `startRemoteProxySession` This function starts a proxy session for a given set of bindings. It accepts options to control session behavior, including an `auth` option with your Cloudflare account ID and API token for remote binding access. It returns an object with: * `ready` `Promise`: Resolves when the session is ready. * `dispose` `() => Promise`: Stops the session. * `updateBindings` `(bindings: StartDevWorkerInput['bindings']) => Promise`: Updates session bindings. * `remoteProxyConnectionString` `remoteProxyConnectionString`: String to pass to Miniflare for remote binding access. #### `unstable_convertConfigBindingsToStartWorkerBindings` The `unstable_readConfig` utility returns an `Unstable_Config` object which includes the definition of the bindings included in the configuration file. These bindings definitions are however not directly compatible with `startRemoteProxySession`. It can be quite convenient to however read the binding declarations with `unstable_readConfig` and then pass them to `startRemoteProxySession`, so for this wrangler exposes `unstable_convertConfigBindingsToStartWorkerBindings` which is a simple utility to convert the bindings in an `Unstable_Config` object into a structure that can be passed to `startRemoteProxySession`. Note This type conversion is temporary. In the future, the types will be unified so you can pass the config object directly to `startRemoteProxySession`. #### `maybeStartOrUpdateRemoteProxySession` This wrapper simplifies proxy session management. It takes: * An object that contains either: * the path to a Wrangler configuration and a potential target environment * the name of the Worker and the bindings it is using * The current proxy session details (this parameter can be set to `null` or not being provided if none). * Potentially the auth data to use for the remote proxy session. It returns an object with the proxy session details if started or updated, or `null` if no proxy session is needed. The function: * Based on the first argument prepares the input arguments for the proxy session. * If there are no remote bindings to be used (nor a pre-existing proxy session) it returns null, signaling that no proxy session is needed. * If the details of an existing proxy session have been provided it updates the proxy session accordingly. * Otherwise if starts a new proxy session. * Returns the proxy session details (that can later be passed as the second argument to `maybeStartOrUpdateRemoteProxySession`). #### Example Here's a basic example of using Miniflare with `maybeStartOrUpdateRemoteProxySession` to provide a local dev session with remote bindings. This example uses a single hardcoded KV binding. ```js import { Miniflare, MiniflareOptions } from "miniflare"; import { maybeStartOrUpdateRemoteProxySession } from "wrangler"; let mf; let remoteProxySessionDetails = null; async function startOrUpdateDevSession() { remoteProxySessionDetails = await maybeStartOrUpdateRemoteProxySession( { bindings: { MY_KV: { type: "kv_namespace", id: "kv-id", remote: true, }, }, }, remoteProxySessionDetails, ); const miniflareOptions = { scriptPath: "./worker.js", kvNamespaces: { MY_KV: { id: "kv-id", remoteProxyConnectionString: remoteProxySessionDetails?.session.remoteProxyConnectionString, }, }, }; if (!mf) { mf = new Miniflare(miniflareOptions); } else { mf.setOptions(miniflareOptions); } } // ... tool logic that invokes `startOrUpdateDevSession()` ... // ... once the dev session is no longer needed run // `remoteProxySessionDetails?.session.dispose()` ``` ```ts import { Miniflare, MiniflareOptions } from "miniflare"; import { maybeStartOrUpdateRemoteProxySession } from "wrangler"; let mf: Miniflare | null; let remoteProxySessionDetails: Awaited< ReturnType > | null = null; async function startOrUpdateDevSession() { remoteProxySessionDetails = await maybeStartOrUpdateRemoteProxySession( { bindings: { MY_KV: { type: "kv_namespace", id: "kv-id", remote: true, }, }, }, remoteProxySessionDetails, ); const miniflareOptions: MiniflareOptions = { scriptPath: "./worker.js", kvNamespaces: { MY_KV: { id: "kv-id", remoteProxyConnectionString: remoteProxySessionDetails?.session.remoteProxyConnectionString, }, }, }; if (!mf) { mf = new Miniflare(miniflareOptions); } else { mf.setOptions(miniflareOptions); } } // ... tool logic that invokes `startOrUpdateDevSession()` ... // ... once the dev session is no longer needed run // `remoteProxySessionDetails?.session.dispose()` ``` ## `wrangler dev --remote` (Legacy) Separate from Miniflare-powered local development, Wrangler also offers a fully remote development mode via [wrangler dev --remote](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev). Remote development is [**not** supported in the Vite plugin](https://developers.cloudflare.com/workers/local-development/wrangler-vs-vite/). npmyarnpnpm ``` npx wrangler dev --remote ``` ``` yarn wrangler dev --remote ``` ``` pnpm wrangler dev --remote ``` During **remote development**, all of your Worker code is uploaded to a temporary preview environment on Cloudflare's infrastructure, and changes to your code are automatically uploaded as you save. When using remote development, all bindings automatically connect to their remote resources. Unlike local development, you cannot configure bindings to use local simulations - they will always use the deployed resources on Cloudflare's network. ### When to use Remote development * For most development tasks, the most efficient and productive experience will be local development along with [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings) when needed. * You may want to use `wrangler dev --remote` for testing features or behaviors that are highly specific to Cloudflare's network and cannot be adequately simulated locally or tested via remote bindings. ### Considerations * Iteration is significantly slower than local development due to the upload/deployment step for each change. ### Limitations * When you run a remote development session using the `--remote` flag, a limit of 50 [routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) per zone is enforced. Learn more in[ Workers platform limits](https://developers.cloudflare.com/workers/platform/limits/#routes-and-domains-when-using-wrangler-dev---remote). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/local-development/#page","headline":"Local development · Cloudflare Workers docs","description":"Develop and test your Workers locally.","url":"https://developers.cloudflare.com/workers/local-development/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-25","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Supported bindings per development mode title: Supported bindings per development mode image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Supported bindings per development mode Last updated Jun 25, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/bindings-per-env/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Local development **Local simulations**: During local development, your Worker code always executes locally and bindings connect to locally simulated resources [by default](https://developers.cloudflare.com/workers/local-development/#remote-bindings). This is supported in [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/). **Remote binding connections:**: Allows you to connect to remote resources on a [per-binding basis](https://developers.cloudflare.com/workers/local-development/#remote-bindings). This is supported in [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/). | Binding | Local simulations | Remote binding connections | | --------------------------------------- | ----------------- | -------------------------- | | **AI** | ❌ | ✅ | | **Assets** | ✅ | ❌ | | **Analytics Engine** | ✅ | ❌ | | **Browser Run** | ✅ | ✅ | | **D1** | ✅ | ✅ | | **Durable Objects** | ✅ | ❌ [1](#user-content-fn-1) | | **Containers** | ✅ | ❌ | | **Email Bindings** | ✅ | ✅ | | **Hyperdrive** | ✅ | ❌ | | **Images** | ✅ | ✅ | | **KV** | ✅ | ✅ | | **Media Transformations** | ❌ | ✅ | | **mTLS** | ❌ | ✅ | | **Queues** | ✅ | ✅ | | **R2** | ✅ | ✅ | | **Rate Limiting** | ✅ | ❌ | | **Service Bindings (multiple Workers)** | ✅ | ✅ | | **Vectorize** | ❌ | ✅ | | **Workflows** | ✅ | ❌ | ## Remote development During remote development, all of your Worker code is uploaded and executed on Cloudflare's infrastructure, and bindings always connect to remote resources. **We recommend using local development with remote binding connections instead** for faster iteration and debugging. Supported only in [wrangler dev --remote](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) \- there is **no Vite plugin equivalent**. | Binding | Remote development | | --------------------------------------- | ------------------ | | **AI** | ✅ | | **Assets** | ✅ | | **Analytics Engine** | ✅ | | **Browser Run** | ✅ | | **D1** | ✅ | | **Durable Objects** | ✅ | | **Containers** | ❌ | | **Email Bindings** | ✅ | | **Hyperdrive** | ✅ | | **Images** | ✅ | | **KV** | ✅ | | **Media Transformations** | ✅ | | **mTLS** | ✅ | | **Queues** | ❌ | | **R2** | ✅ | | **Rate Limiting** | ✅ | | **Service Bindings (multiple Workers)** | ✅ | | **Vectorize** | ✅ | | **Workflows** | ❌ | ## Footnotes 1. Refer to [Using remote resources with Durable Objects and Workflows](https://developers.cloudflare.com/workers/local-development/#using-remote-resources-with-durable-objects-and-workflows) for recommended workarounds. [↩](#user-content-fnref-1) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/local-development/bindings-per-env/#page","headline":"Supported bindings per development mode · Cloudflare Workers docs","description":"Supported bindings per development mode","url":"https://developers.cloudflare.com/workers/local-development/bindings-per-env/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-25","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Configuring environment variables and secrets for local development title: Environment variables and secrets image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Environment variables and secrets Last updated Jun 25, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/environment-variables/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Caution Do not use `vars` to store sensitive information in your Worker's Wrangler configuration file. Use secrets instead. Put secrets for use in local development in either a `.dev.vars` file or a `.env` file, in the same directory as the Wrangler configuration file. Note You can use the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) to declare which secret names your Worker requires. When defined, only the keys listed in `secrets.required` are loaded from `.dev.vars` or `.env`. Additional keys are excluded and missing keys produce a warning. Note Choose to use either `.dev.vars` or `.env` but not both. If you define a `.dev.vars` file, then values in `.env` files will not be included in the `env` object during local development. These files should be formatted using the [dotenv ↗](https://hexdocs.pm/dotenvy/dotenv-file-format.html) syntax. For example: ```bash SECRET_KEY="value" API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ``` Do not commit secrets to git The `.dev.vars` and `.env` files should not be committed to git. Add `.dev.vars*` and `.env*` to your project's `.gitignore` file. To set different secrets for each Cloudflare environment, create files named `.dev.vars.` or `.env.`. When you select a Cloudflare environment in your local development, the corresponding environment-specific file will be loaded ahead of the generic `.dev.vars` (or `.env`) file. * When using `.dev.vars.` files, all secrets must be defined per environment. If `.dev.vars.` exists then only this will be loaded; the `.dev.vars` file will not be loaded. * In contrast, all matching `.env` files are loaded and the values are merged. For each variable, the value from the most specific file is used, with the following precedence: * `.env..local` (most specific) * `.env.local` * `.env.` * `.env` (least specific) Controlling \`.env\` handling It is possible to control how `.env` files are loaded in local development by setting environment variables on the process running the tools. * To disable loading local dev vars from `.env` files without providing a `.dev.vars` file, set the `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV` environment variable to `"false"`. * To include every environment variable defined in your system's process environment as a local development variable, ensure there is no `.dev.vars` and then set the `CLOUDFLARE_INCLUDE_PROCESS_ENV` environment variable to `"true"`. This is not needed when using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property), which loads from `process.env` automatically. ### Basic setup Here are steps to set up environment variables for local development using either `.dev.vars` or `.env` files. 1. Create a `.dev.vars` / `.env` file in your project root. 2. Add key-value pairs: ```ini API_HOST="localhost:3000" DEBUG="true" SECRET_TOKEN="my-local-secret-token" ``` 3. Run your `dev` command **Wrangler** npmyarnpnpm ``` npx wrangler dev ``` ``` yarn wrangler dev ``` ``` pnpm wrangler dev ``` **Vite plugin** npmyarnpnpm ``` npx vite dev ``` ``` yarn vite dev ``` ``` pnpm vite dev ``` ## Multiple local environments To simulate different local environments, you can provide environment-specific files. For example, you might have a `staging` environment that requires different settings than your development environment. 1. Create a file named `.dev.vars.`/`.env.`. For example, we can use `.dev.vars.staging`/`.env.staging`. 2. Add key-value pairs: ```ini API_HOST="staging.localhost:3000" DEBUG="false" SECRET_TOKEN="staging-token" ``` 3. Specify the environment when running the `dev` command: **Wrangler** npmyarnpnpm ``` npx wrangler dev --env staging ``` ``` yarn wrangler dev --env staging ``` ``` pnpm wrangler dev --env staging ``` **Vite plugin** npmyarnpnpm ``` CLOUDFLARE_ENV=staging npx vite dev ``` ``` CLOUDFLARE_ENV=staging yarn vite dev ``` ``` CLOUDFLARE_ENV=staging pnpm vite dev ``` * If using `.dev.vars.staging`, only the values from that file will be applied instead of `.dev.vars`. * If using `.env.staging`, the values will be merged with `.env` files, with the most specific file taking precedence. ## Learn more * To learn how to configure multiple environments in Wrangler configuration, [read the documentation](https://developers.cloudflare.com/workers/wrangler/environments/#%5Ftop). * To learn how to use Wrangler environments and Vite environments together, [read the Vite plugin documentation](https://developers.cloudflare.com/workers/vite-plugin/reference/cloudflare-environments/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/local-development/environment-variables/#page","headline":"Environment variables and secrets · Cloudflare Workers docs","description":"Configuring environment variables and secrets for local development","url":"https://developers.cloudflare.com/workers/local-development/environment-variables/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-25","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Populating local resources with data title: Adding local data image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Adding local data Last updated Jun 25, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/local-data/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Whether you are using Wrangler or the [Cloudflare Vite plugin ↗](https://developers.cloudflare.com/workers/vite-plugin/), your workflow for **accessing** data during local development remains the same. However, you can only [populate local resources with data](https://developers.cloudflare.com/workers/local-development/local-data/#populating-local-resources-with-data) via the Wrangler CLI. ### How it works When you run either `wrangler dev` or [vite ↗](https://vite.dev/guide/cli#dev-server), [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/) automatically creates **local versions** of your resources (like [KV](https://developers.cloudflare.com/kv), [D1](https://developers.cloudflare.com/d1/), or [R2](https://developers.cloudflare.com/r2)). This means you **don’t** need to manually set up separate local instances for each service. However, newly created local resources **won’t** contain any data — you'll need to use Wrangler commands with the `--local` flag to populate them. Changes made to local resources won’t affect production data. ## Populating local resources with data When you first start developing, your local resources will be empty. You'll need to populate them with data using the Wrangler CLI. ### KV namespaces Syntax note Since version 3.60.0, Wrangler supports the `kv ...` syntax. If you are using versions below 3.60.0, the command follows the `kv:...` syntax. Learn more in the [Wrangler commands for KV page](https://developers.cloudflare.com/kv/reference/kv-commands/). #### [Add a single key-value pair](https://developers.cloudflare.com/workers/wrangler/commands/kv/#kv-key) npmyarnpnpm ``` npx wrangler kv key put --binding= --local ``` ``` yarn wrangler kv key put --binding= --local ``` ``` pnpm wrangler kv key put --binding= --local ``` #### [Bulk upload](https://developers.cloudflare.com/workers/wrangler/commands/kv/#kv-bulk) npmyarnpnpm ``` npx wrangler kv bulk put --binding= --local ``` ``` yarn wrangler kv bulk put --binding= --local ``` ``` pnpm wrangler kv bulk put --binding= --local ``` ### R2 buckets #### [Upload a file](https://developers.cloudflare.com/workers/wrangler/commands/r2/#r2-object) npmyarnpnpm ``` npx wrangler r2 object put / --file= --local ``` ``` yarn wrangler r2 object put / --file= --local ``` ``` pnpm wrangler r2 object put / --file= --local ``` You may also include [other metadata](https://developers.cloudflare.com/workers/wrangler/commands/r2/#r2-object-put). ### D1 databases #### [Execute a SQL statement](https://developers.cloudflare.com/workers/wrangler/commands/d1/#d1-execute) npmyarnpnpm ``` npx wrangler d1 execute --command="" --local ``` ``` yarn wrangler d1 execute --command="" --local ``` ``` pnpm wrangler d1 execute --command="" --local ``` #### [Execute a SQL file](https://developers.cloudflare.com/workers/wrangler/commands/d1/#d1-execute) npmyarnpnpm ``` npx wrangler d1 execute --file=./schema.sql --local ``` ``` yarn wrangler d1 execute --file=./schema.sql --local ``` ``` pnpm wrangler d1 execute --file=./schema.sql --local ``` ### Durable Objects For Durable Objects, unlike KV, D1, and R2, there are no CLI commands to populate them with local data. To add data to Durable Objects during local development, you must write application code that creates Durable Object instances and [calls methods on them that store state](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/). This typically involves creating development endpoints or test routes that initialize your Durable Objects with the desired data. ## Where local data gets stored By default, both Wrangler and the Vite plugin store local binding data in the same location: the `.wrangler/state` folder in your project directory. This folder stores data in subdirectories for all local bindings: KV namespaces, R2 buckets, D1 databases, Durable Objects, etc. ### Clearing local storage You can delete the `.wrangler/state` folder at any time to reset your local environment, and Miniflare will recreate it the next time you run your `dev` command. You can also delete specific sub-folders within `.wrangler/state` for more targeted clean-up. ### Changing the local data directory If you prefer to specify a different directory for local storage, you can do so through the Wrangler CLI or in the Vite plugin's configuration. #### Using Wrangler Use the [\--persist-to](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) flag with `wrangler dev`. You need to specify this flag every time you run the `dev` command: npmyarnpnpm ``` npx wrangler dev --persist-to ``` ``` yarn wrangler dev --persist-to ``` ``` pnpm wrangler dev --persist-to ``` Note The local persistence folder (like `.wrangler/state` or any custom folder you set) should be added to your `.gitignore` to avoid committing local development data to version control. Using `--local` with `--persist-to` If you run `wrangler dev --persist-to ` to specify a custom location for local data, you must also include the same `--persist-to ` when running other Wrangler commands that modify local data (and be sure to include the `--local` flag). For example, to create a KV key named `test` with a value of `12345` in a local KV namespace, run: npmyarnpnpm ``` npx wrangler kv key put test 12345 --binding MY_KV_NAMESPACE --local --persist-to worker-local ``` ``` yarn wrangler kv key put test 12345 --binding MY_KV_NAMESPACE --local --persist-to worker-local ``` ``` pnpm wrangler kv key put test 12345 --binding MY_KV_NAMESPACE --local --persist-to worker-local ``` This command: * Sets the KV key `test` to `12345` in the binding `MY_KV_NAMESPACE` (defined in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/)). * Uses `--persist-to worker-local` to ensure the data is created in the **worker-local** directory instead of the default `.wrangler/state`. * Adds the `--local` flag, indicating you want to modify local data. If `--persist-to` is not specified, Wrangler defaults to using `.wrangler/state` for local data. #### Using the Cloudflare Vite plugin To customize where the Vite plugin stores local data, configure the [persistState option](https://developers.cloudflare.com/workers/vite-plugin/reference/api/#interface-pluginconfig) in your Vite config file: ```js import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ plugins: [ cloudflare({ persistState: { path: "./my-custom-directory" }, }), ], }); ``` #### Sharing state between tools If you want Wrangler and the Vite plugin to share the same state, configure them to use the same persistence path. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/local-development/local-data/#page","headline":"Adding local data · Cloudflare Workers docs","description":"Populating local resources with data","url":"https://developers.cloudflare.com/workers/local-development/local-data/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-25","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Expose a local Wrangler or Vite dev server over a public tunnel URL. title: Share a local dev server image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Share a local dev server Last updated Jun 25, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/local-dev-tunnels/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can expose your local dev server over a [Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/) when you need to share a preview, test a webhook, or access your app from another device. This page covers tunnel support in [Wrangler](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/). ## Start a tunnel You can start a tunnel with Wrangler or the Cloudflare Vite plugin for the current session. This gives you either a random `*.trycloudflare.com` hostname via a [Quick tunnel](https://developers.cloudflare.com/tunnel/setup/#quick-tunnels-development), or a stable hostname via a [named tunnel](https://developers.cloudflare.com/tunnel/setup/#create-a-tunnel). **Wrangler** Run `wrangler dev`, then press `[t]` to start or close the tunnel. Wrangler will print the public tunnel URL or URLs for the current session. To use a named tunnel, run: npmyarnpnpm ``` npx wrangler dev --tunnel-name=my-tunnel ``` ``` yarn wrangler dev --tunnel-name=my-tunnel ``` ``` pnpm wrangler dev --tunnel-name=my-tunnel ``` Use `--tunnel` if you want the tunnel to open automatically when Wrangler starts. npmyarnpnpm ``` npx wrangler dev --tunnel ``` ``` yarn wrangler dev --tunnel ``` ``` pnpm wrangler dev --tunnel ``` **Cloudflare Vite plugin** Run `vite dev`, then press `t + Enter` to start or close the tunnel. Add `tunnel` to the plugin config if you want to configure a named tunnel or have the tunnel open automatically when Vite starts. To use a named tunnel with stable hostnames: ```js import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ plugins: [ cloudflare({ tunnel: { name: "my-tunnel" }, }), ], }); ``` ```ts import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ plugins: [ cloudflare({ tunnel: { name: "my-tunnel" }, }), ], }); ``` If you want the tunnel to open automatically when Vite starts, set `tunnel.autoStart` to `true`. When using `vite preview`, Vite's preview host validation still applies: * For Quick tunnel, add `.trycloudflare.com` to `preview.allowedHosts`. * For named tunnel, add the resolved hostnames or a matching domain suffix such as `.my-domain.com` to `preview.allowedHosts`. For example: ```js import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ preview: { allowedHosts: [ // For Quick tunnels: ".trycloudflare.com", // For named tunnels: ".my-domain.com", ], }, plugins: [ cloudflare({ tunnel: { name: "my-tunnel" }, }), ], }); ``` ```ts import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ preview: { allowedHosts: [ // For Quick tunnels: ".trycloudflare.com", // For named tunnels: ".my-domain.com" ], }, plugins: [ cloudflare({ tunnel: { name: "my-tunnel" }, }), ], }); ``` ## Security considerations Anyone with the tunnel URL can reach your dev server, so review what your app exposes before enabling a tunnel. * Pay special attention to ungated preview or admin endpoints. * Review any [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings) connected to real resources. * Review any code that proxies requests to private or internal services. * If you are using the Cloudflare Vite plugin with `vite dev`, HMR and module serving may expose source files, file paths, or project structure over the tunnel. If you only need to share a built preview, prefer `vite preview` for public sharing. * Local dev-related routes, such as `/cdn-cgi/*`, remain restricted and are not exposed over the tunnel. * If you need a stable hostname or stricter access control, use a named tunnel protected by Cloudflare Access. ## Related docs * [Cloudflare Tunnel](https://developers.cloudflare.com/tunnel/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/local-development/local-dev-tunnels/#page","headline":"Share a local dev server · Cloudflare Workers docs","description":"Expose a local Wrangler or Vite dev server over a public tunnel URL.","url":"https://developers.cloudflare.com/workers/local-development/local-dev-tunnels/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-25","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Browse and edit local binding data from your browser during development. title: Local Explorer image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Local Explorer Last updated Aug 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/local-explorer/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Local Explorer is a browser-based interface for viewing and editing the data in your local [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) and debugging Worker invocations during development. It is available at `/cdn-cgi/explorer` on your local development server. Instead of running CLI commands or writing throwaway code to inspect local state, you can open Local Explorer in your browser to work with your data, view traces, and search logs. This is useful when you want to seed test data, verify what your Worker wrote, debug a failing request, or run SQL queries against a local [D1](https://developers.cloudflare.com/d1/) database. Local Explorer works with both [Wrangler](https://developers.cloudflare.com/workers/wrangler/) and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/). ## Prerequisites * Wrangler 4.118.0 or later, or [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) 1.50.0 or later ## Open Local Explorer 1. Start a local development session: npmyarnpnpm ``` npx wrangler dev ``` ``` yarn wrangler dev ``` ``` pnpm wrangler dev ``` 2. Open Local Explorer in your browser: * **Wrangler**: press `e` in your terminal. * **Vite plugin**: navigate directly to `/cdn-cgi/explorer` on your dev server's route and port. Local Explorer is available by default and detects the bindings defined in your [Wrangler configuration](https://developers.cloudflare.com/workers/wrangler/configuration/) automatically. ## Supported bindings Local Explorer supports the following binding types: | Binding | View | Edit | | -------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------- | | [KV](https://developers.cloudflare.com/kv/) | Browse keys, view values and metadata | Create, update, and delete key-value pairs | | [R2](https://developers.cloudflare.com/r2/) | List objects, view metadata | Upload and delete objects | | [D1](https://developers.cloudflare.com/d1/) | Browse tables and rows, run SQL queries | Insert, update, and delete rows through SQL | | [Durable Objects](https://developers.cloudflare.com/durable-objects/) (SQLite storage) | Browse SQLite tables and rows, run SQL queries | Insert, update, and delete rows through SQL | | [Workflows](https://developers.cloudflare.com/workflows/) | List instances, view status and step history | Trigger new runs, retry failed instances | ### D1 and Durable Objects SQL Studio For [D1](https://developers.cloudflare.com/d1/) databases and [Durable Objects](https://developers.cloudflare.com/durable-objects/) that use the [SQLite storage API](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/), Local Explorer includes a SQL Studio. This is the same experience available in the Cloudflare dashboard for deployed D1 databases. It provides both a visual table browser with inline editing and a SQL query editor where you can run arbitrary queries. ## Observability Local Explorer automatically captures traces and logs from every Worker invocation during `wrangler dev` without modifying your code. You get the same instrumentation as production [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/) and [Traces](https://developers.cloudflare.com/workers/observability/traces/), including invocation logs, binding operations, timing, and console output, directly in your browser during development. ### Logs The **Logs** view captures all `console.*` output from your Worker. Filter by level (error, warn, info, log, debug) or search by text to find specific messages. ### Traces Each Worker invocation appears as a trace. Select any trace to see every binding operation with timing, status, and error details. Tracing also captures operations made through [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings), so you can inspect calls from your locally running Worker to deployed resources. For example, if a request makes two D1 calls and the second one fails, the trace shows you exactly which call succeeded and which errored without adding `console.log()` or try/catch blocks. ![Trace view for POST /api/todos showing two D1 database spans: the first INSERT succeeded, the second failed with error "no such table: audit_log"](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2048,height=847,format=webp/_astro/local-trace-failed-request.Bf0avznU.png) ## API Local Explorer exposes an API at `/cdn-cgi/explorer/api` that provides programmatic access to the same operations available in the browser interface. The API serves an [OpenAPI specification ↗](https://www.openapis.org/) that describes all available endpoints, parameters, and response formats. To retrieve the OpenAPI spec: ```sh curl http://localhost:8787/cdn-cgi/explorer/api ``` ### Use with AI agents When Wrangler or the Cloudflare Vite plugin detects it is running inside an AI agent, it prints a hint with the Local Explorer API endpoint directly to the terminal. The agent can fetch the [OpenAPI specification ↗](https://www.openapis.org/) from that endpoint to discover all available operations, then make API calls to read or modify local data, query traces and logs, and debug your Worker. The hint includes the API URL and relevant endpoints: ```txt This dev session is running in an AI agent. The Local Explorer API is available at http://localhost:8787/cdn-cgi/explorer/api ... Debug with traces: POST /cdn-cgi/explorer/api/local/observability/query -- query traces and logs with SQL ``` This can be useful as an alternative to the CLI when you want an agent to: * Populate test data in your local [KV](https://developers.cloudflare.com/kv/) namespaces or [D1](https://developers.cloudflare.com/d1/) databases * Inspect the state of a [Durable Object](https://developers.cloudflare.com/durable-objects/) during debugging * Trigger or retry a [Workflow](https://developers.cloudflare.com/workflows/) run with different input data * Upload test files to a local [R2](https://developers.cloudflare.com/r2/) bucket * Find recent requests with errors and drill into failing spans Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/local-development/local-explorer/#page","headline":"Local Explorer · Cloudflare Workers docs","description":"Browse and edit local binding data from your browser during development.","url":"https://developers.cloudflare.com/workers/local-development/local-explorer/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to develop with multiple Workers using different approaches and configurations. title: Developing with multiple Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Developing with multiple Workers Last updated Jun 25, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/multi-workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) When building complex applications, you may want to run multiple Workers during development. This guide covers the different approaches for running multiple Workers locally and when to use each approach. ## Single dev command Note We recommend this approach as the default for most development workflows as it ensures the best compatibility with bindings. You can run multiple Workers in a single dev command by passing multiple configuration files to your dev server: **Using Wrangler** npmyarnpnpm ``` npx wrangler dev -c ./app/wrangler.jsonc -c ./api/wrangler.jsonc ``` ``` yarn wrangler dev -c ./app/wrangler.jsonc -c ./api/wrangler.jsonc ``` ``` pnpm wrangler dev -c ./app/wrangler.jsonc -c ./api/wrangler.jsonc ``` The first config (`./app/wrangler.jsonc`) is treated as the primary Worker, exposed at `http://localhost:8787`. Additional configs (e.g. `./api/wrangler.jsonc`) run as auxiliary Workers, available via service bindings or tail consumers from the primary Worker. **Using the Vite plugin** Configure `auxiliaryWorkers` in your Vite configuration: ```js import { defineConfig } from "vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ plugins: [ cloudflare({ configPath: "./app/wrangler.jsonc", auxiliaryWorkers: [ { configPath: "./api/wrangler.jsonc", }, ], }), ], }); ``` Then run: npmyarnpnpm ``` npx vite dev ``` ``` yarn vite dev ``` ``` pnpm vite dev ``` **Use this approach when:** * You want the simplest setup for development * Workers are part of the same application or codebase * You need to access a Durable Object namespace or Workflow from another Worker using `script_name`, or set up Queues where the producer and consumer Workers are separated. ## Multiple dev commands You can also run each Worker in separate dev commands, each with its own terminal and configuration. npmyarnpnpm ``` # Terminal 1 npx wrangler dev -c ./app/wrangler.jsonc ``` ``` # Terminal 1 yarn wrangler dev -c ./app/wrangler.jsonc ``` ``` # Terminal 1 pnpm wrangler dev -c ./app/wrangler.jsonc ``` npmyarnpnpm ``` # Terminal 2 npx wrangler dev -c ./api/wrangler.jsonc ``` ``` # Terminal 2 yarn wrangler dev -c ./api/wrangler.jsonc ``` ``` # Terminal 2 pnpm wrangler dev -c ./api/wrangler.jsonc ``` These Workers run in different dev commands but can still communicate with each other via service bindings or tail consumers **regardless of whether they are started with `wrangler dev` or `vite dev`**. Note You can also combine both approaches — for example, run a group of Workers together through `vite dev` using `auxiliaryWorkers`, while running another Worker separately with `wrangler dev`. This allows you to keep tightly coupled Workers running under a single dev command, while keeping independent or shared Workers in separate ones. **Use this approach when:** * You want each Worker to be accessible on its own local URL during development, since only the primary Worker is exposed when using a single dev command * Each Worker has its own build setup or tooling — for example, one uses Vite with custom plugins while another is a vanilla Wrangler project * You need the flexibility to run and develop Workers independently without restructuring your project or consolidating configs This setup is especially useful in larger projects where each team maintains a subset of Workers. Running everything in a single dev command might require significant restructuring or build integration that isn't always practical. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/local-development/multi-workers/#page","headline":"Developing with multiple Workers · Cloudflare Workers docs","description":"Learn how to develop with multiple Workers using different approaches and configurations.","url":"https://developers.cloudflare.com/workers/local-development/multi-workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-25","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: A full-featured integration between Vite and the Workers runtime title: Vite plugin image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Vite plugin Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/vite-plugin/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The Cloudflare Vite plugin enables a full-featured integration between [Vite ↗](https://vite.dev/) and the [Workers runtime](https://developers.cloudflare.com/workers/runtime-apis/). Your Worker code runs inside [workerd ↗](https://github.com/cloudflare/workerd), matching the production behavior as closely as possible and providing confidence as you develop and deploy your applications. ## Features * Uses the Vite [Environment API ↗](https://vite.dev/guide/api-environment) to integrate Vite with the Workers runtime * Provides direct access to [Workers runtime APIs](https://developers.cloudflare.com/workers/runtime-apis/) and [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) * Builds your front-end assets for deployment to Cloudflare, enabling you to build static sites, SPAs, and full-stack applications * Official support for [TanStack Start ↗](https://tanstack.com/start/) and [React Router v8 ↗](https://reactrouter.com/) with server-side rendering * Leverages Vite's hot module replacement for consistently fast updates * Supports `vite preview` for previewing your build output in the Workers runtime prior to deployment ## Use cases * [TanStack Start ↗](https://tanstack.com/start/) * [React Router v8 ↗](https://reactrouter.com/) * Static sites, such as single-page applications, with or without an integrated backend API * Standalone Workers * Multi-Worker applications ## Get started To create a new application from a ready-to-go template, refer to the [TanStack Start](https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start/), [React Router](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/), [React](https://developers.cloudflare.com/workers/framework-guides/web-apps/react/) or [Vue](https://developers.cloudflare.com/workers/framework-guides/web-apps/vue/) framework guides. To create a standalone Worker from scratch, refer to [Get started](https://developers.cloudflare.com/workers/vite-plugin/get-started/). For a more in-depth look at adapting an existing Vite project and an introduction to key concepts, refer to the [Tutorial](https://developers.cloudflare.com/workers/vite-plugin/tutorial/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/vite-plugin/#page","headline":"Vite plugin · Cloudflare Workers docs","description":"A full-featured integration between Vite and the Workers runtime","url":"https://developers.cloudflare.com/workers/vite-plugin/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Choose between Wrangler and the Cloudflare Vite plugin for local development. title: Choosing between Wrangler & Vite image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Choosing between Wrangler & Vite Last updated Jul 28, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/local-development/wrangler-vs-vite/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Wrangler and the Cloudflare Vite plugin both provide local development environments for Workers. Both support backend Workers, local and remote bindings, and multi-Worker applications. Choose based on the build tools your project uses. You can also use the Vite plugin for development and builds while using Wrangler for deployment and other Workers commands. ## Compare Wrangler and Vite | Capability or workflow | Wrangler | Cloudflare Vite plugin | | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | Standalone JavaScript or TypeScript Workers | Supported | Supported | | Full-stack and backend Workers | Supported | Supported | | Local binding simulations via [Miniflare](https://developers.cloudflare.com/workers/testing/miniflare/) | Supported | Supported | | [Remote bindings](https://developers.cloudflare.com/workers/local-development/) | Supported | Supported | | Multi-Worker development | Supported | Supported | | Frontend and server-side rendering frameworks | Use the framework build output | Integrates with Vite-powered frameworks | | Build pipeline | Uses Wrangler's bundler or a custom build | Uses Vite transformations, Hot Module Replacement, and plugins | | Deployment and resource management | Supported | Use Wrangler after vite build | | [Rust Workers](https://developers.cloudflare.com/workers/languages/rust/) | Supported | Not supported | | [Python Workers](https://developers.cloudflare.com/workers/languages/python/) | Use [pywrangler](https://developers.cloudflare.com/workers/languages/python/) instead of wrangler | Not supported | Use the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) when your project already uses Vite or would benefit from its build pipeline. Vite is valid for standalone backend Workers, not only frontend applications. Use [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) when your project does not use Vite or you want a direct command-line workflow. Wrangler also provides deployment and resource management commands. For local development that requires deployed resources, both tools support [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings). Your Worker runs locally while selected bindings connect to deployed Cloudflare resources. For configuration differences when moving an existing project, refer to [Migrating from wrangler dev](https://developers.cloudflare.com/workers/vite-plugin/reference/migrating-from-wrangler-dev/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/local-development/wrangler-vs-vite/#page","headline":"Choosing between Wrangler & Vite · Cloudflare Workers docs","description":"Choose between Wrangler and the Cloudflare Vite plugin for local development.","url":"https://developers.cloudflare.com/workers/local-development/wrangler-vs-vite/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-28","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Preview and test Cloudflare Workers code in a browser-based sandbox without setup or authentication. title: Playground image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Playground Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/playground/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Browser support The Cloudflare Workers Playground is currently only supported in Firefox and Chrome desktop browsers. In Safari, it will show a `PreviewRequestFailed` error message. The quickest way to experiment with Cloudflare Workers is in the [Playground ↗](https://workers.cloudflare.com/playground). It does not require any setup or authentication. The Playground is a sandbox which gives you an instant way to preview and test a Worker directly in the browser. The Playground uses the same editor as the authenticated experience. The Playground provides the ability to [share](#share) the code you write as well as [deploy](#deploy) it instantly to Cloudflare's global network. This way, you can try new things out and deploy them when you are ready. [Launch the Playground](https://workers.cloudflare.com/playground) ## Hello Cloudflare Workers When you arrive in the Playground, you will see this default code: ```js import welcome from "welcome.html"; /** * @typedef {Object} Env */ export default { /** * @param {Request} request * @param {Env} env * @param {ExecutionContext} ctx * @returns {Response} */ fetch(request, env, ctx) { console.log("Hello Cloudflare Workers!"); return new Response(welcome, { headers: { "content-type": "text/html", }, }); }, }; ``` This is an example of a multi-module Worker that is receiving a [request](https://developers.cloudflare.com/workers/runtime-apis/request/), logging a message to the console, and then returning a [response](https://developers.cloudflare.com/workers/runtime-apis/response/) body containing the content from `welcome.html`. Refer to the [Fetch handler documentation](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) to learn more. ## Use the Playground As you edit the default code, the Worker will auto-update such that the preview on the right shows your Worker running just as it would in a browser. If your Worker uses URL paths, you can enter those in the input field on the right to navigate to them. The Playground provides type-checking via JSDoc comments and [workers-types ↗](https://www.npmjs.com/package/@cloudflare/workers-types). The Playground also provides pretty error pages in the event of application errors. To test a raw HTTP request (for example, to test a `POST` request), go to the **HTTP** tab and select **Send**. You can add and edit headers via this panel, as well as edit the body of a request. ## Log viewer The Playground and the quick editor in the Workers dashboard include a lightweight log viewer at the bottom of the preview panel. The log viewer displays the output of any calls to `console.log` made during preview runs. The log viewer supports the following: * Logging primitive values, objects, and arrays. * Clearing the log output between runs. At this time, the log viewer does not support logging class instances or their properties (for example, `request.url`). If you need a more complete development experience with full debugging capabilities, you can use [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) locally. To clone an existing Worker from your dashboard for local development, sign up and use the [wrangler init --from-dash](https://developers.cloudflare.com/workers/wrangler/commands/general/#init) command once your worker is deployed. ## Share To share what you have created, select **Copy Link** in the top right of the screen. This will copy a unique URL to your clipboard that you can share with anyone. These links do not expire, so you can bookmark your creation and share it at any time. Users that open a shared link will see the Playground with the shared code and preview. ## Deploy You can deploy a Worker from the Playground. If you are already logged in, you can review the Worker before deploying. Otherwise, you will be taken through the first-time user onboarding flow before you can review and deploy. Once deployed, your Worker will get its own unique URL and be available almost instantly on Cloudflare's global network. From here, you can add [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/), [storage resources](https://developers.cloudflare.com/workers/platform/storage-options/), and more. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/playground/#page","headline":"Playground · Cloudflare Workers docs","description":"Preview and test Cloudflare Workers code in a browser-based sandbox without setup or authentication.","url":"https://developers.cloudflare.com/workers/playground/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Manage Cloudflare Workers project settings, bindings, and deployment options. title: Configuration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Configuration Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Worker configuration is managed through a [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/), which defines your project settings, bindings, and deployment options. Wrangler is the command-line tool used to develop, test, and deploy Workers. For more information on Wrangler, refer to [Wrangler](https://developers.cloudflare.com/workers/wrangler/). * [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) * [Compatibility dates](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) * [Compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags/) * [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) * [Environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) * [Integrations](https://developers.cloudflare.com/workers/configuration/integrations/) * [Multipart upload metadata](https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/) * [Page Rules](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/) * [Placement](https://developers.cloudflare.com/workers/configuration/placement/) * [Routes and domains](https://developers.cloudflare.com/workers/configuration/routing/) * [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) * [Workers Sites](https://developers.cloudflare.com/workers/configuration/sites/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/configuration/#page","headline":"Configuration · Cloudflare Workers docs","description":"Manage Cloudflare Workers project settings, bindings, and deployment options.","url":"https://developers.cloudflare.com/workers/configuration/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Worker Bindings that allow for interaction with other Cloudflare Resources. title: Bindings (env) image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Bindings (env) Last updated Jul 22, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform. Bindings provide better performance and less restrictions when accessing resources from Workers than the [REST APIs](https://developers.cloudflare.com/api/) which are intended for non-Workers applications. During local development, bindings connect to locally simulated resources by default. You can also configure them to connect to real, production resources using [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings). The following bindings are available today: * [AI](https://developers.cloudflare.com/workers-ai/get-started/workers-wrangler/#2-connect-your-worker-to-workers-ai) * [Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine) * [Assets](https://developers.cloudflare.com/workers/static-assets/binding/) * [Browser Run](https://developers.cloudflare.com/browser-run/) * [D1](https://developers.cloudflare.com/d1/worker-api/) * [Dispatcher (Workers for Platforms)](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/) * [Durable Objects](https://developers.cloudflare.com/durable-objects/api/) * [Dynamic Worker Loaders](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/) * [Environment Variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) * [Hyperdrive](https://developers.cloudflare.com/hyperdrive) * [Images](https://developers.cloudflare.com/images/optimization/binding/) * [KV](https://developers.cloudflare.com/kv/api/) * [Media Transformations](https://developers.cloudflare.com/stream/transform-videos/bindings/) * [mTLS](https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls/) * [Queues](https://developers.cloudflare.com/queues/configuration/javascript-apis/) * [R2](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/) * [Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) * [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) * [Secrets Store](https://developers.cloudflare.com/secrets-store/integrations/workers/) * [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) * [Stream](https://developers.cloudflare.com/stream/manage-video-library/bindings/) * [Vectorize](https://developers.cloudflare.com/vectorize/reference/client-api/) * [Version metadata](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) * [Workflows](https://developers.cloudflare.com/workflows/) ## What is a binding? When you declare a binding on your Worker, you grant it a specific capability, such as being able to read and write files to an [R2](https://developers.cloudflare.com/r2/) bucket. For example: ```jsonc { "main": "./src/index.js", "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "" } ] } ``` ```toml main = "./src/index.js" [[r2_buckets]] binding = "MY_BUCKET" bucket_name = "" ``` ```js export default { async fetch(request, env) { const url = new URL(request.url); const key = url.pathname.slice(1); await env.MY_BUCKET.put(key, request.body); return new Response(`Put ${key} successfully!`); }, }; ``` ```python from workers import WorkerEntrypoint, Response from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): url = urlparse(request.url) key = url.path.slice(1) await self.env.MY_BUCKET.put(key, request.body) return Response(f"Put {key} successfully!") ``` You can think of a binding as a permission and an API in one piece. With bindings, you never have to add secret keys or tokens to your Worker in order to access resources on your Cloudflare account — the permission is embedded within the API itself. The underlying secret is never exposed to your Worker's code, and therefore can't be accidentally leaked. ## Making changes to bindings When you deploy a change to your Worker, and only change its bindings (i.e. you don't change the Worker's code), Cloudflare may reuse existing isolates that are already running your Worker. This improves performance — you can change an environment variable or other binding without unnecessarily reloading your code. As a result, you must be careful when "polluting" global scope with derivatives of your bindings. Anything you create there might continue to exist despite making changes to any underlying bindings. Consider an external client instance which uses a secret API key accessed from `env`: if you put this client instance in global scope and then make changes to the secret, a client instance using the original value might continue to exist. The correct approach would be to create a new client instance for each request. The following is a good approach: ```ts export default { fetch(request, env) { let client = new Client(env.MY_SECRET); // `client` is guaranteed to be up-to-date with the latest value of `env.MY_SECRET` since a new instance is constructed with every incoming request // ... do things with `client` }, }; ``` Compared to this alternative, which might have surprising and unwanted behavior: ```ts let client = undefined; export default { fetch(request, env) { client ??= new Client(env.MY_SECRET); // `client` here might not be updated when `env.MY_SECRET` changes, since it may already exist in global scope // ... do things with `client` }, }; ``` If you have more advanced needs, explore the [AsyncLocalStorage API](https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/), which provides a mechanism for exposing values down to child execution handlers. ## How to access `env` Bindings are located on the `env` object, which can be accessed in several ways: * It is an argument to entrypoint handlers such as [fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/): ```js export default { async fetch(request, env) { return new Response(`Hi, ${env.NAME}`); }, }; ``` * It is as class property on [WorkerEntrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env), [DurableObject](https://developers.cloudflare.com/durable-objects/), and [Workflow](https://developers.cloudflare.com/workflows/): ```js export class MyDurableObject extends DurableObject { async sayHello() { return `Hi, ${this.env.NAME}!`; } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response(f"Hi {self.env.NAME}") ``` * It can be imported from `cloudflare:workers`: ```js import { env } from "cloudflare:workers"; console.log(`Hi, ${env.Name}`); ``` ```python from workers import env print(f"Hi, {env.NAME}") ``` ### Importing `env` as a global Importing `env` from `cloudflare:workers` is useful when you need to access a binding such as [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) or [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/)in top-level global scope. For example, to initialize an API client: ```js import { env } from "cloudflare:workers"; import ApiClient from "example-api-client"; // API_KEY and LOG_LEVEL now usable in top-level scope let apiClient = ApiClient.new({ apiKey: env.API_KEY }); const LOG_LEVEL = env.LOG_LEVEL || "info"; export default { fetch(req) { // you can use apiClient or LOG_LEVEL, configured before any request is handled }, }; ``` ```python from workers import WorkerEntrypoint, env from example_api_client import ApiClient api_client = ApiClient(api_key=env.API_KEY) LOG_LEVEL = getattr(env, "LOG_LEVEL", "info") class Default(WorkerEntrypoint): async def fetch(self, request): # ... ``` Workers do not allow I/O from outside a request context. This means that even though `env` is accessible from the top-level scope, you will not be able to access every binding's methods. For instance, environment variables and secrets are accessible, and you are able to call `env.NAMESPACE.get` to get a [Durable Object stub](https://developers.cloudflare.com/durable-objects/api/stub/) in the top-level context. However, calling methods on the Durable Object stub, making [calls to a KV store](https://developers.cloudflare.com/kv/api/), and [calling to other Workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings) will not work. ```js import { env } from "cloudflare:workers"; // This would error! // env.KV.get('my-key') export default { async fetch(req) { // This works let myVal = await env.KV.get("my-key"); Response.new(myVal); }, }; ``` ```python from workers import Response, WorkerEntrypoint, env # This would fail! # env.KV.get('my-key') class Default(WorkerEntrypoint): async def fetch(self, request): # This works mv_val = await env.KV.get("my-key") return Response(my_val) ``` Additionally, importing `env` from `cloudflare:workers` lets you avoid passing `env`as an argument through many function calls if you need to access a binding from a deeply-nested function. This can be helpful in a complex codebase. ```js import { env } from "cloudflare:workers"; export default { fetch(req) { Response.new(sayHello()); }, }; // env is not an argument to sayHello... function sayHello() { let myName = getName(); return `Hello, ${myName}`; } // ...nor is it an argument to getName function getName() { return env.MY_NAME; } ``` ```python from workers import Response, WorkerEntrypoint, env class Default(WorkerEntrypoint): def fetch(req): return Response(say_hello()) # env is not an argument to say_hello... def say_hello(): my_name = get_name() return f"Hello, {myName}" # ...nor is it an argument to getName def get_name(): return env.MY_NAME ``` Note While using `env` from `cloudflare:workers` may be simpler to write than passing it through a series of function calls, passing `env` as an argument is a helpful pattern for dependency injection and testing. ### Overriding `env` values The `withEnv` function provides a mechanism for overriding values of `env`. Imagine a user has defined the [environment variable](https://developers.cloudflare.com/workers/configuration/environment-variables/)"NAME" to be "Alice" in their Wrangler configuration file and deployed a Worker. By default, logging `env.NAME` would print "Alice". Using the `withEnv` function, you can override the value of "NAME". ```js import { env, withEnv } from "cloudflare:workers"; function logName() { console.log(env.NAME); } export default { fetch(req) { // this will log "Alice" logName(); withEnv({ NAME: "Bob" }, () => { // this will log "Bob" logName(); }); // ...etc... }, }; ``` ```python from workers import Response, WorkerEntrypoint, env, patch_env def log_name(): print(env.NAME) class Default(WorkerEntrypoint): async def fetch(req): # this will log "Alice" log_name() with patch_env(NAME="Bob"): # this will log "Bob" log_name() # ...etc... ``` This can be useful when testing code that relies on an imported `env` object. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/#page","headline":"Bindings (env) · Cloudflare Workers docs","description":"Worker Bindings that allow for interaction with other Cloudflare Resources.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-22","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Bindings"]} ``` --- --- description: Opt into a specific version of the Workers runtime for your Workers project. title: Compatibility dates image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Compatibility dates Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/compatibility-dates/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Cloudflare regularly updates the Workers runtime. These updates apply to all Workers globally and should never cause a Worker that is already deployed to stop functioning. Sometimes, though, some changes may be backwards-incompatible. In particular, there might be bugs in the runtime API that existing Workers may inadvertently depend upon. Cloudflare implements bug fixes that new Workers can opt into while existing Workers will continue to see the buggy behavior to prevent breaking deployed Workers. The compatibility date and flags are how you, as a developer, opt into these runtime changes. [Compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags) will often have a date in which they are enabled by default, and so, by specifying a `compatibility_date` for your Worker, you can quickly enable all of these various compatibility flags up to, and including, that date. ## Setting compatibility date When you start your project, you should always set `compatibility_date` to the current date. You should occasionally update the `compatibility_date` field. When updating, you should refer to the [compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags) page to find out what has changed, and you should be careful to test your Worker to see if the changes affect you, updating your code as necessary. The new compatibility date takes effect when you next run the [npx wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) command. There is no need to update your `compatibility_date` if you do not want to. The Workers runtime will support old compatibility dates forever. If, for some reason, Cloudflare finds it is necessary to make a change that will break live Workers, Cloudflare will actively contact affected developers. That said, Cloudflare aims to avoid this if at all possible. However, even though you do not need to update the `compatibility_date` field, it is a good practice to do so for two reasons: 1. Sometimes, new features can only be made available to Workers that have a current `compatibility_date`. To access the latest features, you need to stay up-to-date. 2. Generally, other than the [compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags) page, the Workers documentation may only describe the current `compatibility_date`, omitting information about historical behavior. If your Worker uses an old `compatibility_date`, you will need to continuously refer to the compatibility flags page in order to check if any of the APIs you are using have changed. #### Via Wrangler The compatibility date can be set in a Worker's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). ```jsonc { // Opt into backwards-incompatible changes through April 5, 2022. "compatibility_date": "2022-04-05" } ``` ```toml compatibility_date = "2022-04-05" ``` #### Via the Cloudflare Dashboard When a Worker is created through the Cloudflare Dashboard, the compatibility date is automatically set to the current date. The compatibility date can be updated in the Workers settings on the [Cloudflare dashboard ↗](https://dash.cloudflare.com/). #### Via the Cloudflare API The compatibility date can be set when uploading a Worker using the [Workers Script API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/) or [Workers Versions API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/versions/methods/create/) in the request body's `metadata` field. If a compatibility date is not specified on upload via the API, it defaults to the oldest compatibility date, before any flags took effect (2021-11-02). When creating new Workers, it is highly recommended to set the compatibility date to the current date when uploading via the API. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/compatibility-dates/#page","headline":"Compatibility dates · Cloudflare Workers docs","description":"Opt into a specific version of the Workers runtime for your Workers project.","url":"https://developers.cloudflare.com/workers/configuration/compatibility-dates/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Opt into a specific features of the Workers runtime for your Workers project. title: Compatibility flags image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Compatibility flags Last updated Aug 12, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/compatibility-flags/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Compatibility flags enable specific features. They can be useful if you want to help the Workers team test upcoming changes that are not yet enabled by default, or if you need to hold back a change that your code depends on but still want to apply other compatibility changes. Compatibility flags will often have a date in which they are enabled by default, and so, by specifying a [compatibility\_date](https://developers.cloudflare.com/workers/configuration/compatibility-dates) for your Worker, you can quickly enable all of these various compatibility flags up to, and including, that date. ## Setting compatibility flags You may provide a list of `compatibility_flags`, which enable or disable specific changes. #### Via Wrangler Compatibility flags can be set in a Worker's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). This example enables the specific flag `formdata_parser_supports_files`, which is described [below](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#formdata-parsing-supports-file). As of the specified date, `2021-09-14`, this particular flag was not yet enabled by default, but, by specifying it in `compatibility_flags`, we can enable it anyway. `compatibility_flags` can also be used to disable changes that became the default in the past. ```jsonc { // Opt into backwards-incompatible changes through September 14, 2021. "compatibility_date": "2021-09-14", // Also opt into an upcoming fix to the FormData API. "compatibility_flags": [ "formdata_parser_supports_files" ] } ``` ```toml compatibility_date = "2021-09-14" compatibility_flags = [ "formdata_parser_supports_files" ] ``` #### Via the Cloudflare Dashboard Compatibility flags can be updated in the Workers settings on the [Cloudflare dashboard ↗](https://dash.cloudflare.com/). #### Via the Cloudflare API Compatibility flags can be set when uploading a Worker using the [Workers Script API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/) or [Workers Versions API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/versions/methods/create/) in the request body's `metadata` field. ## Node.js compatibility flag Note [The nodejs\_compat flag](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) also enables `nodejs_compat_v2` for compatibility dates from `2024-09-23` through `2026-08-03`. The v2 flag improves Node.js compatibility by bundling additional polyfills and globals into your Worker. However, this improvement increases bundle size. For compatibility dates through `2024-09-22`, add `nodejs_compat_v2` alongside `nodejs_compat` to turn on v2\. For compatibility dates from `2024-09-23` through `2026-08-03`, add `no_nodejs_compat_v2` alongside `nodejs_compat` to use v1 only. A [growing subset](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) of Node.js APIs are available directly as [Runtime APIs](https://developers.cloudflare.com/workers/runtime-apis/nodejs/), with no need to add polyfills to your own code. For compatibility dates of `2026-08-04` or later, Workers and Pages projects enable both `nodejs_compat` and `nodejs_compat_v2` by default. Built-in runtime APIs and polyfills are available without additional configuration. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. If your compatibility date is before `2026-08-04`, add the [nodejs\_compat](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag) [compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag) to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in: ```jsonc { "compatibility_flags": [ "nodejs_compat" ] } ``` ```toml compatibility_flags = [ "nodejs_compat" ] ``` To turn off [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) completely for a compatibility date of `2026-08-04` or later, remove the positive flags if present. Then add both `no_nodejs_compat` and `no_nodejs_compat_v2`. For configuration examples, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates because the compatibility date enables the same behavior. Wrangler, Miniflare, the Cloudflare Vite plugin, and Vitest Pool Workers ignore these redundant flags when starting the runtime. Existing projects do not need to remove them when updating their compatibility date. Omit them from new configurations. To turn off Node.js compatibility completely for a compatibility date of `2026-08-04` or later, remove `nodejs_compat` and `nodejs_compat_v2` if present. Then add both of the following flags: ```jsonc { "compatibility_flags": [ "no_nodejs_compat", "no_nodejs_compat_v2" ] } ``` ```toml compatibility_flags = [ "no_nodejs_compat", "no_nodejs_compat_v2" ] ``` The Node.js `AsyncLocalStorage` API is a particularly useful feature for Workers. To enable only the `AsyncLocalStorage` API, use the `nodejs_als` compatibility flag. ```jsonc { "compatibility_flags": [ "nodejs_als" ] } ``` ```toml compatibility_flags = [ "nodejs_als" ] ``` ## Flags history Newest flags are listed first. ### Remove Node.js 24.x end-of-life APIs | **Default as of** | 2028-04-30 | | ------------------- | -------------------------------- | | **Flag to enable** | remove\_nodejs\_compat\_eol\_v24 | | **Flag to disable** | add\_nodejs\_compat\_eol\_v24 | When `remove_nodejs_compat_eol_v24` is enabled, APIs that reached end-of-life in Node.js 24.x are removed. This flag is automatically enabled when the `remove_nodejs_compat_eol` flag is enabled after 2028-04-30. ### Remove Node.js 22.x end-of-life APIs | **Default as of** | 2027-04-30 | | ------------------- | -------------------------------- | | **Flag to enable** | remove\_nodejs\_compat\_eol\_v22 | | **Flag to disable** | add\_nodejs\_compat\_eol\_v22 | When `remove_nodejs_compat_eol_v22` is enabled, APIs that reached end-of-life in Node.js 22.x are removed. This flag is automatically enabled when the `remove_nodejs_compat_eol` flag is enabled after 2027-04-30. ### Throw On Not Implements TLS Options | **Default as of** | 2026-06-16 | | ------------------- | --------------------------------------------- | | **Flag to enable** | throw\_on\_not\_implemented\_tls\_options | | **Flag to disable** | no\_throw\_on\_not\_implemented\_tls\_options | When enabled, passing unsupported TLS options (e.g. `checkServerIdentity`) to `tls.connect()` or `new TLSSocket()` throws `ERR_OPTION_NOT_IMPLEMENTED` instead of silently ignoring them ### Process .pth files for Python Workers | **Default as of** | 2026-05-26 | | ------------------- | ------------------------------------ | | **Flag to enable** | python\_process\_pth\_files | | **Flag to disable** | disable\_python\_process\_pth\_files | When the `python_process_pth_files` flag is set, Python Workers process `.pth`files in the `python_modules/` directory during startup by calling [site.addsitedir() ↗](https://docs.python.org/3/library/site.html#site.addsitedir)on it. This lets packages extend `sys.path` declaratively, for example to add subdirectories or register import hooks. Without this flag, `.pth` files in `python_modules/` are ignored. This flag also moves the top-level entropy context managers required by some packages out of the runtime and into [workers-py ↗](https://github.com/cloudflare/workers-py). You must use `workers-py` version `1.1.3` or later when this flag is set. ### Node.js diagnostics\_channel hasSubscribers getter | **Default as of** | 2026-05-19 | | ------------------- | -------------------------------------------------- | | **Flag to enable** | diagnostics\_channel\_has\_subscribers\_getter | | **Flag to disable** | no\_diagnostics\_channel\_has\_subscribers\_getter | When `diagnostics_channel_has_subscribers_getter` is enabled, `Channel.hasSubscribers` and `TracingChannel.hasSubscribers` from `node:diagnostics_channel` become read-only getter properties that evaluate directly to a boolean, matching Node.js behavior. Previously, `hasSubscribers` was registered as a method, requiring users to call `ch.hasSubscribers()` with parentheses. With this flag enabled, `ch.hasSubscribers` returns a boolean without a function call, consistent with the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/diagnostics%5Fchannel.html#channelhassubscribers). This flag requires [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) to be enabled. ### Workflows preserve `NonRetryableError` message | **Default as of** | 2026-05-14 | | ------------------- | --------------------------------------------------- | | **Flag to enable** | workflows\_preserve\_non\_retryable\_error\_message | | **Flag to disable** | workflows\_replace\_non\_retryable\_error\_message | When enabled, if a [Workflow](https://developers.cloudflare.com/workflows/) step throws a [NonRetryableError](https://developers.cloudflare.com/workflows/build/workers-api/#nonretryableerror), the error `message` and `name` properties are preserved on the thrown exception instead of being replaced with a generic termination string. Previously, throwing a `NonRetryableError` with a custom message would result in the original error message being lost and replaced with `"The execution of the Workflow instance was terminated, as a step threw an NonRetryableError and it was not handled"`: ```js import { WorkflowEntrypoint, NonRetryableError } from "cloudflare:workers"; export class MyWorkflow extends WorkflowEntrypoint { async run(event, step) { await step.do("my-step", async () => { throw new NonRetryableError("custom error message"); // Without this flag: error.message === "The execution of the Workflow instance was terminated, as a step threw an NonRetryableError and it was not handled" // With this flag: error.message === "custom error message" }); } } ``` With the `workflows_preserve_non_retryable_error_message` flag enabled, the original error message and name are preserved, making it easier to debug and handle specific error cases in your Workflow code. ### Enhanced error serialization | **Default as of** | 2026-04-21 | | ------------------- | ------------------------------ | | **Flag to enable** | enhanced\_error\_serialization | | **Flag to disable** | legacy\_error\_serialization | When `enhanced_error_serialization` is enabled, errors serialized using `structuredClone()` or V8 serialization support more error types and include own properties on the error object. Note that when enabled, deserialization of errors will not preserve the original stack trace by default. Previously, only basic `Error` types were serialized, and own properties added to error objects were lost during serialization. ### Use an isolated PID namespace for containers | **Default as of** | 2026-04-01 | | ------------------- | ------------------------------ | | **Flag to enable** | containers\_pid\_namespace | | **Flag to disable** | no\_containers\_pid\_namespace | When `containers_pid_namespace` is set, containers will use an isolated PID namespace. The `ENTRYPOINT` of your container will have PID 1. When unset, the container shares the PID namespace with the virtual machine (VM) containing the container. The `ENTRYPOINT` of your container will _not_ have PID 1 and other processes running on the VM (that are not part of your container) will be visible. ### Spec-compliant TextEncoderStream/TextDecoderStream backpressure | **Default as of** | 2026-03-24 | | ------------------- | -------------------------------------------------- | | **Flag to enable** | encoder\_stream\_spec\_compliant\_backpressure | | **Flag to disable** | no\_encoder\_stream\_spec\_compliant\_backpressure | When `encoder_stream_spec_compliant_backpressure` is enabled, `TextEncoderStream` and `TextDecoderStream` use a readable-side high water mark of 0, as specified by the [WHATWG Encoding Standard ↗](https://encoding.spec.whatwg.org/). With a high water mark of 0, the readable side starts with backpressure applied, so writes correctly block until a reader pulls. Previously, the high water mark defaulted to 1, which caused `pull()` to fire at startup, clearing backpressure before any write occurred. ### Spec-compliant WritableStream writer behavior | **Default as of** | 2026-03-24 | | ------------------- | --------------------------------------------- | | **Flag to enable** | writable\_stream\_spec\_compliant\_writer | | **Flag to disable** | no\_writable\_stream\_spec\_compliant\_writer | When `writable_stream_spec_compliant_writer` is enabled, several `WritableStream` spec compliance issues around writer lock and release behavior are fixed to match the [WHATWG Streams Standard ↗](https://streams.spec.whatwg.org/). ### Enable global Performance classes | **Default as of** | 2026-03-17 | | ------------------- | ------------------------------------- | | **Flag to enable** | enable\_global\_performance\_classes | | **Flag to disable** | disable\_global\_performance\_classes | When `enable_global_performance_classes` is enabled, the following classes are available on the global scope: `PerformanceEntry`, `PerformanceMark`, `PerformanceMeasure`, `PerformanceResourceTiming`, `PerformanceObserver`, and `PerformanceObserverEntryList`. These classes are also implicitly enabled by the `enable_nodejs_perf_hooks_module` flag. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. ### Enable `node:child_process` module | **Default as of** | 2026-03-17 | | ------------------- | --------------------------------------- | | **Flag to enable** | enable\_nodejs\_child\_process\_module | | **Flag to disable** | disable\_nodejs\_child\_process\_module | The `enable_nodejs_child_process_module` flag enables the `node:child_process` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/child%5Fprocess.html) for more details about the `node:child_process` API. ### Enable `node:perf_hooks` module | **Default as of** | 2026-03-17 | | ------------------- | ------------------------------------ | | **Flag to enable** | enable\_nodejs\_perf\_hooks\_module | | **Flag to disable** | disable\_nodejs\_perf\_hooks\_module | The `enable_nodejs_perf_hooks_module` flag enables the `node:perf_hooks` module in Workers. This flag also implicitly enables global Performance classes (`PerformanceEntry`, `PerformanceMark`, `PerformanceMeasure`, `PerformanceResourceTiming`, `PerformanceObserver`, and `PerformanceObserverEntryList`). This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/perf%5Fhooks.html) for more details about the `node:perf_hooks` API. ### Enable `node:readline` module | **Default as of** | 2026-03-17 | | ------------------- | --------------------------------- | | **Flag to enable** | enable\_nodejs\_readline\_module | | **Flag to disable** | disable\_nodejs\_readline\_module | The `enable_nodejs_readline_module` flag enables the `node:readline` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/readline.html) for more details about the `node:readline` API. ### Enable `node:repl` module | **Default as of** | 2026-03-17 | | ------------------- | ----------------------------- | | **Flag to enable** | enable\_nodejs\_repl\_module | | **Flag to disable** | disable\_nodejs\_repl\_module | The `enable_nodejs_repl_module` flag enables the `node:repl` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/repl.html) for more details about the `node:repl` API. ### Enable `node:tty` module | **Default as of** | 2026-03-17 | | ------------------- | ---------------------------- | | **Flag to enable** | enable\_nodejs\_tty\_module | | **Flag to disable** | disable\_nodejs\_tty\_module | The `enable_nodejs_tty_module` flag enables the `node:tty` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/tty.html) for more details about the `node:tty` API. ### Enable `node:v8` module | **Default as of** | 2026-03-17 | | ------------------- | --------------------------- | | **Flag to enable** | enable\_nodejs\_v8\_module | | **Flag to disable** | disable\_nodejs\_v8\_module | The `enable_nodejs_v8_module` flag enables the `node:v8` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/v8.html) for more details about the `node:v8` API. ### Enable `node:worker_threads` module | **Default as of** | 2026-03-17 | | ------------------- | ---------------------------------------- | | **Flag to enable** | enable\_nodejs\_worker\_threads\_module | | **Flag to disable** | disable\_nodejs\_worker\_threads\_module | The `enable_nodejs_worker_threads_module` flag enables the `node:worker_threads` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-03-17 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/worker%5Fthreads.html) for more details about the `node:worker_threads` API. ### WebSocket standard binary type | **Default as of** | 2026-03-17 | | ------------------- | ------------------------------------- | | **Flag to enable** | websocket\_standard\_binary\_type | | **Flag to disable** | no\_websocket\_standard\_binary\_type | This flag controls the default value of the [binaryType](https://developers.cloudflare.com/workers/runtime-apis/websockets/#binarytype) property on `WebSocket`, which in turn controls how binary frames are delivered to the `message` event. With the flag active, `binaryType` defaults to `"blob"` and binary frames arrive as [Blob ↗](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, matching the [WebSocket specification ↗](https://websockets.spec.whatwg.org/) and standard browser behavior. Without the flag, `binaryType` defaults to `"arraybuffer"` and binary frames arrive as [ArrayBuffer ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/ArrayBuffer), matching the runtime's historical behavior. The `binaryType` property itself is available on every `WebSocket` regardless of the flag. Assigning a value overrides the default for that specific WebSocket: ```js const resp = await fetch("https://example.com", { headers: { Upgrade: "websocket" }, }); const ws = resp.webSocket; // Opt back into ArrayBuffer delivery before calling accept(). ws.binaryType = "arraybuffer"; ws.accept(); ws.addEventListener("message", (event) => { // event.data is an ArrayBuffer for binary frames. }); ``` If you are not ready to migrate and want to keep `ArrayBuffer` as the default for every WebSocket in your Worker, add the `no_websocket_standard_binary_type` flag to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). This flag has no effect on the Durable Object hibernatable WebSocket [webSocketMessage](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) handler, which always receives binary data as `ArrayBuffer`. ### Expose error codes in Queue operations | **Default as of** | 2026-03-12 | | ------------------- | ------------------------------- | | **Flag to enable** | queue\_expose\_error\_codes | | **Flag to disable** | no\_queue\_expose\_error\_codes | When `queue_expose_error_codes` is enabled, [Queue](https://developers.cloudflare.com/queues/) operations will include detailed error information, including error codes and causes, making it easier to handle and diagnose queue-related errors programmatically. ### WebSocket auto-reply to close | **Default as of** | 2026-04-07 | | ------------------- | ------------------------------------- | | **Flag to enable** | web\_socket\_auto\_reply\_to\_close | | **Flag to disable** | web\_socket\_manual\_reply\_to\_close | When a server sends a WebSocket Close frame, the Workers runtime now automatically sends a reciprocal Close frame and transitions `readyState` to `CLOSED` before firing the `close` event. This matches the [WebSocket spec ↗](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close%5Fevent) and browser behavior. Previously, receiving a server-initiated Close frame left the WebSocket in `CLOSING` and required the application to call `close()` itself. With this flag active, you no longer need to call `close()` in your `close` event handler. The runtime handles the close handshake automatically. ```js const [client, server] = Object.values(new WebSocketPair()); server.accept(); server.addEventListener("close", (event) => { // readyState is already CLOSED — no need to call server.close(). console.log(server.readyState); // WebSocket.CLOSED console.log(event.code); // 1000 console.log(event.wasClean); // true }, { once: true }); ``` If you do still call `close()` inside the handler, the call is silently ignored. This means existing code that manually replies to Close frames will not break when you update your compatibility date. The automatic close behavior can interfere with WebSocket proxying. When a Worker proxies between a client and a backend, the old behavior allowed the Worker to observe a backend Close frame without the runtime tearing down the connection, giving the Worker time to coordinate a clean close on the client side. To support this pattern, the `accept()` method now accepts an option `allowHalfOpen`. Call `ws.accept({ allowHalfOpen: true })` to restore the old half-open behavior regardless of the compatibility flag. ```js const [client, server] = Object.values(new WebSocketPair()); // Opt into half-open mode for proxying server.accept({ allowHalfOpen: true }); server.addEventListener("close", (event) => { // With allowHalfOpen true, readyState is still CLOSING here, // giving you time to coordinate the close on the other side. console.log(server.readyState); // WebSocket.CLOSING // Manually close when ready. server.close(1000, "done"); }, { once: true }); ``` Note that there is no corresponding option to the `WebSocket` constructor. WebSockets constructed with `new WebSocket` will always auto-reply to closes after this flag takes effect. WebSockets constructed this way are automatically "accepted", so there is no opportunity to pass the option to `accept()`. If you are creating a WebSocket with `new WebSocket`, but you need half-open behavior, you will need to switch to using `fetch()` instead. ```js // This does not allow half-open: let ws = new WebSocket("wss://example.com"); // But you can do this instead: let resp = await fetch("https://example.com", { headers: { "Upgrade": "websocket" } }); if (!resp.webSocket) { throw new Error("WebSocket handshake not accepted"); } let ws = resp.webSocket; ws.accept({ allowHalfOpen: true }); ``` For more information, refer to the [WebSocket API documentation](https://developers.cloudflare.com/workers/runtime-apis/websockets/). ### Dedicated CJK TextDecoder implementation | **Default as of** | 2026-03-03 | | ------------------- | ------------------------------------ | | **Flag to enable** | text\_decoder\_cjk\_decoder | | **Flag to disable** | disable\_text\_decoder\_cjk\_decoder | When `text_decoder_cjk_decoder` is enabled, a dedicated CJK `TextDecoder` implementation is used for CJK encoding overrides and Big5 lead-byte handling, instead of the legacy ICU-only code path. This improves spec compliance for CJK text decoding. ### Defer unhandled rejection processing to after microtask checkpoint | **Default as of** | 2026-03-03 | | ------------------- | ------------------------------------------------------ | | **Flag to enable** | unhandled\_rejection\_after\_microtask\_checkpoint | | **Flag to disable** | no\_unhandled\_rejection\_after\_microtask\_checkpoint | When `unhandled_rejection_after_microtask_checkpoint` is enabled, `unhandledrejection` event processing is deferred until the microtask checkpoint completes. This avoids misfires on multi-tick promise chains where a rejection handler is added in a later microtask. Previously, unhandled rejection processing could fire prematurely before all microtasks in the current checkpoint had been processed, leading to false `unhandledrejection` events for promises that were actually handled. ### Enforce WebSocket close reason byte limit | **Default as of** | 2026-03-03 | | ------------------- | ----------------------------------------- | | **Flag to enable** | websocket\_close\_reason\_byte\_limit | | **Flag to disable** | no\_websocket\_close\_reason\_byte\_limit | When `websocket_close_reason_byte_limit` is enabled, `WebSocket.close()` throws a `SyntaxError` `DOMException` if the `reason` string exceeds 123 bytes when UTF-8 encoded, as required by the [WHATWG WebSocket spec ↗](https://websockets.spec.whatwg.org/) and [RFC 6455 Section 5.5 ↗](https://www.rfc-editor.org/rfc/rfc6455#section-5.5). Previously, Workers allowed arbitrarily long close reasons without validation. ### Durable Object `deleteAll()` deletes alarms | **Default as of** | 2026-02-24 | | ------------------- | ----------------------------- | | **Flag to enable** | delete\_all\_deletes\_alarm | | **Flag to disable** | delete\_all\_preserves\_alarm | With the `delete_all_deletes_alarm` flag set, calling `deleteAll()` on a Durable Object's storage will delete any active alarm in addition to all stored data. Previously, `deleteAll()` only deleted user-stored data, and alarms required a separate `deleteAlarm()` call to remove. This change applies to both KV-backed and SQLite-backed Durable Objects. ### TextDecoder replaces lone surrogates | **Default as of** | 2026-02-24 | | ------------------- | ------------------------------------------- | | **Flag to enable** | text\_decoder\_replace\_surrogates | | **Flag to disable** | disable\_text\_decoder\_replace\_surrogates | When `text_decoder_replace_surrogates` is enabled, the UTF-16le `TextDecoder` will replace lone surrogates with U+FFFD (the Unicode replacement character) as required by the [Encoding Standard ↗](https://encoding.spec.whatwg.org/). Previously, lone surrogates were passed through unchanged, producing non-well-formed strings. ### Support iterables as fetch Request/Response body | **Default as of** | 2026-02-19 | | ------------------- | ---------------------------------- | | **Flag to enable** | fetch\_iterable\_type\_support | | **Flag to disable** | no\_fetch\_iterable\_type\_support | When `fetch_iterable_type_support` is enabled, sync and async iterables can be passed as the body of a `fetch()` `Request` or `Response` and will be properly iterated over. Previously, sync iterables like Arrays would be accepted but stringified (e.g., `[1, 2, 3]` would become `"1,2,3"`), and async iterables would be treated as regular objects and not iterated at all. With this flag enabled, iterables are properly consumed as streaming body content. Note that Arrays will now be treated as iterables instead of being stringified, which is a breaking change for code that relied on the previous behavior. ### Enable Node.js-compatible global timers | **Default as of** | 2026-02-10 | | ------------------- | ------------------------------ | | **Flag to enable** | enable\_nodejs\_global\_timers | | **Flag to disable** | no\_nodejs\_global\_timers | When `enable_nodejs_global_timers` is enabled, `setTimeout`, `setInterval`, `clearTimeout`, and `clearInterval` return Node.js-compatible `Timeout` objects with methods like `refresh()`, `ref()`, `unref()`, and `hasRef()`, matching the behavior of `node:timers`. This flag requires [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) to be enabled and is automatically enabled for Workers using a compatibility date of 2026-02-10 or later when `nodejs_compat` is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/timers.html) for more details about the timer APIs. ### Enable `node:dgram` module | **Default as of** | 2026-01-29 | | ------------------- | ------------------------------ | | **Flag to enable** | enable\_nodejs\_dgram\_module | | **Flag to disable** | disable\_nodejs\_dgram\_module | The `enable_nodejs_dgram_module` flag enables the `node:dgram` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-01-29 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/dgram.html) for more details about the `node:dgram` API. ### Enable `node:inspector` module | **Default as of** | 2026-01-29 | | ------------------- | ---------------------------------- | | **Flag to enable** | enable\_nodejs\_inspector\_module | | **Flag to disable** | disable\_nodejs\_inspector\_module | The `enable_nodejs_inspector_module` flag enables the `node:inspector` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-01-29 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/inspector.html) for more details about the `node:inspector` API. ### Enable `node:sqlite` module | **Default as of** | 2026-01-29 | | ------------------- | ------------------------------- | | **Flag to enable** | enable\_nodejs\_sqlite\_module | | **Flag to disable** | disable\_nodejs\_sqlite\_module | The `enable_nodejs_sqlite_module` flag enables the `node:sqlite` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-01-29 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/sqlite.html) for more details about the `node:sqlite` API. ### Enable `node:_stream_wrap` module | **Default as of** | 2026-01-29 | | ------------------- | ------------------------------------- | | **Flag to enable** | enable\_nodejs\_stream\_wrap\_module | | **Flag to disable** | disable\_nodejs\_stream\_wrap\_module | The `enable_nodejs_stream_wrap_module` flag enables the `node:_stream_wrap` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2026-01-29 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. ### `require()` returns default export | **Default as of** | 2026-01-22 | | ------------------- | --------------------------------- | | **Flag to enable** | require\_returns\_default\_export | | **Flag to disable** | require\_returns\_namespace | When `require_returns_default_export` is enabled, `require()` will return the default export of a module if it exists. If the default export does not exist, it falls back to returning a mutable copy of the module namespace object. This matches the behavior that Node.js uses for `require(esm)`, where the default export is returned when available. This flag is useful for frameworks like Next.js that expect to be able to patch module exports. Previously, `require()` always returned the module namespace object (an object like `{default: module.exports}`). ### Duplicate stubs in RPC params instead of transferring ownership | **Default as of** | 2026-01-20 | | ------------------- | ---------------------------- | | **Flag to enable** | rpc\_params\_dup\_stubs | | **Flag to disable** | rpc\_params\_transfer\_stubs | Changes the ownership semantics of RPC stubs embedded in the parameters of an RPC call, fixing compatibility issues with [Cap'n Web ↗](https://github.com/cloudflare/capnweb). When the [Workers RPC system](https://developers.cloudflare.com/workers/runtime-apis/rpc/) was first introduced, RPC stubs that were embedded in the params or return value of some other call had their ownership transferred. That is, the original stub was implicitly disposed, with a duplicate stub being delivered to the destination. This turns out to compose poorly with another rule: in the callee, any stubs received in the params of a call are automatically disposed when the call returns. These two rules combine to mean that if you proxy a call -- i.e. the implementation of an RPC just makes another RPC call passing along the same params -- then any stubs in the params get disposed twice. Worse, if the eventual recipient of the stub wants to keep a duplicate past the end of the call, this may not work because the copy of the stub in the proxy layer gets disposed anyway, breaking the connection. For this reason, the pure-JS implementation of Cap'n Web switched to saying that stubs in params do NOT transfer ownership -- they are simply duplicated. This compat flag fixes the Workers Runtime built-in RPC to match Cap'n Web behavior. One common use case that this fixes is clients that subscribe to callbacks from a Durable Object via Cap'n Web. In this use case, the client app passes a callback function over a Cap'n Web WebSocket to a stateless Worker, which in turn forwards the stub over Workers RPC to a Durable Object. The Durable Object stores a [dup()](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#the-dup-method) of the stub in order to call it back later to notify the client of events. Unfortunately, before this flag, this didn't work: as soon as the subscribe function itself returned, the Cap'n Web stub in the stateless worker would be disposed (because it was a parameter to a call that returned, and it was not `dup()`ed within the context of the stateless worker). Hence, when the Durable Object later tried to call the subscription callback, it would receive "Error: RPC stub used after being disposed", despite the fact that it had carefully `dup()`ed the stub at its end. ### Fetch iterable body respects toString/toPrimitive overrides | **Default as of** | 2026-01-15 | | ------------------- | -------------------------------------------------------- | | **Flag to enable** | fetch\_iterable\_type\_support\_override\_adjustment | | **Flag to disable** | no\_fetch\_iterable\_type\_support\_override\_adjustment | When `fetch_iterable_type_support_override_adjustment` is enabled, objects passed as the body of a `fetch()` `Request` or `Response` that are sync iterable but also have a custom `toString` or `Symbol.toPrimitive` method will not be treated as iterables. Instead, they will fall through to being handled as stringified objects, matching the previous behavior for such objects. This flag refines the behavior introduced by the `fetch_iterable_type_support` flag and is automatically enabled when `fetch_iterable_type_support` is enabled after 2026-01-15. ### Strip UTF-8 BOM in stream `readAllText()` | **Default as of** | 2026-01-13 | | ------------------- | ---------------------------------------- | | **Flag to enable** | strip\_bom\_in\_read\_all\_text | | **Flag to disable** | do\_not\_strip\_bom\_in\_read\_all\_text | When `strip_bom_in_read_all_text` is enabled, the `readAllText()` method on streams will strip a leading UTF-8 Byte Order Mark (BOM) if present, matching the expected behavior per web platform standards. Previously, the BOM was included in the returned string, which could cause unexpected behavior when parsing text content. ### Enable `node:cluster` module | **Default as of** | 2025-12-04 | | ------------------- | -------------------------------- | | **Flag to enable** | enable\_nodejs\_cluster\_module | | **Flag to disable** | disable\_nodejs\_cluster\_module | The `enable_nodejs_cluster_module` flag enables the `node:cluster` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-12-04 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/cluster.html) for more details about the `node:cluster` API. ### Enable `node:domain` module | **Default as of** | 2025-12-04 | | ------------------- | ------------------------------- | | **Flag to enable** | enable\_nodejs\_domain\_module | | **Flag to disable** | disable\_nodejs\_domain\_module | The `enable_nodejs_domain_module` flag enables the `node:domain` module stub in Workers. Note that `node:domain` is deprecated in Node.js itself. This flag is automatically enabled for Workers using a compatibility date of 2025-12-04 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/domain.html) for more details about the `node:domain` API. ### Enable `node:punycode` module | **Default as of** | 2025-12-04 | | ------------------- | --------------------------------- | | **Flag to enable** | enable\_nodejs\_punycode\_module | | **Flag to disable** | disable\_nodejs\_punycode\_module | The `enable_nodejs_punycode_module` flag enables the `node:punycode` module in Workers. Note that `node:punycode` is deprecated in Node.js itself. This flag is automatically enabled for Workers using a compatibility date of 2025-12-04 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/punycode.html) for more details about the `node:punycode` API. ### Enable `node:trace_events` module | **Default as of** | 2025-12-04 | | ------------------- | -------------------------------------- | | **Flag to enable** | enable\_nodejs\_trace\_events\_module | | **Flag to disable** | disable\_nodejs\_trace\_events\_module | The `enable_nodejs_trace_events_module` flag enables the `node:trace_events` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-12-04 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/tracing.html) for more details about the `node:trace_events` API. ### Enable `node:wasi` module | **Default as of** | 2025-12-04 | | ------------------- | ----------------------------- | | **Flag to enable** | enable\_nodejs\_wasi\_module | | **Flag to disable** | disable\_nodejs\_wasi\_module | The `enable_nodejs_wasi_module` flag enables the `node:wasi` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-12-04 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/wasi.html) for more details about the `node:wasi` API. ### Enable fast JSG struct optimization | **Default as of** | 2025-12-03 | | ------------------- | -------------------------- | | **Flag to enable** | enable\_fast\_jsg\_struct | | **Flag to disable** | disable\_fast\_jsg\_struct | When `enable_fast_jsg_struct` is enabled, internal struct types used by Workers runtime APIs are constructed using a more efficient pattern that reduces object creation time. However, optional fields will be explicitly set to `undefined` rather than being omitted from the object entirely, which is an observable behavior change. Code that checks for the presence of a property using `"key" in obj` or `Object.hasOwn(obj, "key")` may behave differently, since optional fields that were previously absent will now be present with a value of `undefined`. To check for a value, prefer `obj.key !== undefined` over `"key" in obj`. ### Enable ctx.exports | **Default as of** | 2025-11-17 | | ------------------- | --------------------- | | **Flag to enable** | enable\_ctx\_exports | | **Flag to disable** | disable\_ctx\_exports | This flag enables [the ctx.exports API](https://developers.cloudflare.com/workers/runtime-apis/context/#exports), which contains automatically-configured loopback bindings for your Worker's top-level exports. This allows you to skip configuring explicit bindings for your `WorkerEntrypoint`s and Durable Object namespaces defined in the same Worker. ### Automatic tracing | **Flag to enable** | enable\_workers\_observability\_tracing | | ------------------ | --------------------------------------- | This flag will enable [Workers Tracing](https://developers.cloudflare.com/workers/observability/traces/) by default if you have the following configured in your Wrangler configuration file: ```json { "observability": { "enabled": true } } ``` You can also explictly turn on automatic tracing without the flag and with older compatibility dates by setting the following: ```json { "observability": { "traces": { "enabled": true } } } ``` ### Enable `node:vm` module | **Default as of** | 2025-10-01 | | ------------------- | --------------------------- | | **Flag to enable** | enable\_nodejs\_vm\_module | | **Flag to disable** | disable\_nodejs\_vm\_module | The `enable_nodejs_vm_module` flag enables the `node:vm` module stub in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-10-01 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/vm.html) for more details about the `node:vm` API. ### Enable `node:console` module | **Default as of** | 2025-09-21 | | ------------------- | -------------------------------- | | **Flag to enable** | enable\_nodejs\_console\_module | | **Flag to disable** | disable\_nodejs\_console\_module | The `enable_nodejs_console_module` flag enables the `node:console` module in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-09-21 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/console.html) for more details about the `node:console` API. ### Enable workflow entrypoint validation | **Default as of** | 2025-09-20 | | ------------------- | --------------------------------------- | | **Flag to enable** | enable\_validate\_workflow\_entrypoint | | **Flag to disable** | disable\_validate\_workflow\_entrypoint | When `enable_validate_workflow_entrypoint` is enabled, additional validation checks are performed to ensure that [Workflows](https://developers.cloudflare.com/workflows/) are defined and used correctly. This helps catch configuration errors at upload time rather than at runtime. ### Enable `node:fs` module | **Default as of** | 2025-09-15 | | ------------------- | --------------------------- | | **Flag to enable** | enable\_nodejs\_fs\_module | | **Flag to disable** | disable\_nodejs\_fs\_module | The `enable_nodejs_fs_module` flag enables the `node:fs` module in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-09-15 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/fs.html) for more details about the `node:fs` API. ### Enable `node:os` module | **Default as of** | 2025-09-15 | | ------------------- | --------------------------- | | **Flag to enable** | enable\_nodejs\_os\_module | | **Flag to disable** | disable\_nodejs\_os\_module | The `enable_nodejs_os_module` flag enables the `node:os` module in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-09-15 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/os.html) for more details about the `node:os` API. ### Enable `process` v2 implementation | **Default as of** | 2025-09-15 | | ------------------- | ---------------------------- | | **Flag to enable** | enable\_nodejs\_process\_v2 | | **Flag to disable** | disable\_nodejs\_process\_v2 | When enabled after 2025-09-15, the `enable_nodejs_process_v2` flag along with the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) compat flag ensures a comprehensive Node.js-compatible `process` implementation, updating from the previous minimal process implementation that only provided the limited `nextTick`, `env`, `exit`, `getBuiltinModule`, `platform` and `features` properties. To continue using the previous minimal implementation after the compat date, set the `disable_nodejs_process_v2` flag instead. Most Node.js-supported process properties are implemented where possible, with undefined exports for unsupported features. See the [process documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs/process/) for Workers-specific implementation details. ### Enable Node.js HTTP server modules | **Default as of** | 2025-09-01 | | ------------------- | -------------------------------------- | | **Flag to enable** | enable\_nodejs\_http\_server\_modules | | **Flag to disable** | disable\_nodejs\_http\_server\_modules | The `enable_nodejs_http_server_modules` flag enables the availability of Node.js HTTP server modules such as `node:_http_server` in Workers. The `disable_nodejs_http_server_modules` flag disables the availability of these server modules. This enables compatibility with Node.js libraries and existing code that use the standard Node.js HTTP server APIs. The available functionality includes: * `http.createServer()` for creating HTTP servers * `http.Server` class for server instances * `http.ServerResponse` for handling server responses This flag must be used in combination with the `enable_nodejs_http_modules` flag to enable full features of `node:http`. This flag is automatically enabled for Workers using a compatibility date of 2025-09-01 or later when `nodejs_compat` is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/http.html) for more details about the Node.js HTTP APIs. ### Enable `node:http2` module | **Default as of** | 2025-09-01 | | ------------------- | ------------------------------ | | **Flag to enable** | enable\_nodejs\_http2\_module | | **Flag to disable** | disable\_nodejs\_http2\_module | The `enable_nodejs_http2_module` flag enables the `node:http2` module stubs in Workers. This flag is automatically enabled for Workers using a compatibility date of 2025-09-01 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/http2.html) for more details about the `node:http2` API. ### Remove end-of-life Node.js APIs | **Default as of** | 2025-09-01 | | ------------------- | --------------------------- | | **Flag to enable** | remove\_nodejs\_compat\_eol | | **Flag to disable** | add\_nodejs\_compat\_eol | When `remove_nodejs_compat_eol` is enabled, APIs that have reached End-of-Life in Node.js will be removed for Workers. When disabled, the APIs are present but might still be non-functional stubs. This flag is a roll-up flag. As additional APIs reach EOL in specific Node.js versions, new version-specific compat flags are added (such as `remove_nodejs_compat_eol_v22`, `remove_nodejs_compat_eol_v23`, and `remove_nodejs_compat_eol_v24`) that are implied by this flag after their respective dates. This flag is automatically enabled for Workers using a compatibility date of 2025-09-01 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. ### Remove Node.js 23.x end-of-life APIs | **Default as of** | 2025-09-01 | | ------------------- | -------------------------------- | | **Flag to enable** | remove\_nodejs\_compat\_eol\_v23 | | **Flag to disable** | add\_nodejs\_compat\_eol\_v23 | When `remove_nodejs_compat_eol_v23` is enabled, APIs that reached end-of-life in Node.js 23.x (EOL June 2025) are removed. This flag is automatically enabled when the `remove_nodejs_compat_eol_v24` flag is enabled after 2025-09-01. ### Strip Authorization header on cross-origin redirects | **Default as of** | 2025-09-01 | | ------------------- | -------------------------------------------------- | | **Flag to enable** | strip\_authorization\_on\_cross\_origin\_redirect | | **Flag to disable** | retain\_authorization\_on\_cross\_origin\_redirect | When `strip_authorization_on_cross_origin_redirect` is enabled, the `Authorization` header is automatically removed when following a redirect to a different origin. This behavior is required by the current [Fetch API specification ↗](https://fetch.spec.whatwg.org/). This requirement was added to the Fetch spec in 2022, after Cloudflare Workers originally implemented its fetch handling. Workers did not originally implement this requirement, so the new behavior is gated behind a compatibility flag. The old behavior was not inherently insecure, and could be desirable in some circumstances. For example, if an API that requires authorization wishes to redirect to a new hostname while having the client send along their credentials. Under the new behavior, such a redirect will not include credentials automatically. However, the old behavior could lead to unintentional credential leakage when redirecting to untrusted origins. To retain the old behavior, set the `retain_authorization_on_cross_origin_redirect` flag. ### Enable availability of `node:http` and `node:https` modules | **Default as of** | 2025-08-15 | | ------------------- | ------------------------------ | | **Flag to enable** | enable\_nodejs\_http\_modules | | **Flag to disable** | disable\_nodejs\_http\_modules | The `enable_nodejs_http_modules` flag enables the availability of Node.js `node:http` and `node:https` modules in Workers (client APIS only). The `disable_nodejs_http_modules` flag disables the availability of these modules. This enables compatibility with Node.js libraries and existing code that use the standard node:http and node:https APIs for making HTTP requests. The available functionality includes: * `http.request()` and `https.request()` for making HTTP/HTTPS requests * `http.get()` and `https.get()` for making GET requests * Request and response objects with standard Node.js APIs * Support for standard HTTP methods, headers, and options See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/http.html)for more details about the Node.js APIs. ### Expose global MessageChannel and MessagePort | **Default as of** | 2025-08-15 | | ------------------- | ------------------------------------ | | **Flag to enable** | expose\_global\_message\_channel | | **Flag to disable** | no\_expose\_global\_message\_channel | When the `expose_global_message_channel` flag is set, Workers will expose the `MessageChannel` and `MessagePort` constructors globally. When the `no_expose_global_message_channel` flag is set, Workers will not expose these. ### Disable global handlers for Python Workers | **Default as of** | 2025-08-14 | | ------------------- | ------------------------------------- | | **Flag to enable** | python\_no\_global\_handlers | | **Flag to disable** | disable\_python\_no\_global\_handlers | When the `python_no_global_handlers` flag is set, Python Workers will disable the global handlers and enforce their use via default entrypoint classes. ### Enable `cache: no-cache` HTTP standard API | **Default as of** | 2025-08-07 | | ------------------- | -------------------------- | | **Flag to enable** | cache\_no\_cache\_enabled | | **Flag to disable** | cache\_no\_cache\_disabled | When you enable the `cache_no_cache_enabled` compatibility flag, you can specify the `no-cache`value for the `cache` property of the Request interface. When this compatibility flag is not enabled, or `cache_option_disabled` is set, the Workers runtime will throw a `TypeError` saying `Unsupported cache mode: no-cache`. When this flag is enabled you can instruct Cloudflare to force its cache to revalidate the response from a subrequest you make from your Worker using the [fetch()API](https://developers.cloudflare.com/workers/runtime-apis/fetch/): When `no-cache` is specified: * All requests have the headers `Pragma: no-cache` and `Cache-Control: no-cache` are set on them. * Subrequests to origins not hosted by Cloudflare force Cloudflare's cache to revalidate with the origin. Revalidating with the origin means that the Worker request will first look for a match in Cloudflare's cache, then: * If there is a match, a conditional request is sent to the origin, regardless of whether or not the match is fresh or stale. If the resource has not changed, the cached version is returned. If the resource has changed, it will be downloaded from the origin, updated in the cache, and returned. * If there is no match, Workers will make a standard request to the origin and cache the response. Examples using `cache: 'no-cache'`: ```js const response = await fetch("https://example.com", { cache: "no-cache" }); ``` The cache value can also be set on a `Request` object. ```js const request = new Request("https://example.com", { cache: "no-cache" }); const response = await fetch(request); ``` ### Set the `this` value of EventTarget event handlers | **Default as of** | 2025-08-01 | | ------------------- | ---------------------------- | | **Flag to enable** | set\_event\_target\_this | | **Flag to disable** | no\_set\_event\_target\_this | When the `set_event_target_this` flag is se, Workers will set the `this` value of event handlers to the `EventTarget` instance that the event is being dispatched on. This is compliant with the specification. When then `no_set_event_target_this` flag is set, Workers will not set the `this` value of event handlers, and it will be `undefined` instead. ### Set forwardable email full headers | **Default as of** | 2025-08-01 | | ------------------- | ---------------------------------------- | | **Flag to enable** | set\_forwardable\_email\_full\_headers | | **Flag to disable** | set\_forwardable\_email\_single\_headers | The original version of the headers sent to edgeworker were truncated to a single value for specific header names, such as To and Cc. With the `set_forwardable_email_full_headers` flag set, Workers will receive the full header values to the worker script. ### Pedantic Web Platform Tests (WPT) compliance | **Flag to enable** | pedantic\_wpt | | ------------------- | ------------------ | | **Flag to disable** | non\_pedantic\_wpt | The `pedantic_wpt` flag enables strict compliance with Web Platform Tests (WPT) in Workers. Initially this only effects `Event` and `EventTarget` APIs but will be expanded to other APIs in the future. There is no default enable date for this flag. ### Bind AsyncLocalStorage snapshots to the request | **Default as of** | 2025-06-16 | | ------------------- | ---------------------------------------------- | | **Flag to enable** | bind\_asynclocalstorage\_snapshot\_to\_request | | **Flag to disable** | do\_not\_bind\_asynclocalstorage\_snapshot\_to | The AsyncLocalStorage frame can capture values that are bound to the current request context. This is not always in the users control since we use the ALS storage frame to propagate internal trace spans as well as user-provided values. When the `bind_asynclocalstorage_snapshot_to_request`flag is set, the runtime binds the snapshot / bound functions to the current request context and will throw an error if the bound functions are called outside of the request in which they were created. The `do_not_bind_asynclocalstorage_snapshot_to` flag disables this behavior. ### Throw on unrecognized import assertions | **Default as of** | 2025-06-16 | | ------------------- | ------------------------------------------ | | **Flag to enable** | throw\_on\_unrecognized\_import\_assertion | | **Flag to disable** | ignore\_unrecognized\_import\_assertion | The `throw_on_unrecognized_import_assertion` flag controls how Workers handle import attributes that are not recognized by the runtime. Previously, Workers would ignore all import attributes, which is not compliant with the specification. Runtimes are expected to throw an error when an import attribute is encountered that is not recognized. When the `ignore_unrecognized_import_assertion` flag is set, Workers will ignore unrecognized import attributes. ### Enable eval during startup | **Default as of** | 2025-06-01 | | ------------------- | ------------------------------- | | **Flag to enable** | allow\_eval\_during\_startup | | **Flag to disable** | disallow\_eval\_during\_startup | When the `allow_eval_during_startup` flag is set, Workers can use `eval()`and `new Function(text)` during the startup phase of a Worker script. This allows for dynamic code execution at the beginning of a Worker lifecycle. When the `disallow_eval_during_startup` flag is set, using `eval()` or `new Function(text)` during the startup phase will throw an error. ### Enable `Request.signal` for incoming requests | **Flag to enable** | enable\_request\_signal | | ------------------- | ------------------------ | | **Flag to disable** | disable\_request\_signal | When you use the `enable_request_signal` compatibility flag, you can attach an event listener to [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) objects, using the [signal property ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request/signal). This allows you to perform tasks when the request to your Worker is canceled by the client. ### Cache API request `cf` overrides cache rules | **Default as of** | 2025-05-19 | | ------------------- | ---------------------------------------------------- | | **Flag to enable** | cache\_api\_request\_cf\_overrides\_cache\_rules | | **Flag to disable** | no\_cache\_api\_request\_cf\_overrides\_cache\_rules | When `cache_api_request_cf_overrides_cache_rules` is enabled, cache settings specified in the `cf` object of a request passed to the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) will override cache rules. This applies only to user-owned or grey-clouded sites. This is the Cache API counterpart to the [request\_cf\_overrides\_cache\_rules](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#fetch-api-request-cf-overrides-cache-rules) flag, which applies to the `fetch()` API. ### Enable `navigator.language` | **Default as of** | 2025-05-19 | | ------------------- | ---------------------------- | | **Flag to enable** | enable\_navigator\_language | | **Flag to disable** | disable\_navigator\_language | When the `enable_navigator_language` flag is set, the `navigator.language` property will be available in Workers. For now, the value of `navigator.language` will always be `en`. When the `disable_navigator_language` flag is set, the `navigator.language` property will not be available. ### Disallowing importable environment | **Flag to enable** | disallow\_importable\_env | | ------------------- | ------------------------- | | **Flag to disable** | allow\_importable\_env | When the `disallow_importable_env` flag is enabled, Workers will not allow importing the environment variables via the `cloudflare:workers` module and will not populate the environment variables in the global `process.env` object when Node.js compatibility is enabled. There is no default enabled date for this flag. ### Enable `FinalizationRegistry` and `WeakRef` | **Default as of** | 2025-05-05 | | ------------------- | ------------------ | | **Flag to enable** | enable\_weak\_ref | | **Flag to disable** | disable\_weak\_ref | Enables the use of [FinalizationRegistry ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/FinalizationRegistry) and [WeakRef ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/WeakRef) built-ins. * `FinalizationRegistry` allows you to register a cleanup callback that runs after an object has been garbage-collected. * `WeakRef` creates a weak reference to an object, allowing it to be garbage-collected if no other strong references exist. :::note\[Behaviour\] `FinalizationRegistry` cleanup callbacks may execute at any point during your request lifecycle, even after your invoked handler has completed (similar to `ctx.waitUntil()`). These callbacks do not have an associated async context. You cannot perform any I/O within them, including emitting events to a tail Worker. ::: :::caution These APIs are fundamentally non-deterministic. The timing and execution of garbage collection are unpredictable, and you **should not rely on them for essential program logic**. Additionally, cleanup callbacks registered with `FinalizationRegistry` may **never be executed**, including but not limited to cases where garbage collection is not triggered, or your Worker gets evicted. ::: ### Passthrough AbortSignal of incoming request to subrequests | **Flag to enable** | request\_signal\_passthrough | | ------------------- | -------------------------------- | | **Flag to disable** | no\_request\_signal\_passthrough | When the `request_signal_passthrough` flag set, the `AbortSignal` of an incoming request will be passed through to subrequests when the request is forwarded to a subrequest using the `fetch()` API. The the `no_request_signal_passthrough` flag is set, the `AbortSignal` of the incoming request will not be passed through. ### Spec-compliant URLPattern implementation | **Default as of** | 2025-05-01 | | ------------------- | -------------------- | | **Flag to enable** | urlpattern\_standard | | **Flag to disable** | urlpattern\_original | The original `URLPattern` implementation was not fully compliant with the [WHATWG URLPattern Standard ↗](https://urlpattern.spec.whatwg.org/), leading to a number of issues reported by users. With `urlpattern_standard` enabled, Workers uses a spec-compliant URLPattern implementation. This is a breaking change from the original behavior, so it is gated behind a compatibility flag. If you are using `URLPattern` and encounter unexpected behavior changes after updating your compatibility date, you can set `urlpattern_original` to revert to the previous implementation. ### Navigation requests prefer asset serving | **Default as of** | 2025-04-01 | | ------------------- | ------------------------------------------- | | **Flag to enable** | assets\_navigation\_prefers\_asset\_serving | | **Flag to disable** | assets\_navigation\_has\_no\_effect | For Workers with [static assets](https://developers.cloudflare.com/workers/static-assets/) and this compatibility flag enabled, navigation requests (requests which have a `Sec-Fetch-Mode: navigate` header) will prefer to be served by our asset-serving logic, even when an exact asset match cannot be found. This is particularly useful for applications which operate in either [Single Page Application (SPA) mode](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/) or have [custom 404 pages](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/#custom-404-pages), as this now means the fallback pages of `200 /index.html` and `404 /404.html` will be served ahead of invoking a Worker script and will therefore avoid incurring a charge. Without this flag, the runtime will continue to apply the old behavior of invoking a Worker script (if present) for any requests which do not exactly match a static asset. When `assets.run_worker_first = true` is set, this compatibility flag has no effect. The `assets.run_worker_first = true` setting ensures the Worker script executes before any asset-serving logic. ### Enable auto-populating `process.env` | **Default as of** | 2025-04-01 | | ------------------- | ----------------------------------------------- | | **Flag to enable** | nodejs\_compat\_populate\_process\_env | | **Flag to disable** | nodejs\_compat\_do\_not\_populate\_process\_env | When you enable the `nodejs_compat_populate_process_env` compatibility flag and the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/)flag is also enabled, `process.env` will be populated with values from any bindings with text or JSON values. This means that if you have added [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/), [secrets](https://developers.cloudflare.com/workers/configuration/secrets/), or [version metadata](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/)bindings, these values can be accessed on `process.env`. ```js const apiClient = ApiClient.new({ apiKey: process.env.API_KEY }); const LOG_LEVEL = process.env.LOG_LEVEL || "info"; ``` This makes accessing these values easier and conforms to common Node.js patterns, which can reduce toil and help with compatibility for existing Node.js libraries. If users do not wish for these values to be accessible via `process.env`, they can use the `nodejs_compat_do_not_populate_process_env` flag. In this case, `process.env` will still be available, but will not have values automatically added. If the `disallow_importable_env` compatibility flag is set, the `process.env` will also not be populated. ### Queue consumers don't wait for `ctx.waitUntil()` to resolve | **Flag to enable** | queue\_consumer\_no\_wait\_for\_wait\_until | | ------------------ | ------------------------------------------- | By default, [Queues](https://developers.cloudflare.com/queues/) Consumer Workers acknowledge messages only after promises passed to [ctx.waitUntil()](https://developers.cloudflare.com/workers/runtime-apis/context) have resolved. This behavior can cause queue consumers which utilize `ctx.waitUntil()` to process messages slowly. The default behavior is documented in the [Queues Consumer Configuration Guide](https://developers.cloudflare.com/queues/configuration/javascript-apis#consumer). This Consumer Worker is an example of a Worker which utilizes `ctx.waitUntil()`. Under the default behavior, this consumer Worker will only acknowledge a batch of messages after the sleep function has resolved. ```js export default { async fetch(request, env, ctx) { // omitted }, async queue(batch, env, ctx) { console.log(`received batch of ${batch.messages.length} messages to queue ${batch.queue}`); for (let i = 0; i < batch.messages.length; ++i) { console.log(`message #${i}: ${JSON.stringify(batch.messages[i])}`); } ctx.waitUntil(sleep(30 * 1000)); } }; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } ``` If the `queue_consumer_no_wait_for_wait_until` flag is enabled, Queues consumers will no longer wait for promises passed to `ctx.waitUntil()` to resolve before acknowledging messages. This can improve the performance of queue consumers which utilize `ctx.waitUntil()`. With the flag enabled, in the above example, the consumer Worker will acknowledge the batch without waiting for the sleep function to resolve. Using this flag will not affect the behavior of `ctx.waitUntil()`. `ctx.waitUntil()` will continue to extend the lifetime of your consumer Worker to continue to work even after the batch of messages has been acknowledged. ### Apply TransformStream backpressure fix | **Default as of** | 2024-12-16 | | ------------------- | -------------------------------------- | | **Flag to enable** | fixup-transform-stream-backpressure | | **Flag to disable** | original-transform-stream-backpressure | The original implementation of `TransformStream` included a bug that would cause backpressure signaling to fail after the first write to the transform. Unfortunately, the fix can cause existing code written to address the bug to fail. Therefore, the `fixup-transform-stream-backpressure` compat flag is provided to enable the fix. The fix is enabled by default with compatibility dates of 2024-12-16 or later. To restore the original backpressure logic, disable the fix using the `original-transform-stream-backpressure` flag. ### Disable top-level await in require(...) | **Default as of** | 2024-12-02 | | ------------------- | --------------------------------------- | | **Flag to enable** | disable\_top\_level\_await\_in\_require | | **Flag to disable** | enable\_top\_level\_await\_in\_require | Workers implements the ability to use the Node.js style `require(...)` method to import modules in the Worker bundle. Historically, this mechanism allowed required modules to use top-level await. This, however, is not Node.js compatible. The `disable_top_level_await_in_require` compat flag will cause `require()`to fail if the module uses a top-level await. This flag is default enabled with a compatibility date of 2024-12-02 or later. To restore the original behavior allowing top-level await, use the `enable_top_level_await_in_require` compatibility flag. ### Enable `cache: no-store` HTTP standard API | **Default as of** | 2024-11-11 | | ------------------- | ----------------------- | | **Flag to enable** | cache\_option\_enabled | | **Flag to disable** | cache\_option\_disabled | When you enable the `cache_option_enabled` compatibility flag, you can specify a value for the `cache` property of the Request interface. When this compatibility flag is not enabled, or `cache_option_disabled` is set, the Workers runtime will throw an `Error` saying `The 'cache' field on 'RequestInitializerDict' is not implemented.` When this flag is enabled you can instruct Cloudflare not to cache the response from a subrequest you make from your Worker using the [fetch() API](https://developers.cloudflare.com/workers/runtime-apis/fetch/): The only cache option enabled with `cache_option_enabled` is `'no-store'`. Specifying any other value will cause the Workers runtime to throw a `TypeError` with the message `Unsupported cache mode: `. When `no-store` is specified: * All requests have the headers `Pragma: no-cache` and `Cache-Control: no-cache` are set on them. * Subrequests to origins not hosted by Cloudflare bypass Cloudflare's cache. Examples using `cache: 'no-store'`: ```js const response = await fetch("https://example.com", { cache: "no-store" }); ``` The cache value can also be set on a `Request` object. ```js const request = new Request("https://example.com", { cache: "no-store" }); const response = await fetch(request); ``` ### Global fetch() strictly public | **Flag to enable** | global\_fetch\_strictly\_public | | ------------------- | ------------------------------- | | **Flag to disable** | global\_fetch\_private\_origin | When the `global_fetch_strictly_public` compatibility flag is enabled, the global [fetch() function](https://developers.cloudflare.com/workers/runtime-apis/fetch/) will strictly route requests as if they were made on the public Internet. This means requests to a Worker's own zone will loop back to the "front door" of Cloudflare and will be treated like a request from the Internet, possibly even looping back to the same Worker again. When the `global_fetch_strictly_public` is not enabled, such requests are routed to the zone's origin server, ignoring any Workers mapped to the URL and also bypassing Cloudflare security settings. ### Handle cross-request promise resolution correctly | **Default as of** | 2024-10-14 | | ------------------- | ----------------------------------------------- | | **Flag to enable** | handle\_cross\_request\_promise\_resolution | | **Flag to disable** | no\_handle\_cross\_request\_promise\_resolution | Historically, it was possible to resolve a promise from an incorrect request context, which could lead to promise continuations being scheduled in the wrong context, causing errors and difficult-to-diagnose bugs. With `handle_cross_request_promise_resolution` enabled, promise continuations are scheduled to run in the correct request context if it is still alive, or dropped with a warning if the correct context has already ended. ### Upper-case HTTP methods | **Default as of** | 2024-10-14 | | ------------------- | ----------------------------------- | | **Flag to enable** | upper\_case\_all\_http\_methods | | **Flag to disable** | no\_upper\_case\_all\_http\_methods | HTTP methods are expected to be upper-cased. Per the fetch spec, if the method is specified as `get`, `post`, `put`, `delete`, `head`, or `options`, implementations are expected to uppercase the method. All other method names would generally be expected to throw as unrecognized (for example, `patch` would be an error while `PATCH` is accepted). This is a bit restrictive, even if it is in the spec. This flag modifies the behavior to uppercase all methods prior to parsing so that the method is always recognized if it is a known method. To restore the standard behavior, use the `no_upper_case_all_http_methods`compatibility flag. ### Automatically set the Symbol.toStringTag for Workers API objects | **Default as of** | 2024-09-26 | | ------------------- | --------------------------- | | **Flag to enable** | set\_tostring\_tag | | **Flag to disable** | do\_not\_set\_tostring\_tag | A change was made to set the Symbol.toStringTag on all Workers API objects in order to fix several spec compliance bugs. Unfortunately, this change was more breaking than anticipated. The `do_not_set_tostring_tag` compat flag restores the original behavior with compatibility dates of 2024-09-26 or earlier. ### Enable `node:zlib` module | **Default as of** | 2024-09-23 | | ------------------- | ---------------- | | **Flag to enable** | nodejs\_zlib | | **Flag to disable** | no\_nodejs\_zlib | The `nodejs_zlib` flag enables the `node:zlib` module in Workers. This flag is automatically enabled for Workers using a compatibility date of 2024-09-23 or later when [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled. See the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/zlib.html) for more details about the `node:zlib` API. ### Allow specifying a custom port when making a subrequest with the fetch() API | **Default as of** | 2024-09-02 | | ------------------- | --------------------- | | **Flag to enable** | allow\_custom\_ports | | **Flag to disable** | ignore\_custom\_ports | When this flag is enabled, and you specify a port when making a subrequest with the [fetch() API](https://developers.cloudflare.com/workers/runtime-apis/fetch/), the port number you specify will be used. When you make a subrequest to a website that uses Cloudflare ("Orange Clouded") — only [ports supported by Cloudflare's reverse proxy](https://developers.cloudflare.com/fundamentals/reference/network-ports/#network-ports-compatible-with-cloudflares-proxy) can be specified. If you attempt to specify an unsupported port, it will be ignored. When you make a subrequest to a website that does not use Cloudflare ("Grey Clouded") - any port can be specified. For example: ```js const response = await fetch("https://example.com:8000"); ``` With allow\_custom\_ports the above example would fetch `https://example.com:8000` rather than `https://example.com:443`. Note that creating a WebSocket client with a call to `new WebSocket(url)` will also obey this flag. ### WritableStream abort clears pending write queue | **Default as of** | 2024-09-02 | | ------------------- | ---------------------------------------------------------- | | **Flag to enable** | internal\_writable\_stream\_abort\_clears\_queue | | **Flag to disable** | internal\_writable\_stream\_abort\_does\_not\_clear\_queue | When using the original WritableStream implementation ("internal" streams), the `abort()` operation was previously handled lazily, meaning that the queue of pending writes would not be cleared until the next time the queue was processed. This behavior could cause the stream to hang if the consumer stopped consuming. With `internal_writable_stream_abort_clears_queue` enabled, the queue is cleared immediately upon `abort()`, preventing hangs in cases where the consumer has stopped processing writes. ### Properly extract blob MIME type from `content-type` headers | **Default as of** | 2024-06-03 | | ------------------- | -------------------------- | | **Flag to enable** | blob\_standard\_mime\_type | | **Flag to disable** | blob\_legacy\_mime\_type | When calling `response.blob.type()`, the MIME type will now be properly extracted from `content-type` headers, per the [WHATWG spec ↗](https://fetch.spec.whatwg.org/#concept-header-extract-mime-type). ### Use standard URL parsing in `fetch()` | **Default as of** | 2024-06-03 | | ------------------- | -------------------- | | **Flag to enable** | fetch\_standard\_url | | **Flag to disable** | fetch\_legacy\_url | The `fetch_standard_url` flag makes `fetch()` use [WHATWG URL Standard ↗](https://url.spec.whatwg.org/) parsing rules. The original implementation would throw `TypeError: Fetch API cannot load` errors with some URLs where standard parsing does not, for instance with the inclusion of whitespace before the URL. URL errors will now be thrown immediately upon calling `new Request()` with an improper URL. Previously, URL errors were thrown only once `fetch()` was called. ### Returning empty Uint8Array on final BYOB read | **Default as of** | 2024-05-13 | | ------------------- | ----------------------------------------- | | **Flag to enable** | internal\_stream\_byob\_return\_view | | **Flag to disable** | internal\_stream\_byob\_return\_undefined | In the original implementation of BYOB ("Bring your own buffer") `ReadableStreams`, the `read()` method would return `undefined` when the stream was closed and there was no more data to read. This behavior was inconsistent with the standard `ReadableStream` behavior, which returns an empty `Uint8Array` when the stream is closed. When the `internal_stream_byob_return_view` flag is used, the BYOB `read()` will implement standard behavior. ```js const resp = await fetch('https://example.org'); const reader = resp.body.getReader({ mode: 'byob' }); await result = await reader.read(new Uint8Array(10)); if (result.done) { // The result gives us an empty Uint8Array... console.log(result.value.byteLength); // 0 // However, it is backed by the same underlying memory that was passed // into the read call. console.log(result.value.buffer.byteLength); // 10 } ``` ### Brotli Content-Encoding support | **Default as of** | 2024-04-29 | | ------------------- | ----------------------------- | | **Flag to enable** | brotli\_content\_encoding | | **Flag to disable** | no\_brotli\_content\_encoding | When the `brotli_content_encoding` compatibility flag is enabled, Workers supports the `br` content encoding and can request and respond with data encoded using the [Brotli ↗](https://developer.mozilla.org/en-US/docs/Glossary/Brotli%5Fcompression) compression algorithm. This reduces the amount of data that needs to be fetched and can be used to pass through the original compressed data to the client. See the Fetch API [documentation](https://developers.cloudflare.com/workers/runtime-apis/fetch/#how-the-accept-encoding-header-is-handled) for details. ### Durable Object stubs and Service Bindings support RPC | **Default as of** | 2024-04-03 | | ------------------- | ---------- | | **Flag to enable** | rpc | | **Flag to disable** | no\_rpc | With this flag on, [Durable Object](https://developers.cloudflare.com/durable-objects/) stubs and [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) support [RPC](https://developers.cloudflare.com/workers/runtime-apis/rpc/). This means that these objects now appear as if they define every possible method name. Calling any method name sends an RPC to the remote Durable Object or Worker service. For most applications, this change will have no impact unless you use it. However, it is possible some existing code will be impacted if it explicitly checks for the existence of method names that were previously not defined on these types. For example, we have seen code in the wild which iterates over [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) and tries to auto-detect their types based on what methods they implement. Such code will now see service bindings as implementing every method, so may misinterpret service bindings as being some other type. In the cases we have seen, the impact was benign (nothing actually broke), but out of caution we are guarding this change behind a flag. ### Handling custom thenables | **Default as of** | 2024-04-01 | | ------------------- | ----------------------------- | | **Flag to enable** | unwrap\_custom\_thenables | | **Flag to disable** | no\_unwrap\_custom\_thenables | With the `unwrap_custom_thenables` flag set, various Workers APIs that accept promises will also correctly handle custom thenables (objects with a `then` method) that are not native promises, but are intended to be treated as such). For example, the `waitUntil` method of the `ExecutionContext`object will correctly handle custom thenables, allowing them to be used in place of native promises. ```js async fetch(req, env, ctx) { ctx.waitUntil({ then(res) { // Resolve the thenable after 1 second setTimeout(res, 1000); } }); // ... } ``` ### Fetchers no longer have get/put/delete helper methods | **Default as of** | 2024-03-26 | | ------------------- | ------------------------------ | | **Flag to enable** | fetcher\_no\_get\_put\_delete | | **Flag to disable** | fetcher\_has\_get\_put\_delete | [Durable Object](https://developers.cloudflare.com/durable-objects/) stubs and [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) both implement a `fetch()` method which behaves similarly to the global `fetch()` method, but requests are instead sent to the destination represented by the object, rather than being routed based on the URL. Historically, API objects that had such a `fetch()` method also had methods `get()`, `put()`, and `delete()`. These methods were thin wrappers around `fetch()` which would perform the corresponding HTTP method and automatically handle writing/reading the request/response bodies as needed. These methods were a very early idea from many years ago, but were never actually documented, and therefore rarely (if ever) used. Enabling the `fetcher_no_get_put_delete`, or setting a compatibility date on or after `2024-03-26` disables these methods for your Worker. This change paves a future path for you to be able to define your own custom methods using these names. Without this change, you would be unable to define your own `get`, `put`, and `delete` methods, since they would conflict with these built-in helper methods. ### Queues send messages in `JSON` format | **Default as of** | 2024-03-18 | | ------------------- | -------------------------- | | **Flag to enable** | queues\_json\_messages | | **Flag to disable** | no\_queues\_json\_messages | With the `queues_json_messages` flag set, Queue bindings will serialize values passed to `send()` or `sendBatch()` into JSON format by default (when no specific `contentType` is provided). ### Suppress global `importScripts()` | **Default as of** | 2024-03-04 | | ------------------- | ------------------------- | | **Flag to enable** | no\_global\_importscripts | | **Flag to disable** | global\_importscripts | Suppresses the global `importScripts()` function. This method was included in the Workers global scope but was marked explicitly as non-implemented. However, the presence of the function could cause issues with some libraries. This compatibility flag removes the function from the global scope. ### Node.js AsyncLocalStorage | **Flag to enable** | nodejs\_als | | ------------------- | --------------- | | **Flag to disable** | no\_nodejs\_als | Enables the availability of the Node.js [AsyncLocalStorage ↗](https://nodejs.org/api/async%5Fhooks.html#async%5Fhooks%5Fclass%5Fasynclocalstorage) API in Workers. ### Python Workers | **Flag to enable** | python\_workers | | ------------------ | --------------- | This flag enables first class support for Python. [Python Workers](https://developers.cloudflare.com/workers/languages/python/) implement the majority of Python's [standard library](https://developers.cloudflare.com/workers/languages/python/stdlib), support all [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings), [environment variable](https://developers.cloudflare.com/workers/configuration/environment-variables), and [secrets](https://developers.cloudflare.com/workers/configuration/secrets), and integration with JavaScript objects and functions via a [foreign function interface](https://developers.cloudflare.com/workers/languages/python/ffi). ### WebCrypto preserve publicExponent field | **Default as of** | 2023-12-01 | | ------------------- | -------------------------------------- | | **Flag to enable** | crypto\_preserve\_public\_exponent | | **Flag to disable** | no\_crypto\_preserve\_public\_exponent | In the WebCrypto API, the `publicExponent` field of the algorithm of RSA keys would previously be an `ArrayBuffer`. Using this flag, `publicExponent` is a `Uint8Array` as mandated by the specification. ### `Vectorize` query with metadata optionally returned | **Default as of** | 2023-11-08 | | ------------------- | ------------------------------------ | | **Flag to enable** | vectorize\_query\_metadata\_optional | | **Flag to disable** | vectorize\_query\_original | A set value on `vectorize_query_metadata_optional` indicates that the Vectorize query operation should accept newer arguments with `returnValues` and `returnMetadata` specified discretely over the older argument `returnVectors`. This also changes the return format. If the vector values have been indicated for return, the return value is now a flattened vector object with `score` attached where it previously contained a nested vector object. ### WebSocket Compression | **Default as of** | 2023-08-15 | | ------------------- | ---------------------------- | | **Flag to enable** | web\_socket\_compression | | **Flag to disable** | no\_web\_socket\_compression | The Workers runtime did not support WebSocket compression when the initial WebSocket implementation was released. Historically, the runtime has stripped or ignored the `Sec-WebSocket-Extensions` header -- but is now capable of fully complying with the WebSocket Compression RFC. Since many clients are likely sending `Sec-WebSocket-Extensions: permessage-deflate` to their Workers today (`new WebSocket(url)` automatically sets this in browsers), we have decided to maintain prior behavior if this flag is absent. If the flag is present, the Workers runtime is capable of using WebSocket Compression on both inbound and outbound WebSocket connections. Like browsers, calling `new WebSocket(url)` in a Worker will automatically set the `Sec-WebSocket-Extensions: permessage-deflate` header. If you are using the non-standard `fetch()` API to obtain a WebSocket, you can include the `Sec-WebSocket-Extensions` header with value `permessage-deflate` and include any of the compression parameters defined in [RFC-7692 ↗](https://datatracker.ietf.org/doc/html/rfc7692#section-7). ### Strict crypto error checking | **Default as of** | 2023-08-01 | | ------------------- | -------------------------- | | **Flag to enable** | strict\_crypto\_checks | | **Flag to disable** | no\_strict\_crypto\_checks | Perform additional error checking in the Web Crypto API to conform with the specification and reject possibly unsafe key parameters: * For RSA key generation, key sizes are required to be multiples of 128 bits as boringssl may otherwise truncate the key. * The size of imported RSA keys must be at least 256 bits and at most 16384 bits, as with newly generated keys. * The public exponent for imported RSA keys is restricted to the commonly used values `[3, 17, 37, 65537]`. * In conformance with the specification, an error will be thrown when trying to import a public ECDH key with non-empty usages. ### Strict compression error checking | **Default as of** | 2023-08-01 | | ------------------- | ------------------------------- | | **Flag to enable** | strict\_compression\_checks | | **Flag to disable** | no\_strict\_compression\_checks | Perform additional error checking in the Compression Streams API and throw an error if a `DecompressionStream` has trailing data or gets closed before the full compressed data has been provided. ### Override cache rules cache settings in `request.cf` object for Fetch API | **Default as of** | 2025-04-02 | | ------------------- | ---------------------------------------- | | **Flag to enable** | request\_cf\_overrides\_cache\_rules | | **Flag to disable** | no\_request\_cf\_overrides\_cache\_rules | This flag changes the behavior of cache when requesting assets via the [Fetch API](https://developers.cloudflare.com/workers/runtime-apis/fetch). Cache settings specified in the `request.cf` object, such as `cacheEverything` and `cacheTtl`, are now given precedence over any [Cache Rules](https://developers.cloudflare.com/cache/how-to/cache-rules/) set. ### Bot Management data | **Default as of** | 2023-08-01 | | ------------------- | ------------------------------ | | **Flag to enable** | no\_cf\_botmanagement\_default | | **Flag to disable** | cf\_botmanagement\_default | This flag streamlines Workers requests by reducing unnecessary properties in the `request.cf` object. With the flag enabled - either by default after 2023-08-01 or by setting the `no_cf_botmanagement_default` flag - Cloudflare will only include the [Bot Management object](https://developers.cloudflare.com/bots/reference/bot-management-variables/) in a Worker's `request.cf` if the account has access to Bot Management. With the flag disabled, Cloudflare will include a default Bot Management object, regardless of whether the account is entitled to Bot Management. ### URLSearchParams delete() and has() value argument | **Default as of** | 2023-07-01 | | ------------------- | -------------------------------------------- | | **Flag to enable** | urlsearchparams\_delete\_has\_value\_arg | | **Flag to disable** | no\_urlsearchparams\_delete\_has\_value\_arg | The WHATWG introduced additional optional arguments to the `URLSearchParams` object [delete() ↗](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/delete) and [has() ↗](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/has) methods that allow for more precise control over the removal of query parameters. Because the arguments are optional and change the behavior of the methods when present there is a risk of breaking existing code. If your compatibility date is set to July 1, 2023 or after, this compatibility flag will be enabled by default. For an example of how this change could break existing code, consider code that uses the `Array` `forEach()` method to iterate through a number of parameters to delete: ```js const usp = new URLSearchParams(); // ... ['abc', 'xyz'].forEach(usp.delete.bind(usp)); ``` The `forEach()` automatically passes multiple parameters to the function that is passed in. Prior to the addition of the new standard parameters, these extra arguments would have been ignored. Now, however, the additional arguments have meaning and change the behavior of the function. With this flag, the example above would need to be changed to: ```js const usp = new URLSearchParams(); // ... ['abc', 'xyz'].forEach((key) => usp.delete(key)); ``` ### Use a spec compliant URL implementation in redirects | **Default as of** | 2023-03-14 | | ------------------- | --------------------------------- | | **Flag to enable** | response\_redirect\_url\_standard | | **Flag to disable** | response\_redirect\_url\_original | Change the URL implementation used in `Response.redirect()` to be spec-compliant (WHATWG URL Standard). ### Dynamic Dispatch Exception Propagation | **Default as of** | 2023-03-01 | | ------------------- | --------------------------------------------- | | **Flag to enable** | dynamic\_dispatch\_tunnel\_exceptions | | **Flag to disable** | dynamic\_dispatch\_treat\_exceptions\_as\_500 | Previously, when using Workers for Platforms' [dynamic dispatch API](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/) to send an HTTP request to a user Worker, if the user Worker threw an exception, the dynamic dispatch Worker would receive an HTTP `500` error with no body. When the `dynamic_dispatch_tunnel_exceptions` compatibility flag is enabled, the exception will instead propagate back to the dynamic dispatch Worker. The `fetch()` call in the dynamic dispatch Worker will throw the same exception. This matches the similar behavior of [service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) and [Durable Objects](https://developers.cloudflare.com/durable-objects/). ### `Headers` supports `getSetCookie()` | **Default as of** | 2023-03-01 | | ------------------- | ------------------------------- | | **Flag to enable** | http\_headers\_getsetcookie | | **Flag to disable** | no\_http\_headers\_getsetcookie | Adds the [getSetCookie() ↗](https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie) method to the [Headers ↗](https://developer.mozilla.org/en-US/docs/Web/API/Headers) API in Workers. ```js const response = await fetch("https://example.com"); let cookieValues = response.headers.getSetCookie(); ``` ### Node.js compatibility | **Default as of** | 2026-08-04 | | ------------------- | ------------------ | | **Flag to enable** | nodejs\_compat | | **Flag to disable** | no\_nodejs\_compat | Enables [Node.js APIs](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) in the Workers Runtime. For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. Note that some Node.js APIs are only enabled when your Worker's compatibility date is on or after the following dates: | Node.js API | Enabled with nodejs\_compat on or after | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | [Disable Top-level Await in require()](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#disable-top-level-await-in-require) | 2024-12-02 | | [process.env](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-auto-populating-processenv) | 2025-04-01 | | [node:http, node:https](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-availability-of-nodehttp-and-nodehttps-modules) | 2025-08-15 | | [http.server](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-nodejs-http-server-modules) | 2025-09-01 | Some Node.js modules are available in Workers only as non-functional stubs. These modules can be imported or required, but do not provide working implementations of the corresponding Node.js APIs. Stubs exist for compatibility with packages that check whether a module exists, and should not be used directly in application code. The following stubs are enabled automatically only when `nodejs_compat` is enabled and your Worker's compatibility date is on or after the date shown: | Stub module | Enabled with nodejs\_compat on or after | Enable flag | Disable flag | | ---------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------- | ---------------------------------------- | | [node:http2 ↗](https://nodejs.org/docs/latest/api/http2.html) | 2025-09-01 | enable\_nodejs\_http2\_module | disable\_nodejs\_http2\_module | | [node:vm ↗](https://nodejs.org/docs/latest/api/vm.html) | 2025-10-01 | enable\_nodejs\_vm\_module | disable\_nodejs\_vm\_module | | [node:cluster ↗](https://nodejs.org/docs/latest/api/cluster.html) | 2025-12-04 | enable\_nodejs\_cluster\_module | disable\_nodejs\_cluster\_module | | [node:domain ↗](https://nodejs.org/docs/latest/api/domain.html) | 2025-12-04 | enable\_nodejs\_domain\_module | disable\_nodejs\_domain\_module | | [node:trace\_events ↗](https://nodejs.org/docs/latest/api/tracing.html) | 2025-12-04 | enable\_nodejs\_trace\_events\_module | disable\_nodejs\_trace\_events\_module | | [node:wasi ↗](https://nodejs.org/docs/latest/api/wasi.html) | 2025-12-04 | enable\_nodejs\_wasi\_module | disable\_nodejs\_wasi\_module | | node:\_stream\_wrap | 2026-01-29 | enable\_nodejs\_stream\_wrap\_module | disable\_nodejs\_stream\_wrap\_module | | [node:dgram ↗](https://nodejs.org/docs/latest/api/dgram.html) | 2026-01-29 | enable\_nodejs\_dgram\_module | disable\_nodejs\_dgram\_module | | [node:inspector ↗](https://nodejs.org/docs/latest/api/inspector.html) | 2026-01-29 | enable\_nodejs\_inspector\_module | disable\_nodejs\_inspector\_module | | [node:sqlite ↗](https://nodejs.org/docs/latest/api/sqlite.html) | 2026-01-29 | enable\_nodejs\_sqlite\_module | disable\_nodejs\_sqlite\_module | | [node:child\_process ↗](https://nodejs.org/docs/latest/api/child%5Fprocess.html) | 2026-03-17 | enable\_nodejs\_child\_process\_module | disable\_nodejs\_child\_process\_module | | [node:readline ↗](https://nodejs.org/docs/latest/api/readline.html) | 2026-03-17 | enable\_nodejs\_readline\_module | disable\_nodejs\_readline\_module | | [node:repl ↗](https://nodejs.org/docs/latest/api/repl.html) | 2026-03-17 | enable\_nodejs\_repl\_module | disable\_nodejs\_repl\_module | | [node:tty ↗](https://nodejs.org/docs/latest/api/tty.html) | 2026-03-17 | enable\_nodejs\_tty\_module | disable\_nodejs\_tty\_module | | [node:v8 ↗](https://nodejs.org/docs/latest/api/v8.html) | 2026-03-17 | enable\_nodejs\_v8\_module | disable\_nodejs\_v8\_module | | [node:worker\_threads ↗](https://nodejs.org/docs/latest/api/worker%5Fthreads.html) | 2026-03-17 | enable\_nodejs\_worker\_threads\_module | disable\_nodejs\_worker\_threads\_module | When enabling `nodejs_compat`, we recommend using the latest version of [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/), and the latest compatibility date, in order to maximize compatibility. Some older versions of Wrangler inject additional polyfills that are no longer necessary when your Worker uses a more recent compatibility date, because they are provided by the Workers runtime. For compatibility dates of `2026-08-04` or later, `nodejs_compat` and `nodejs_compat_v2` are not used because the compatibility date enables the same behavior. Existing projects do not need to remove these flags when updating their compatibility date. To turn off Node.js compatibility completely, remove the positive flags if present. Then add both `no_nodejs_compat` and `no_nodejs_compat_v2`. If you see errors using a particular npm package on Workers, you should first try updating your compatibility date and use the latest version of [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) or the [Cloudflare Vite Plugin](https://developers.cloudflare.com/workers/vite-plugin/). If you still encounter issues, please report them by [opening a GitHub issue ↗](https://github.com/cloudflare/workers-sdk/issues/new?template=bug-template.yaml). ### Streams Constructors | **Default as of** | 2022-11-30 | | ------------------- | ------------------------------ | | **Flag to enable** | streams\_enable\_constructors | | **Flag to disable** | streams\_disable\_constructors | Adds the work-in-progress `new ReadableStream()` and `new WritableStream()` constructors backed by JavaScript underlying sources and sinks. ### Compliant TransformStream constructor | **Default as of** | 2022-11-30 | | ------------------- | ----------------------------------------------- | | **Flag to enable** | transformstream\_enable\_standard\_constructor | | **Flag to disable** | transformstream\_disable\_standard\_constructor | Previously, the `new TransformStream()` constructor was not compliant with the Streams API standard. Use the `transformstream_enable_standard_constructor` to opt-in to the backwards-incompatible change to make the constructor compliant. Must be used in combination with the `streams_enable_constructors` flag. ### CommonJS modules do not export a module namespace | **Default as of** | 2022-10-31 | | ------------------- | --------------------------- | | **Flag to enable** | export\_commonjs\_default | | **Flag to disable** | export\_commonjs\_namespace | CommonJS modules were previously exporting a module namespace (an object like `{ default: module.exports }`) rather than exporting only the `module.exports`. When this flag is enabled, the export is fixed. ### Do not throw from async functions | **Default as of** | 2022-10-31 | | ------------------- | ------------------------------------ | | **Flag to enable** | capture\_async\_api\_throws | | **Flag to disable** | do\_not\_capture\_async\_api\_throws | The `capture_async_api_throws` compatibility flag will ensure that, in conformity with the standards API, async functions will only ever reject if they throw an error. The inverse `do_not_capture_async_api_throws` flag means that async functions which contain an error may throw that error synchronously rather than rejecting. ### New URL parser implementation | **Default as of** | 2022-10-31 | | ------------------- | ------------- | | **Flag to enable** | url\_standard | | **Flag to disable** | url\_original | The original implementation of the [URL ↗](https://developer.mozilla.org/en-US/docs/Web/API/URL) API in Workers was not fully compliant with the [WHATWG URL Standard ↗](https://url.spec.whatwg.org/), differing in several ways, including: * The original implementation collapsed sequences of multiple slashes into a single slash: `new URL("https://example.com/a//b").toString() === "https://example.com/a/b"` * The original implementation would throw `"TypeError: Invalid URL string."` if it encountered invalid percent-encoded escape sequences, like `https://example.com/a%%b`. * The original implementation would percent-encode or percent-decode certain content differently: `new URL("https://example.com/a%40b?c d%20e?f").toString() === "https://example.com/a@b?c+d+e%3Ff"` * The original implementation lacked more recently implemented `URL` features, like [URL.canParse() ↗](https://developer.mozilla.org/en-US/docs/Web/API/URL/canParse%5Fstatic). Set the compatibility date of your Worker to a date after `2022-10-31` or enable the `url_standard` compatibility flag to opt-in the fully spec compliant `URL` API implementation. Refer to the [response\_redirect\_url\_standard compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#use-a-spec-compliant-url-implementation-in-redirects) , which affects the URL implementation used in `Response.redirect()`. ### `R2` bucket `list` respects the `include` option | **Default as of** | 2022-08-04 | | ------------------ | ------------------------ | | **Flag to enable** | r2\_list\_honor\_include | With the `r2_list_honor_include` flag set, the `include` argument to R2 `list` options is honored. With an older compatibility date and without this flag, the `include` argument behaves implicitly as `include: ["httpMetadata", "customMetadata"]`. ### Do not substitute `null` on `TypeError` | **Default as of** | 2022-06-01 | | ------------------- | --------------------------------------- | | **Flag to enable** | dont\_substitute\_null\_on\_type\_error | | **Flag to disable** | substitute\_null\_on\_type\_error | There was a bug in the runtime that meant that when being passed into built-in APIs, invalid values were sometimes mistakenly coalesced with `null`. Instead, a `TypeError` should have been thrown. The `dont_substitute_null_on_type_error` fixes this behavior so that an error is correctly thrown in these circumstances. ### Minimal subrequests | **Default as of** | 2022-04-05 | | ------------------- | ------------------------ | | **Flag to enable** | minimal\_subrequests | | **Flag to disable** | no\_minimal\_subrequests | With the `minimal_subrequests` flag set, `fetch()` subrequests sent to endpoints on the Worker's own zone (also called same-zone subrequests) have a reduced set of features applied to them. In general, these features should not have been initially applied to same-zone subrequests, and very few user-facing behavior changes are anticipated. Specifically, Workers might observe the following behavior changes with the new flag: * Response bodies will not be opportunistically gzipped before being transmitted to the Workers runtime. If a Worker reads the response body, it will read it in plaintext, as has always been the case, so disabling this prevents unnecessary decompression. Meanwhile, if the Worker passes the response through to the client, Cloudflare's HTTP proxy will opportunistically gzip the response body on that side of the Workers runtime instead. The behavior change observable by a Worker script should be that some `Content-Encoding: gzip` headers will no longer appear. * Automatic Platform Optimization may previously have been applied on both the Worker's initiating request and its subrequests in some circumstances. It will now only apply to the initiating request. * Link prefetching will now only apply to the Worker's response, not responses to the Worker's subrequests. ### Global `navigator` | **Default as of** | 2022-03-21 | | ------------------- | --------------------- | | **Flag to enable** | global\_navigator | | **Flag to disable** | no\_global\_navigator | With the `global_navigator` flag set, a new global `navigator` property is available from within Workers. Currently, it exposes only a single `navigator.userAgent` property whose value is set to `'Cloudflare-Workers'`. This property can be used to reliably determine whether code is running within the Workers environment. ### Do not use the Custom Origin Trust Store for external subrequests | **Default as of** | 2022-03-08 | | ------------------- | ----------------------------- | | **Flag to enable** | no\_cots\_on\_external\_fetch | | **Flag to disable** | cots\_on\_external\_fetch | The `no_cots_on_external_fetch` flag disables the use of the [Custom Origin Trust Store](https://developers.cloudflare.com/ssl/origin-configuration/custom-origin-trust-store/) when making external (grey-clouded) subrequests from a Cloudflare Worker. ### Setters/getters on API object prototypes | **Default as of** | 2022-01-31 | | ------------------- | --------------------------------------------- | | **Flag to enable** | workers\_api\_getters\_setters\_on\_prototype | | **Flag to disable** | workers\_api\_getters\_setters\_on\_instance | Originally, properties on Workers API objects were defined as instance properties as opposed to prototype properties. This broke subclassing at the JavaScript layer, preventing a subclass from correctly overriding the superclass getters/setters. This flag controls the breaking change made to set those getters/setters on the prototype template instead. This changes applies to: * `AbortSignal` * `AbortController` * `Blob` * `Body` * `DigestStream` * `Event` * `File` * `Request` * `ReadableStream` * `ReadableStreamDefaultReader` * `ReadableStreamBYOBReader` * `Response` * `TextDecoder` * `TextEncoder` * `TransformStream` * `URL` * `WebSocket` * `WritableStream` * `WritableStreamDefaultWriter` ### Durable Object `stub.fetch()` requires a full URL | **Default as of** | 2021-11-10 | | ------------------- | --------------------------------------------- | | **Flag to enable** | durable\_object\_fetch\_requires\_full\_url | | **Flag to disable** | durable\_object\_fetch\_allows\_relative\_url | Originally, when making a request to a Durable Object by calling `stub.fetch(url)`, a relative URL was accepted as an input. The URL would be interpreted relative to the placeholder URL `http://fake-host`, and the resulting absolute URL was delivered to the destination object's `fetch()` handler. This behavior was incorrect — full URLs were meant to be required. This flag makes full URLs required. ### `fetch()` improperly interprets unknown protocols as HTTP | **Default as of** | 2021-11-10 | | ------------------- | ------------------------------------------- | | **Flag to enable** | fetch\_refuses\_unknown\_protocols | | **Flag to disable** | fetch\_treats\_unknown\_protocols\_as\_http | Originally, if the `fetch()` function was passed a URL specifying any protocol other than `http:` or `https:`, it would silently treat it as if it were `http:`. For example, `fetch()` would appear to accept `ftp:` URLs, but it was actually making HTTP requests instead. Note that Cloudflare Workers supports a non-standard extension to `fetch()` to make it support WebSockets. However, when making an HTTP request that is intended to initiate a WebSocket handshake, you should still use `http:` or `https:` as the protocol, not `ws:` nor `wss:`. The `ws:` and `wss:` URL schemes are intended to be used together with the `new WebSocket()` constructor, which exclusively supports WebSocket. The extension to `fetch()` is designed to support HTTP and WebSocket in the same request (the response may or may not choose to initiate a WebSocket), and so all requests are considered to be HTTP. ### Streams BYOB reader detaches buffer | **Default as of** | 2021-11-10 | | ------------------- | ------------------------------------------------ | | **Flag to enable** | streams\_byob\_reader\_detaches\_buffer | | **Flag to disable** | streams\_byob\_reader\_does\_not\_detach\_buffer | Originally, the Workers runtime did not detach the `ArrayBuffer`s from user-provided TypedArrays when using the [BYOB reader's read() method](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/#methods), as required by the Streams spec, meaning it was possible to inadvertently reuse the same buffer for multiple `read()` calls. This change makes Workers conform to the spec. User code should never try to reuse an `ArrayBuffer` that has been passed into a [BYOB reader's read() method](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/#methods). Instead, user code can reuse the `ArrayBuffer` backing the result of the `read()` promise, as in the example below. ```js // Consume and discard `readable` using a single 4KiB buffer. let reader = readable.getReader({ mode: "byob" }); let arrayBufferView = new Uint8Array(4096); while (true) { let result = await reader.read(arrayBufferView); if (result.done) break; // Optionally something with `result` here. // Re-use the same memory for the next `read()` by creating // a new Uint8Array backed by the result's ArrayBuffer. arrayBufferView = new Uint8Array(result.value.buffer); } ``` The more recently added extension method `readAtLeast()` will always detach the `ArrayBuffer` and is unaffected by this feature flag setting. ### `FormData` parsing supports `File` | **Default as of** | 2021-11-03 | | ------------------- | ---------------------------------------------- | | **Flag to enable** | formdata\_parser\_supports\_files | | **Flag to disable** | formdata\_parser\_converts\_files\_to\_strings | [The FormData API ↗](https://developer.mozilla.org/en-US/docs/Web/API/FormData) is used to parse data (especially HTTP request bodies) in `multipart/form-data` format. Originally, the Workers runtime's implementation of the `FormData` API incorrectly converted uploaded files to strings. Therefore, `formData.get("filename")` would return a string containing the file contents instead of a `File` object. This change fixes the problem, causing files to be represented using `File` as specified in the standard. ### `HTMLRewriter` handling of `` | **Flag to enable** | html\_rewriter\_treats\_esi\_include\_as\_void\_tag | | ------------------ | --------------------------------------------------- | The HTML5 standard defines a fixed set of elements as void elements, meaning they do not use an end tag: ``, ``, `
`, ``, ``, ``, `
`, ``, ``, ``, ``, ``, ``, ``, ``, and ``. HTML5 does not recognize XML self-closing tag syntax. For example, `` ending tag is still required. The `/>` syntax simply is not recognized by HTML5 at all and it is treated the same as `>`. However, many developers still like to use this syntax, as a holdover from XHTML, a standard which failed to gain traction in the early 2000's. `` and `` are two tags that are not part of the HTML5 standard, but are instead used as part of [Edge Side Includes ↗](https://en.wikipedia.org/wiki/Edge%5FSide%5FIncludes), a technology for server-side HTML modification. These tags are not expected to contain any body and are commonly written with XML self-closing syntax. `HTMLRewriter` was designed to parse standard HTML5, not ESI. However, it would be useful to be able to implement some parts of ESI using `HTMLRewriter`. To that end, this compatibility flag causes `HTMLRewriter` to treat `` and `` as void tags, so that they can be parsed and handled properly. ## Experimental flags These flags can be enabled via `compatibility_flags`, but are not yet scheduled to become default on any particular date. ### Queue consumers don't wait for `ctx.waitUntil()` to resolve | **Flag to enable** | queue\_consumer\_no\_wait\_for\_wait\_until | | ------------------ | ------------------------------------------- | By default, [Queues](https://developers.cloudflare.com/queues/) Consumer Workers acknowledge messages only after promises passed to [ctx.waitUntil()](https://developers.cloudflare.com/workers/runtime-apis/context) have resolved. This behavior can cause queue consumers which utilize `ctx.waitUntil()` to process messages slowly. The default behavior is documented in the [Queues Consumer Configuration Guide](https://developers.cloudflare.com/queues/configuration/javascript-apis#consumer). This Consumer Worker is an example of a Worker which utilizes `ctx.waitUntil()`. Under the default behavior, this consumer Worker will only acknowledge a batch of messages after the sleep function has resolved. ```js export default { async fetch(request, env, ctx) { // omitted }, async queue(batch, env, ctx) { console.log(`received batch of ${batch.messages.length} messages to queue ${batch.queue}`); for (let i = 0; i < batch.messages.length; ++i) { console.log(`message #${i}: ${JSON.stringify(batch.messages[i])}`); } ctx.waitUntil(sleep(30 * 1000)); } }; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } ``` If the `queue_consumer_no_wait_for_wait_until` flag is enabled, Queues consumers will no longer wait for promises passed to `ctx.waitUntil()` to resolve before acknowledging messages. This can improve the performance of queue consumers which utilize `ctx.waitUntil()`. With the flag enabled, in the above example, the consumer Worker will acknowledge the batch without waiting for the sleep function to resolve. Using this flag will not affect the behavior of `ctx.waitUntil()`. `ctx.waitUntil()` will continue to extend the lifetime of your consumer Worker to continue to work even after the batch of messages has been acknowledged. ### `HTMLRewriter` handling of `` | **Flag to enable** | html\_rewriter\_treats\_esi\_include\_as\_void\_tag | | ------------------ | --------------------------------------------------- | The HTML5 standard defines a fixed set of elements as void elements, meaning they do not use an end tag: ``, ``, `
`, ``, ``, ``, `
`, ``, ``, ``, ``, ``, ``, ``, ``, and ``. HTML5 does not recognize XML self-closing tag syntax. For example, `` ending tag is still required. The `/>` syntax simply is not recognized by HTML5 at all and it is treated the same as `>`. However, many developers still like to use this syntax, as a holdover from XHTML, a standard which failed to gain traction in the early 2000's. `` and `` are two tags that are not part of the HTML5 standard, but are instead used as part of [Edge Side Includes ↗](https://en.wikipedia.org/wiki/Edge%5FSide%5FIncludes), a technology for server-side HTML modification. These tags are not expected to contain any body and are commonly written with XML self-closing syntax. `HTMLRewriter` was designed to parse standard HTML5, not ESI. However, it would be useful to be able to implement some parts of ESI using `HTMLRewriter`. To that end, this compatibility flag causes `HTMLRewriter` to treat `` and `` as void tags, so that they can be parsed and handled properly. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/compatibility-flags/#page","headline":"Compatibility flags · Cloudflare Workers docs","description":"Opt into a specific features of the Workers runtime for your Workers project.","url":"https://developers.cloudflare.com/workers/configuration/compatibility-flags/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-12","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Enable your Worker to be executed on a schedule. title: Cron Triggers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cron Triggers Last updated Jun 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/cron-triggers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Cron Triggers allow users to map a cron expression to a Worker using a [scheduled() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) that enables Workers to be executed on a schedule. Cron Triggers are ideal for running periodic jobs, such as for maintenance or calling third-party APIs to collect up-to-date data. Workers scheduled by Cron Triggers will run on underutilized machines to make the best use of Cloudflare's capacity and route traffic efficiently. Note Cron Triggers are also available directly in [Workflows](https://developers.cloudflare.com/workflows/) via `schedules` on the Workflow binding instead. Refer to [Trigger Workflows](https://developers.cloudflare.com/workflows/build/trigger-workflows/) for details, and use a Wrangler release that supports Workflow schedules. Cron Triggers execute on UTC time. ## Add a Cron Trigger ### 1\. Define a scheduled event listener To respond to a Cron Trigger, you must add a ["scheduled" handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) to your Worker. ```js export default { async scheduled(controller, env, ctx) { console.log("cron processed"); }, }; ``` ```ts interface Env {} export default { async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { console.log("cron processed"); }, }; ``` ```python from workers import WorkerEntrypoint class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx): # All four parameters (self, controller, env, ctx) are required print("cron processed") ``` Refer to the following additional examples to write your code: * [Setting Cron Triggers](https://developers.cloudflare.com/workers/examples/cron-trigger/) * [Multiple Cron Triggers](https://developers.cloudflare.com/workers/examples/multiple-cron-triggers/) ### 2\. Update configuration Cron Trigger changes take time to propagate. Changes such as adding a new Cron Trigger, updating an old Cron Trigger, or deleting a Cron Trigger may take several minutes (up to 15 minutes) to propagate to the Cloudflare global network. After you have updated your Worker code to include a `"scheduled"` event, you must update your Worker project configuration. #### Via the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) If a Worker is managed with Wrangler, Cron Triggers should be exclusively managed through the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). Refer to the example below for a Cron Triggers configuration: ```jsonc { "triggers": { // Schedule cron triggers: // - At every 3rd minute // - At 15:00 (UTC) on first day of the month // - At 23:59 (UTC) on the last weekday of the month "crons": [ "*/3 * * * *", "0 15 1 * *", "59 23 LW * *" ] } } ``` ```toml [triggers] crons = [ "*/3 * * * *", "0 15 1 * *", "59 23 LW * *" ] ``` You also can set a different Cron Trigger for each [environment](https://developers.cloudflare.com/workers/wrangler/environments/) in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). You need to put the `triggers` array under your chosen environment. For example: ```jsonc { "env": { "dev": { "triggers": { "crons": [ "0 * * * *" ] } } } } ``` ```toml [env.dev.triggers] crons = [ "0 * * * *" ] ``` #### Via the dashboard To add Cron Triggers in the Cloudflare dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker > **Settings** \> **Triggers** \> **Cron Triggers**. ## Supported cron expressions Cloudflare supports cron expressions with five fields, along with most [Quartz scheduler ↗](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html#introduction)\-like cron syntax extensions: | Field | Values | Characters | | ------------- | ------------------------------------------------------------------ | ------------ | | Minute | 0-59 | \* , - / | | Hours | 0-23 | \* , - / | | Days of Month | 1-31 | \* , - / L W | | Months | 1-12, case-insensitive 3-letter abbreviations ("JAN", "aug", etc.) | \* , - / | | Weekdays | 1-7, case-insensitive 3-letter abbreviations ("MON", "fri", etc.) | \* , - / L # | Note Days of the week go from 1 = Sunday to 7 = Saturday, which is different on some other cron systems (where 0 = Sunday and 6 = Saturday). To avoid ambiguity you may prefer to use the three-letter abbreviations (e.g. `SUN` rather than 1). ### Examples Some common time intervals that may be useful for setting up your Cron Trigger: * `* * * * *` * At every minute * `*/30 * * * *` * At every 30th minute * `45 * * * *` * On the 45th minute of every hour * `0 17 * * sun` or `0 17 * * 1` * 17:00 (UTC) on Sunday * `10 7 * * mon-fri` or `10 7 * * 2-6` * 07:10 (UTC) on weekdays * `0 15 1 * *` * 15:00 (UTC) on first day of the month * `0 18 * * 6L` or `0 18 * * friL` * 18:00 (UTC) on the last Friday of the month * `59 23 LW * *` * 23:59 (UTC) on the last weekday of the month ## Test Cron Triggers locally Test Cron Triggers using Wrangler with [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev), or using the [Cloudflare Vite plugin ↗](https://developers.cloudflare.com/workers/vite-plugin/). This exposes a `/cdn-cgi/handler/scheduled` route, which can be used to test using an HTTP request. If you are using the Cloudflare Vite Plugin, ensure that you use the correct vite port for the following commands (Vite defaults to 5173). ```sh curl "http://localhost:8787/cdn-cgi/handler/scheduled" ``` By default, the endpoint returns the scheduled handler outcome as text. To return the structured scheduled handler result as JSON, pass `?format=json`. ```sh curl "http://localhost:8787/cdn-cgi/handler/scheduled?format=json" ``` ```json { "outcome": "ok", "noRetry": false } ``` The `noRetry` field is `true` when the scheduled handler calls `controller.noRetry()`. To simulate different cron patterns, a `cron` query parameter can be passed in. ```sh curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*" ``` Optionally, you can also pass a `time` query parameter to override `controller.scheduledTime` in your scheduled event listener. ```sh curl "http://localhost:8787/cdn-cgi/handler/scheduled?cron=*+*+*+*+*&time=1745856238000" ``` ## View past events To view the execution history of Cron Triggers, view **Cron Events**: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your **Worker**. 3. Select **Settings**. 4. Under **Trigger Events**, select **View events**. Cron Events stores the 100 most recent invocations of the Cron scheduled event. [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs) also records invocation logs for the Cron Trigger with a longer retention period and a filter & query interface. If you are interested in an API to access Cron Events, use Cloudflare's [GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api). Note It can take up to 30 minutes before events are displayed in **Past Cron Events** when creating a new Worker or changing a Worker's name. Refer to [Metrics and Analytics](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/) for more information. ## Remove a Cron Trigger ### Via the dashboard To delete a Cron Trigger on a deployed Worker via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker. 3. Go to **Triggers** \> select the three dot icon next to the Cron Trigger you want to remove > **Delete**. #### Via the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) If a Worker is managed with Wrangler, Cron Triggers should be exclusively managed through the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). When deploying a Worker with Wrangler any previous Cron Triggers are replaced with those specified in the `triggers` array. * If the `crons` property is an empty array then all the Cron Triggers are removed. * If the `triggers` or `crons` property are `undefined` then the currently deploy Cron Triggers are left in-place. ```jsonc { "triggers": { // Remove all cron triggers: "crons": [] } } ``` ```toml [triggers] crons = [ ] ``` ## Limits Refer to [Limits](https://developers.cloudflare.com/workers/platform/limits/) to track the maximum number of Cron Triggers per Worker. ## Green Compute With Green Compute enabled, your Cron Triggers will only run on Cloudflare points of presence that are located in data centers that are powered purely by renewable energy. Organizations may claim that they are powered by 100 percent renewable energy if they have procured sufficient renewable energy to account for their overall energy use. Renewable energy can be purchased in a number of ways, including through on-site generation (wind turbines, solar panels), directly from renewable energy producers through contractual agreements called Power Purchase Agreements (PPA), or in the form of Renewable Energy Credits (REC, IRECs, GoOs) from an energy credit market. Green Compute can be configured at the account level: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In the **Account details** section, find **Compute Setting**. 3. Select **Change**. 4. Select **Green Compute**. 5. Select **Confirm**. ## Related resources * [Triggers](https://developers.cloudflare.com/workers/wrangler/configuration/#triggers) \- Review Wrangler configuration file syntax for Cron Triggers. * Learn how to access Cron Triggers in [ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) for an optimized experience. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/cron-triggers/#page","headline":"Cron Triggers · Cloudflare Workers docs","description":"Enable your Worker to be executed on a schedule.","url":"https://developers.cloudflare.com/workers/configuration/cron-triggers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: You can add environment variables, which are a type of binding, to attach text strings or JSON values to your Worker. title: Environment variables image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Environment variables Last updated Jun 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/environment-variables/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background You can add environment variables, which are a type of binding, to attach text strings or JSON values to your Worker. Environment variables are available on the [env parameter](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#parameters) passed to your Worker's [fetch event handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). Text strings and JSON values are not encrypted and are useful for storing application configuration. ## Add environment variables via Wrangler To add env variables using Wrangler, define text and JSON via the `[vars]` configuration in your Wrangler file. In the following example, `API_HOST` and `API_ACCOUNT_ID` are text values and `SERVICE_X_DATA` is a JSON value. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker-dev", "vars": { "API_HOST": "example.com", "API_ACCOUNT_ID": "example_user", "SERVICE_X_DATA": { "URL": "service-x-api.dev.example", "MY_ID": 123 } } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker-dev" [vars] API_HOST = "example.com" API_ACCOUNT_ID = "example_user" [vars.SERVICE_X_DATA] URL = "service-x-api.dev.example" MY_ID = 123 ``` Refer to the following example on how to access the `API_HOST` environment variable in your Worker code: ```js export default { async fetch(request, env, ctx) { return new Response(`API host: ${env.API_HOST}`); }, }; ``` ```ts export interface Env { API_HOST: string; } export default { async fetch(request, env, ctx): Promise { return new Response(`API host: ${env.API_HOST}`); }, } satisfies ExportedHandler; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): # Environment variables are accessed via attribute access on self.env return Response(f"API host: {self.env.API_HOST}") ``` ### Import `env` for global access You can also import `env` from [cloudflare:workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) to access environment variables from anywhere in your code, including outside of request handlers: ```js import { env } from "cloudflare:workers"; // Access environment variables at the top level const apiHost = env.API_HOST; export default { async fetch(request) { return new Response(`API host: ${apiHost}`); }, }; ``` ```ts import { env } from "cloudflare:workers"; // Access environment variables at the top level const apiHost = env.API_HOST; export default { async fetch(request: Request): Promise { return new Response(`API host: ${apiHost}`); }, }; ``` This approach is useful when you need to: * Initialize configuration or API clients at the top level of your Worker. * Access environment variables from deeply nested functions without passing `env` through every function call. For more details, refer to [Importing env as a global](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global). ### Configuring different environments in Wrangler [Environments in Wrangler](https://developers.cloudflare.com/workers/wrangler/environments) let you specify different configurations for the same Worker, including different values for `vars` in each environment. As `vars` is a [non-inheritable key](https://developers.cloudflare.com/workers/wrangler/configuration/#non-inheritable-keys), they are not inherited by environments and must be specified for each environment. The example below sets up two environments, `staging` and `production`, with different values for `API_HOST`. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker-dev", // top level environment "vars": { "API_HOST": "api.example.com" }, "env": { "staging": { "vars": { "API_HOST": "staging.example.com" } }, "production": { "vars": { "API_HOST": "production.example.com" } } } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker-dev" [vars] API_HOST = "api.example.com" [env.staging.vars] API_HOST = "staging.example.com" [env.production.vars] API_HOST = "production.example.com" ``` To run Wrangler commands in specific environments, you can pass in the `--env` or `-e` flag. For example, you can develop the Worker in an environment called `staging` by running `npx wrangler dev --env staging`, and deploy it with `npx wrangler deploy --env staging`. Learn about [environments in Wrangler](https://developers.cloudflare.com/workers/wrangler/environments). ## Add environment variables via the dashboard To add environment variables via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Select **Settings**. 4. Under **Variables and Secrets**, select **Add**. 5. Select a **Type**, input a **Variable name**, and input its **Value**. This variable will be made available to your Worker. 6. (Optional) To add multiple environment variables, select **Add variable**. 7. Select **Deploy** to implement your changes. Plaintext strings and secrets Select the **Secret** type if your environment variable is a [secret](https://developers.cloudflare.com/workers/configuration/secrets/). Alternatively, consider [Cloudflare Secrets Store](https://developers.cloudflare.com/secrets-store/), for account-level secrets. ## Compare secrets and environment variables Use secrets for sensitive information Do not use plaintext environment variables to store sensitive information. Use [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) or [Secrets Store bindings](https://developers.cloudflare.com/secrets-store/integrations/workers/) instead. [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) are [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). The difference is secret values are not visible within Wrangler or Cloudflare dashboard after you define them. This means that sensitive data, including passwords or API tokens, should always be encrypted to prevent data leaks. To your Worker, there is no difference between an environment variable and a secret. The secret's value is passed through as defined. ### Local development with secrets Caution Do not use `vars` to store sensitive information in your Worker's Wrangler configuration file. Use secrets instead. Put secrets for use in local development in either a `.dev.vars` file or a `.env` file, in the same directory as the Wrangler configuration file. Note You can use the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) to declare which secret names your Worker requires. When defined, only the keys listed in `secrets.required` are loaded from `.dev.vars` or `.env`. Additional keys are excluded and missing keys produce a warning. Note Choose to use either `.dev.vars` or `.env` but not both. If you define a `.dev.vars` file, then values in `.env` files will not be included in the `env` object during local development. These files should be formatted using the [dotenv ↗](https://hexdocs.pm/dotenvy/dotenv-file-format.html) syntax. For example: ```bash SECRET_KEY="value" API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ``` Do not commit secrets to git The `.dev.vars` and `.env` files should not be committed to git. Add `.dev.vars*` and `.env*` to your project's `.gitignore` file. To set different secrets for each Cloudflare environment, create files named `.dev.vars.` or `.env.`. When you select a Cloudflare environment in your local development, the corresponding environment-specific file will be loaded ahead of the generic `.dev.vars` (or `.env`) file. * When using `.dev.vars.` files, all secrets must be defined per environment. If `.dev.vars.` exists then only this will be loaded; the `.dev.vars` file will not be loaded. * In contrast, all matching `.env` files are loaded and the values are merged. For each variable, the value from the most specific file is used, with the following precedence: * `.env..local` (most specific) * `.env.local` * `.env.` * `.env` (least specific) Controlling \`.env\` handling It is possible to control how `.env` files are loaded in local development by setting environment variables on the process running the tools. * To disable loading local dev vars from `.env` files without providing a `.dev.vars` file, set the `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV` environment variable to `"false"`. * To include every environment variable defined in your system's process environment as a local development variable, ensure there is no `.dev.vars` and then set the `CLOUDFLARE_INCLUDE_PROCESS_ENV` environment variable to `"true"`. This is not needed when using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property), which loads from `process.env` automatically. ## Environment variables and Node.js compatibility When you enable [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) and the [nodejs\_compat\_populate\_process\_env](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs%5Fcompat%5Fpopulate%5Fprocess%5Fenv) compatibility flag (enabled by default for compatibility dates on or after 2025-04-01), environment variables are available via the global `process.env`. The `process.env` will be populated lazily the first time that `process` is accessed in the worker. Text variable values are exposed directly. JSON variable values that evaluate to string values are exposed as the parsed value. JSON variable values that do not evaluate to string values are exposed as the raw JSON string. For example, imagine a Worker with three environment variables, two text values, and one JSON value: ```plaintext [vars] FOO = "abc" BAR = "abc" BAZ = { "a": 123 } ``` Environment variables can be added using either the `wrangler.{json|jsonc|toml}` file or via the Cloudflare dashboard UI. The values of `process.env.FOO` and `process.env.BAR` will each be the JavaScript string `"abc"`. The value of `process.env.BAZ` will be the JSON-encoded string `"{ \"a\": 123 }"`. :::note Note also that because secrets are a form of environment variable within the runtime, secrets are also exposed via `process.env`. ## Related resources * Migrating environment variables from [Service Worker format to ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/#environment-variables). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/environment-variables/#page","headline":"Environment variables · Cloudflare Workers docs","description":"You can add environment variables, which are a type of binding, to attach text strings or JSON values to your Worker.","url":"https://developers.cloudflare.com/workers/configuration/environment-variables/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Integrate with third-party services and products. title: Integrations image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Integrations Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/integrations/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) One of the key features of Cloudflare Workers is the ability to integrate with other services and products. In this document, we will explain the types of integrations available with Cloudflare Workers and provide step-by-step instructions for using them. ## Types of integrations Cloudflare Workers offers several types of integrations, including: * [Databases](https://developers.cloudflare.com/workers/databases/): Cloudflare Workers can be integrated with a variety of databases, including SQL and NoSQL databases. This allows you to store and retrieve data from your databases directly from your Cloudflare Workers code. * [APIs](https://developers.cloudflare.com/workers/configuration/integrations/apis/): Cloudflare Workers can be used to integrate with external APIs, allowing you to access and use the data and functionality exposed by those APIs in your own code. * [Third-party services](https://developers.cloudflare.com/workers/configuration/integrations/external-services/): Cloudflare Workers can be used to integrate with a wide range of third-party services, such as payment gateways, authentication providers, and more. This makes it possible to use these services in your Cloudflare Workers code. ## How to use integrations To use any of the available integrations: * Determine which integration you want to use and make sure you have the necessary accounts and credentials for it. * In your Cloudflare Workers code, import the necessary libraries or modules for the integration. * Use the provided APIs and functions to connect to the integration and access its data or functionality. * Store necessary secrets and keys using secrets via [wrangler secret put ](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret). ## Tips and best practices To help you get the most out of using integrations with Cloudflare Workers: * Secure your integrations and protect sensitive data. Ensure you use secure authentication and authorization where possible, and ensure the validity of libraries you import. * Use [caching](https://developers.cloudflare.com/workers/reference/how-the-cache-works) to improve performance and reduce the load on an external service. * Split your Workers into service-oriented architecture using [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) to make your application more modular, easier to maintain, and more performant. * Use [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) when communicating with external APIs and services, which create a DNS record on your behalf and treat your Worker as an application instead of a proxy. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/integrations/#page","headline":"Integrations · Cloudflare Workers docs","description":"Integrate with third-party services and products.","url":"https://developers.cloudflare.com/workers/configuration/integrations/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Integrate Cloudflare Workers with third-party APIs using the Fetch API. title: APIs image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # APIs Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/integrations/apis/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) To integrate with third party APIs from Cloudflare Workers, use the [fetch API](https://developers.cloudflare.com/workers/runtime-apis/fetch/) to make HTTP requests to the API endpoint. Then use the response data to modify or manipulate your content as needed. For example, if you want to integrate with a weather API, make a fetch request to the API endpoint and retrieve the current weather data. Then use this data to display the current weather conditions on your website. To make the `fetch()` request, add the following code to your project's `src/index.js` file: ```js async function handleRequest(request) { // Make the fetch request to the third party API endpoint const response = await fetch("https://weather-api.com/endpoint", { method: "GET", headers: { "Content-Type": "application/json", }, }); // Retrieve the data from the response const data = await response.json(); // Use the data to modify or manipulate your content as needed return new Response(data); } ``` ## Authentication If your API requires authentication, use Wrangler secrets to securely store your credentials. To do this, create a secret in your Cloudflare Workers project using the following [wrangler secret](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) command: ```sh wrangler secret put SECRET_NAME ``` Then, retrieve the secret value in your code using the following code snippet: ```js const secretValue = env.SECRET_NAME; ``` Then use the secret value to authenticate with the external service. For example, if the external service requires an API key for authentication, include it in your request headers. For services that require mTLS authentication, use [mTLS certificates](https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls) to present a client certificate. ## Tips * Use the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) to cache data from the third party API. This allows you to optimize cacheable requests made to the API. Integrating with third party APIs from Cloudflare Workers adds additional functionality and features to your application. * Use [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) when communicating with external APIs, which treat your Worker as your core application. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/integrations/apis/#page","headline":"APIs · Cloudflare Workers docs","description":"Integrate Cloudflare Workers with third-party APIs using the Fetch API.","url":"https://developers.cloudflare.com/workers/configuration/integrations/apis/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Connect Cloudflare Workers to external services using libraries, SDKs, and secure authentication. title: External Services image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # External Services Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/integrations/external-services/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Many external services provide libraries and SDKs to interact with their APIs. While many Node-compatible libraries work on Workers right out of the box, some, which implement `fs`, `http/net`, or access the browser `window` do not directly translate to the Workers runtime, which is v8-based. ## Authentication If your service requires authentication, use Wrangler secrets to securely store your credentials. To do this, create a secret in your Cloudflare Workers project using the following [wrangler secret](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) command: ```sh wrangler secret put SECRET_NAME ``` Then, retrieve the secret value in your code using the following code snippet: ```js const secretValue = env.SECRET_NAME; ``` Then use the secret value to authenticate with the external service. For example, if the external service requires an API key for authentication, include the secret in your library's configuration. For services that require mTLS authentication, use [mTLS certificates](https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls) to present a client certificate. Use [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) when communicating with external APIs, which treat your Worker as your core application. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/integrations/external-services/#page","headline":"External Services · Cloudflare Workers docs","description":"Connect Cloudflare Workers to external services using libraries, SDKs, and secure authentication.","url":"https://developers.cloudflare.com/workers/configuration/integrations/external-services/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Define Workers configuration in JSON metadata for multipart form-data script uploads. title: Multipart upload metadata image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Multipart upload metadata Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note There is a new API for uploading Workers. Refer to [these docs](https://developers.cloudflare.com/workers/platform/infrastructure-as-code#cloudflare-rest-api) for more information. If you're using the [Workers Script Upload API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/) or [Version Upload API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/versions/methods/create/) directly, `multipart/form-data` uploads require you to specify a `metadata` part. This metadata defines the Worker's configuration in JSON format, analogue to the [wrangler.toml file](https://developers.cloudflare.com/workers/wrangler/configuration/). ## Sample `metadata` ```json { "main_module": "main.js", "bindings": [ { "type": "plain_text", "name": "MESSAGE", "text": "Hello, world!" } ], "compatibility_date": "2021-09-14" } ``` Note See examples of metadata being used with the Workers Script Upload API [here](https://developers.cloudflare.com/workers/platform/infrastructure-as-code#cloudflare-rest-api). ## Attributes The following attributes are configurable at the top-level. Note At a minimum, the `main_module` key is required to upload a Worker. * `main_module` `string`required * The part name that contains the module entry point of the Worker that will be executed. For example, `main.js`. * `assets` `object`optional * [Asset](https://developers.cloudflare.com/workers/static-assets/) configuration for a Worker. * `config` `object`optional * [html\_handling](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/) determines the redirects and rewrites of requests for HTML content. * [not\_found\_handling](https://developers.cloudflare.com/workers/static-assets/#routing-behavior) determines the response when a request does not match a static asset. * `jwt` field provides a token authorizing assets to be attached to a Worker. * `keep_assets` `boolean`optional * Specifies whether assets should be retained from a previously uploaded Worker version; used in lieu of providing a completion token. * `bindings` array\[object\] optional * [Bindings](#bindings) to expose in the Worker. * `placement` `object`optional * [Smart placement](https://developers.cloudflare.com/workers/configuration/placement/) object for the Worker. * `mode` field only supports `smart` for automatic placement. * `compatibility_date` `string`optional * [Compatibility Date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/#setting-compatibility-date) indicating targeted support in the Workers runtime. Backwards incompatible fixes to the runtime following this date will not affect this Worker. Highly recommended to set a `compatibility_date`, otherwise if on upload via the API, it defaults to the oldest compatibility date before any flags took effect (2021-11-02). * `compatibility_flags` array\[string\] optional * [Compatibility Flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#setting-compatibility-flags) that enable or disable certain features in the Workers runtime. Used to enable upcoming features or opt in or out of specific changes not included in a `compatibility_date`. ## Additional attributes: [Workers Script Upload API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/) For [immediately deployed uploads](https://developers.cloudflare.com/workers/versions-and-deployments/#upload-a-new-version-and-deploy-it-immediately), the following **additional** attributes are configurable at the top-level. Note Except for `annotations`, these attributes are **not available** for version uploads. * `migrations` array\[object\] optional * [Durable Objects migrations](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) to apply. * `logpush` `boolean`optional * Whether [Logpush](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/hostname-analytics/#logpush) is turned on for the Worker. * `tail_consumers` array\[object\] optional * [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) that will consume logs from the attached Worker. * `tags` array\[string\] optional * List of strings to use as tags for this Worker. * `annotations` `object`optional * Annotations object for the Worker version created by this upload. Also available on the [Version Upload API](#additional-attributes-version-upload-api). * `workers/message` specifies a custom message for the version. * `workers/tag` specifies a custom identifier for the version. ## Additional attributes: [Version Upload API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/versions/methods/create/) For [version uploads](https://developers.cloudflare.com/workers/versions-and-deployments/#upload-a-new-version-to-be-gradually-deployed-or-deployed-at-a-later-time), the following **additional** attributes are configurable at the top-level. * `annotations` `object`optional * Annotations object specific to the Worker version. * `workers/message` specifies a custom message for the version. * `workers/tag` specifies a custom identifier for the version. * `workers/alias` specifies a custom alias for this version. ## Bindings Workers can interact with resources on the Cloudflare Developer Platform using [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/). Refer to the JSON example below that shows how to add bindings in the `metadata` part. ```json { "bindings": [ { "type": "ai", "name": "" }, { "type": "analytics_engine", "name": "", "dataset": "" }, { "type": "assets", "name": "" }, { "type": "browser_rendering", "name": "" }, { "type": "d1", "name": "", "id": "" }, { "type": "durable_object_namespace", "name": "", "class_name": "" }, { "type": "hyperdrive", "name": "", "id": "" }, { "type": "kv_namespace", "name": "", "namespace_id": "" }, { "type": "mtls_certificate", "name": "", "certificate_id": "" }, { "type": "plain_text", "name": "", "text": "" }, { "type": "queue", "name": "", "queue_name": "" }, { "type": "r2_bucket", "name": "", "bucket_name": "" }, { "type": "secret_text", "name": "", "text": "" }, { "type": "service", "name": "", "service": "", "environment": "production" }, { "type": "vectorize", "name": "", "index_name": "" }, { "type": "version_metadata", "name": "" } ] } ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/#page","headline":"Multipart upload metadata · Cloudflare Workers docs","description":"Define Workers configuration in JSON metadata for multipart form-data script uploads.","url":"https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Control where your Worker runs to reduce latency. title: Placement image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Placement Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/placement/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) By default, [Workers](https://developers.cloudflare.com/workers/) and [Pages Functions](https://developers.cloudflare.com/pages/functions/) run in a data center closest to where the request was received. If your Worker makes requests to back-end infrastructure such as databases or APIs, it may be more performant to run that Worker closer to your back-end than the end user. ```jsonc { "placement": { // Use one of the following options (mutually exclusive): "mode": "smart", // Cloudflare automatically places your Worker closest to the upstream with the most requests "region": "gcp:us-east4", // Explicit cloud region to run your Worker closest to - e.g. "gcp:us-east4" or "aws:us-east-1" "host": "db.example.com:5432", // A host to probe (TCP/layer 4) - e.g. a database host - and place your Worker closest to "hostname": "api.example.com", // A hostname to probe (HTTP/layer 7) - e.g. an API endpoint - and place your Worker closest to }, } ``` ```toml [placement] mode = "smart" region = "gcp:us-east4" host = "db.example.com:5432" hostname = "api.example.com" ``` Placement can reduce the overall latency of a Worker request by minimizing roundtrip latency of requests between your Worker and back-end services. You can achieve single-digit millisecond latency to databases, APIs, and other services running in legacy cloud infrastructure. | Option | Best for | Configuration | | ---------- | --------------------------------------------------------------- | ---------------- | | **Smart** | Multiple back-end services, or unknown infrastructure locations | mode = "smart" | | **Region** | Single back-end service in a known cloud region | region | | **Host** | Single back-end service not in a major cloud provider | host or hostname | ## Understand placement Consider a user in Sydney, Australia accessing an application running on Workers. This application makes multiple round trips to a database in Frankfurt, Germany. ![A user located in Sydney, AU connecting to a Worker in the same region which then makes multiple round trips to a database located in Frankfurt, DE. ](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1292,height=414,format=webp/_astro/workers-smart-placement-disabled.CgvAE24H.png) The latency from multiple round trips between Sydney and Frankfurt adds up. By placing the Worker near the database, Cloudflare reduces the total request duration. ![A user located in Sydney, AU connecting to a Worker in Frankfurt, DE which then makes multiple round trips to a database also located in Frankfurt, DE. ](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1338,height=396,format=webp/_astro/workers-smart-placement-enabled.D6RN33at.png) ## Enable Smart Placement Smart Placement automatically analyzes your Worker's traffic patterns and places it in an optimal location. Use Smart Placement when: * Your Worker connects to multiple back-end services * You do not know the exact location of your infrastructure * Your back-end services are distributed or replicated Smart Placement is enabled on a per-Worker basis. Once enabled, it analyzes the [request duration](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/#request-duration) of the Worker in different Cloudflare locations on a regular basis. For each candidate location, Smart Placement considers the Worker's performance and the network latency added by forwarding the request. If a candidate location is significantly faster, the request is forwarded there. Otherwise, the Worker runs in the default location closest to the request. Smart Placement only considers locations where the Worker has previously run. It cannot place your Worker in a location that does not normally receive traffic. ### Review limitations * Smart Placement only affects the execution of [fetch event handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). It does not affect [RPC methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/) or [named entrypoints](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints). * Workers without a fetch event handler are ignored by Smart Placement. * [Static assets](https://developers.cloudflare.com/workers/static-assets/) are always served from the location nearest to the incoming request. If your code retrieves assets via the [static assets binding](https://developers.cloudflare.com/workers/static-assets/binding/), assets are served from the location where your Worker runs. ### Enable smart placement Smart Placement is available on all Workers plans. #### Configure with Wrangler Add the following to your Wrangler configuration file: ```jsonc { "placement": { "mode": "smart", }, } ``` ```toml [placement] mode = "smart" ``` Smart Placement may take up to 15 minutes to analyze your Worker after deployment. #### Configure in the dashboard 1. Go to **Workers & Pages**. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker. 3. Go to **Settings** \> **General**. 4. Under **Placement**, select **Smart**. Smart Placement requires consistent traffic to the Worker from multiple locations to make a placement decision. The analysis process may take up to 15 minutes. ### Check placement status Query your Worker's placement status through the Workers API: ```bash curl -X GET https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/workers/services/$WORKER_NAME \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" | jq . ``` Possible placement states: | Status | Description | | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | | _(not present)_ | The Worker has not been analyzed yet. It runs in the default location closest to the request. | | SUCCESS | The Worker was analyzed and will be optimized by Smart Placement. | | INSUFFICIENT\_INVOCATIONS | The Worker has not received enough requests from multiple locations to make a placement decision. | | UNSUPPORTED\_APPLICATION | Smart Placement made the Worker slower and reverted the placement. This state is rare (fewer than 1% of Workers). | ### Review request duration analytics Once Smart Placement is enabled, data about request duration is collected. Request duration is measured at the data center closest to the end user. By default, 1% of requests are not routed with Smart Placement to serve as a baseline for comparison. View your Worker's [request duration analytics](https://developers.cloudflare.com/workers/observability/metrics-and-analytics/#request-duration) to measure the impact of Smart Placement. ### Check the `cf-placement` header Cloudflare adds a `cf-placement` header to all requests when placement is enabled. Use this header to check whether a request was routed with Smart Placement and where the Worker processed the request. The header value includes a placement type and an airport code indicating the data center location: * `remote-LHR` — The request was routed using Smart Placement to a data center near London. * `local-EWR` — The request was not routed using Smart Placement. The Worker ran in the default location near Newark. Caution The `cf-placement` header may be removed before Smart Placement exits beta. ## Configure explicit Placement Hints Placement Hints let you explicitly specify where your Worker runs. Use Placement Hints when: * You know the exact location of your back-end infrastructure * Your Worker connects to a single database, API, or service * Your infrastructure is single-homed (not replicated or anycasted) Examples include a primary database, a virtual machine, or a Kubernetes cluster in a specific region. Reducing round-trip latency from 20 to 30 milliseconds per query to 1 to 3 milliseconds improves response times. Note Workers run on [Cloudflare's global network ↗](https://www.cloudflare.com/network/), not inside cloud provider regions. Placement Hints run your Worker in the data center with the lowest latency to your specified cloud region. At extremely high request volumes (hundreds of thousands of requests per second or more), Cloudflare may run instances across a more distributed area to balance traffic. ### Specify a cloud region If your infrastructure runs in AWS, GCP, or Azure, set the `placement.region` property using the format `{provider}:{region}`: ```jsonc { "placement": { "region": "aws:us-east-1", // Explicit cloud region to run your Worker closest to - e.g. "gcp:us-east4" or "aws:us-east-1" }, } ``` ```toml [placement] region = "aws:us-east-1" ``` Cloudflare maps your specified cloud region to the data center with the lowest latency to that region. Cloudflare automatically adjusts placement to account for network maintenance or changes, so you do not need to specify failover regions. ### Specify a host endpoint If your infrastructure is not in a major cloud provider, you can specify an endpoint for Cloudflare to probe. Cloudflare will triangulate the position of your external host and place Workers in a nearby region. Note Host-based placement is experimental. Set `placement.host` to identify a layer 4 service. Cloudflare uses TCP CONNECT checks to measure latency and selects the best data center. ```jsonc { "placement": { "host": "my_database_host.com:5432", // A host to probe (TCP/layer 4) - e.g. a database host - and place your Worker closest to }, } ``` ```toml [placement] host = "my_database_host.com:5432" ``` Set `placement.hostname` to identify a layer 7 service. Cloudflare uses HTTP HEAD checks to measure latency and selects the best data center. ```jsonc { "placement": { "hostname": "my_api_server.com", // A hostname to probe (HTTP/layer 7) - e.g. an API endpoint - and place your Worker closest to }, } ``` ```toml [placement] hostname = "my_api_server.com" ``` Probes are sent from public IP ranges, not Cloudflare IP ranges. Cloudflare rechecks service location at regular intervals. These probes locate single-homed resources and do not work correctly for broadcast, anycast, multicast, or replicated resources. ### List supported regions Placement Hints support Amazon Web Services (AWS), Google Cloud Platform (GCP), and Microsoft Azure region identifiers: | Provider | Format | Examples | | -------- | -------------- | --------------------------------------------------- | | AWS | aws:{region} | aws:us-east-1, aws:us-west-2, aws:eu-central-1 | | GCP | gcp:{region} | gcp:us-east4, gcp:europe-west1, gcp:asia-east1 | | Azure | azure:{region} | azure:westeurope, azure:eastus, azure:southeastasia | For a full list of region codes, refer to [AWS regions ↗](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html), [GCP regions ↗](https://cloud.google.com/compute/docs/regions-zones), or [Azure regions ↗](https://learn.microsoft.com/en-us/azure/reliability/regions-list). ## Placement Behavior Workers placement behaves in similar fashion when either Smart Placement or Placement Hints are used. The following behavior applies to both. ### Review limitations The following limitations apply to both Smart Placement and Placement Hints: * Placement only affects the execution of [fetch event handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). It does not affect [RPC methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/) or [named entrypoints](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints). * Workers without a fetch event handler are ignored by placement. * [Static assets](https://developers.cloudflare.com/workers/static-assets/) are always served from the location nearest to the incoming request. If your code retrieves assets via the [static assets binding](https://developers.cloudflare.com/workers/static-assets/binding/), assets are served from the location where your Worker runs. ### `cf-placement` header Cloudflare adds a `cf-placement` header to all requests when placement is enabled. Use this header to check whether a request was routed with placement and where the Worker processed the request. The header value includes a placement type and an airport code indicating the data center location: * `remote-LHR` — The request was routed using Smart Placement to a data center near London. * `local-EWR` — The request was not routed using Smart Placement. The Worker ran in the default location near Newark. Caution The `cf-placement` header may be removed before Smart Placement exits beta. ## Multiple Workers If you are building full-stack applications on Workers, split your edge logic (authentication, routing) and back-end logic (database queries, API calls) into separate Workers. Use [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) to connect them with type-safe RPC. ![Smart Placement and Service Bindings](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1444,height=692,format=webp/_astro/smart-placement-service-bindings.Ce58BYeF.png) Enable placement on your back-end Worker to invoke it close to your database, while the edge Worker handles authentication close to the user. ### Example: Edge authentication with a placed back-end This example shows two Workers: * `auth-worker` — runs at the edge (no placement), handles authentication * `app-worker` — placed near your database, handles data queries ```jsonc { "name": "auth-worker", "main": "src/index.ts", "services": [{ "binding": "APP", "service": "app-worker" }], } ``` ```toml name = "auth-worker" main = "src/index.ts" [[services]] binding = "APP" service = "app-worker" ``` ```ts import { AppWorker } from "../app-worker/src/index"; interface Env { APP: Service; } export default { async fetch(request: Request, env: Env): Promise { const authHeader = request.headers.get("Authorization"); if (!authHeader?.startsWith("Bearer ")) { return new Response("Unauthorized", { status: 401 }); } const userId = await validateToken(authHeader.slice(7)); if (!userId) { return new Response("Invalid token", { status: 403 }); } // Call the placed back-end Worker via RPC const data = await env.APP.getUser(userId); return Response.json(data); }, }; async function validateToken(token: string): Promise { return token === "valid" ? "user-123" : null; } ``` ```jsonc { "name": "app-worker", "main": "src/index.ts", "placement": { // Use one of the following options (mutually exclusive): // "mode": "smart", // Cloudflare automatically places your Worker closest to the upstream with the most requests "region": "aws:us-east-1", // Explicit cloud region to run your Worker closest to - e.g. "gcp:us-east4" or "aws:us-east-1" // "host": "db.example.com:5432", // A host to probe (TCP/layer 4) - e.g. a database host - and place your Worker closest to // "hostname": "api.example.com", // A hostname to probe (HTTP/layer 7) - e.g. an API endpoint - and place your Worker closest to }, } ``` ```toml name = "app-worker" main = "src/index.ts" [placement] region = "aws:us-east-1" ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export default class AppWorker extends WorkerEntrypoint { async fetch() { return new Response(null, { status: 404 }); } // Each method runs near your database - multiple queries stay fast async getUser(userId: string) { const user = await this.env.DB.prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .first(); return user; } async getUserListings(userId: string) { // Multiple round-trips to the DB are low-latency when placed nearby const user = await this.env.DB.prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .first(); const listings = await this.env.DB.prepare( "SELECT * FROM listings WHERE owner_id = ?", ) .bind(userId) .all(); const reviews = await this.env.DB.prepare( "SELECT * FROM reviews WHERE listing_id IN (SELECT id FROM listings WHERE owner_id = ?)", ) .bind(userId) .all(); return { user, listings: listings.results, reviews: reviews.results }; } } ``` The `auth-worker` runs at the edge to reject unauthorized requests quickly. Authenticated requests are forwarded via RPC to `app-worker`, which runs near your database for fast queries. ### Durable Objects [Durable Objects](https://developers.cloudflare.com/durable-objects/) provide automatic placement without configuration. Queries to a Durable Object's embedded [SQLite database](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) are effectively [zero-latency ↗](https://blog.cloudflare.com/sqlite-in-durable-objects/) because compute runs in the same process as the data. Do as much work as possible within the Durable Object and return a composite result, rather than making multiple round-trips from your Worker: ```ts import { DurableObject } from "cloudflare:workers"; type Session = { id: string; user_id: string; created_at: number }; type PromptHistory = { id: string; session_id: string; role: string; content: string; }; export class AgentHistory extends DurableObject { async getSessionContext(sessionId: string) { // All queries execute with zero network latency — compute and data are colocated const session = this.ctx.storage.sql .exec("SELECT * FROM sessions WHERE id = ?", sessionId) .one(); const prompts = this.ctx.storage.sql .exec( "SELECT * FROM prompt_history WHERE session_id = ? ORDER BY created_at", sessionId, ) .toArray(); return { session, prompts }; } } ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/placement/#page","headline":"Placement · Cloudflare Workers docs","description":"Control where your Worker runs to reduce latency.","url":"https://developers.cloudflare.com/workers/configuration/placement/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Connect your Worker to an external endpoint (via Routes, Custom Domains or a `workers.dev` subdomain) such that it can be accessed by the Internet. title: Routes and domains image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Routes and domains Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/routing/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) To allow a Worker to receive inbound HTTP requests, you must connect it to an external endpoint such that it can be accessed by the Internet. There are three types of routes: * [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains): Routes to a domain or subdomain (such as `example.com` or `shop.example.com`) within a Cloudflare zone where the Worker is the origin. * [Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/): Routes that are set within a Cloudflare zone where your origin server, if you have one, is behind a Worker that the Worker can communicate with. * [workers.dev](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/): A `workers.dev` subdomain route is automatically created for each Worker to help you getting started quickly. You may choose to [disable](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/) your `workers.dev` subdomain. ## What is best for me? It's recommended to run production Workers on a [Workers route or custom domain](https://developers.cloudflare.com/workers/configuration/routing/), rather than on your `workers.dev` subdomain. Your `workers.dev` subdomain is treated as a [Free website ↗](https://www.cloudflare.com/plans/) and is intended for personal or hobby projects that aren't business-critical. Custom Domains are recommended for use cases where your Worker is your application's origin server. Custom Domains can also be invoked within the same zone via `fetch()`, unlike Routes. Routes are recommended for use cases where your application's origin server is external to Cloudflare. Note that Routes cannot be the target of a same-zone `fetch()` call. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/configuration/routing/#page","headline":"Routes and domains · Cloudflare Workers docs","description":"Connect your Worker to an external endpoint (via Routes, Custom Domains or a workers.dev subdomain) such that it can be accessed by the Internet.","url":"https://developers.cloudflare.com/workers/configuration/routing/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Connect a Cloudflare Worker to a domain or subdomain with automatic DNS and certificate management. title: Custom Domains image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Custom Domains Last updated Jun 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Custom Domains allow you to connect your Worker to a domain or subdomain, without having to make changes to your DNS settings or perform any certificate management. After you set up a Custom Domain for your Worker, Cloudflare will create DNS records and issue necessary certificates on your behalf. The created DNS records will point directly to your Worker. Unlike [Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/#set-up-a-route), Custom Domains point all paths of a domain or subdomain to your Worker. Custom Domains are routes to a domain or subdomain (such as `example.com` or `shop.example.com`) within a Cloudflare zone where the Worker is the origin. Custom Domains are recommended if you want to connect your Worker to the Internet and do not have an application server that you want to always communicate with. If you do have external dependencies, you can create a `Request` object with the target URI, and use `fetch()` to reach out. Custom Domains can stack on top of each other. For example, if you have Worker A attached to `app.example.com` and Worker B attached to `api.example.com`, Worker A can call `fetch()` on `api.example.com` and invoke Worker B. ![Custom Domains can stack on top of each other, like any external dependencies](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=863,height=96,format=webp/_astro/custom-domains-subrequest.C6c84jN5.png) Custom Domains can also be invoked within the same zone via `fetch()`, unlike Routes. ## Add a Custom Domain To add a Custom Domain, you must have: 1. An [active Cloudflare zone](https://developers.cloudflare.com/dns/zone-setups/). 2. A Worker to invoke. Custom Domains can be attached to your Worker via the Cloudflare dashboard, [Wrangler](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/#set-up-a-custom-domain-in-your-wrangler-configuration-file) or the [API](https://developers.cloudflare.com/api/resources/workers/subresources/domains/methods/list/). Caution You cannot create a Custom Domain on a hostname with an existing CNAME DNS record or on a zone you do not own. ### Set up a Custom Domain in the dashboard To set up a Custom Domain in the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Go to **Settings** \> **Domains & Routes** \> **Add** \> **Custom Domain**. 4. Enter the domain you want to configure for your Worker. 5. Select **Add Custom Domain**. After you have added the domain or subdomain, Cloudflare will create a new DNS record for you. You can add multiple Custom Domains. ### Set up a Custom Domain in your Wrangler configuration file To configure a Custom Domain in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/), add the `custom_domain=true` option on each pattern under `routes`. For example, to configure a Custom Domain: ```jsonc { "routes": [ { "pattern": "shop.example.com", "custom_domain": true } ] } ``` ```toml [[routes]] pattern = "shop.example.com" custom_domain = true ``` To configure multiple Custom Domains: ```jsonc { "routes": [ { "pattern": "shop.example.com", "custom_domain": true }, { "pattern": "shop-two.example.com", "custom_domain": true } ] } ``` ```toml [[routes]] pattern = "shop.example.com" custom_domain = true [[routes]] pattern = "shop-two.example.com" custom_domain = true ``` ## Worker to Worker communication On the same zone, the only way for a Worker to communicate with another Worker running on a [route](https://developers.cloudflare.com/workers/configuration/routing/routes/#set-up-a-route), or on a [workers.dev](https://developers.cloudflare.com/workers/configuration/routing/routes/#%5Ftop) subdomain, is via [service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). On the same zone, if a Worker is attempting to communicate with a target Worker running on a Custom Domain rather than a route, the limitation is removed. Fetch requests sent on the same zone from one Worker to another Worker running on a Custom Domain will succeed without a service binding. For example, consider the following scenario, where both Workers are running on the `example.com` Cloudflare zone: * `worker-a` running on the [route](https://developers.cloudflare.com/workers/configuration/routing/routes/#set-up-a-route) `auth.example.com/*`. * `worker-b` running on the [route](https://developers.cloudflare.com/workers/configuration/routing/routes/#set-up-a-route) `shop.example.com/*`. If `worker-a` sends a fetch request to `worker-b`, the request will fail, because of the limitation on same-zone fetch requests. `worker-a` must have a service binding to `worker-b` for this request to resolve. ```js export default { fetch(request) { // This will fail return fetch("https://shop.example.com") } } ``` However, if `worker-b` was instead set up to run on the Custom Domain `shop.example.com`, the fetch request would succeed. ## Request matching behaviour Custom Domains do not support [wildcard DNS records](https://developers.cloudflare.com/dns/manage-dns-records/reference/wildcard-dns-records/). An incoming request must exactly match the domain or subdomain your Custom Domain is registered to. Other parts (path, query parameters) of the URL are not considered when executing this matching logic. For example, if you create a Custom Domain on `api.example.com` attached to your `api-gateway` Worker, a request to either `api.example.com/login` or `api.example.com/user` would invoke the same `api-gateway` Worker. ![Custom Domains follow standard DNS ordering and matching logic](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=876,height=224,format=webp/_astro/custom-domains-api-gateway.DmeJZDoL.png) ## Interaction with Routes A Worker running on a Custom Domain is treated as an origin. Any Workers running on routes before your Custom Domain can optionally call the Worker registered on your Custom Domain by issuing `fetch(request)` with the incoming `Request` object. That means that you are able to set up Workers to run before a request gets to your Custom Domain Worker. In other words, you can chain together two Workers in the same request. For example, consider the following workflow: 1. A Custom Domain for `api.example.com` points to your `api-worker` Worker. 2. A route added to `api.example.com/auth` points to your `auth-worker` Worker. 3. A request to `api.example.com/auth` will trigger your `auth-worker` Worker. 4. Using `fetch(request)` within the `auth-worker` Worker will invoke the `api-worker` Worker, as if it was a normal application server. ```js export default { fetch(request) { const url = new URL(request.url) if(url.searchParams.get("auth") !== "SECRET_TOKEN") { return new Response(null, { status: 401 }) } else { // This will invoke `api-worker` return fetch(request) } } } ``` ## Certificates Creating a Custom Domain will also generate an [Advanced Certificate](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/) on your target zone for your target hostname. These certificates are generated with default settings. To override these settings, delete the generated certificate and create your own certificate in the Cloudflare dashboard. Refer to [Manage advanced certificates](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/manage-certificates/) for instructions. Caution When you delete a Custom Domain, the associated Advanced Certificate is **not** automatically deleted. You must manually remove the certificate from the Cloudflare dashboard under **SSL/TLS** \> **Edge Certificates**, or via the [API](https://developers.cloudflare.com/api/resources/ssl/subresources/certificate%5Fpacks/methods/delete/). Leaving unused certificates in place does not affect functionality but may cause confusion when auditing your certificate inventory. ## Redirect between www and root domain Because Custom Domains require an exact hostname match, a Worker attached to `example.com` will not receive requests sent to `www.example.com`, and vice versa. To make both versions of your domain work, set up a redirect rule: * [Redirect from www to root](https://developers.cloudflare.com/rules/url-forwarding/examples/redirect-www-to-root/) * [Redirect from root to www](https://developers.cloudflare.com/rules/url-forwarding/examples/redirect-root-to-www/) You also need a [proxied DNS record](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/) for the hostname you are redirecting _from_, so that Cloudflare can apply the redirect rule. * For www to root: Add a proxied DNS `A` record for `www` pointing to `192.0.2.0`, or a proxied `AAAA` record pointing to `100::` * For root to www: Add a proxied DNS `A` record for your root domain pointing to `192.0.2.0`, or a proxied `AAAA` record pointing to `100::` Note `192.0.2.0` (A record) and `100::` (AAAA record) are [reserved placeholder addresses](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/#originless-setups) for originless setups. Because the DNS record is proxied, requests never reach this address — Cloudflare intercepts them and applies your redirect rule. ## Migrate from Routes If you are currently invoking a Worker using a [route](https://developers.cloudflare.com/workers/configuration/routing/routes/) with `/*`, and you have a CNAME record pointing to `100::` or similar, a Custom Domain is a recommended replacement. ### Migrate from Routes via the dashboard To migrate the route `example.com/*`: 1. In the Cloudflare dashboard, go to the **DNS Records** page for your domain. [Go to **Records** ↗](https://dash.cloudflare.com/?to=/:account/:zone/dns/records) 2. Delete the CNAME record for `example.com`. 3. Go to **Account Home** \> **Workers & Pages**. 4. In **Overview**, select your Worker > **Settings** \> **Domains & Routes**. 5. Select **Add** \> **Custom domain** and add `example.com`. 6. Delete the route `example.com/*` located in your Worker > **Settings** \> **Domains & Routes**. ### Migrate from Routes via Wrangler To migrate the route `example.com/*` in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/): 1. In the Cloudflare dashboard, go to the **DNS Records** page for your domain. [Go to **Records** ↗](https://dash.cloudflare.com/?to=/:account/:zone/dns/records) 2. Delete the CNAME record for `example.com`. 3. Add the following to your Wrangler file: ```jsonc { "routes": [ { "pattern": "example.com", "custom_domain": true } ] } ``` ```toml [[routes]] pattern = "example.com" custom_domain = true ``` 4. Run `npx wrangler deploy` to create the Custom Domain your Worker will run on. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/routing/custom-domains/#page","headline":"Custom Domains · Cloudflare Workers docs","description":"Connect a Cloudflare Worker to a domain or subdomain with automatic DNS and certificate management.","url":"https://developers.cloudflare.com/workers/configuration/routing/custom-domains/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Map URL patterns to Cloudflare Workers to run your code on matching requests. title: Routes image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Routes Last updated Jun 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/routing/routes/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Routes allow users to map a URL pattern to a Worker. When a request comes in to the Cloudflare network that matches the specified URL pattern, your Worker will execute on that route. Routes are a set of rules that evaluate against a request's URL. Routes are recommended for you if you have a designated application server you always need to communicate with. Calling `fetch()` on the incoming `Request` object will trigger a subrequest to your application server, as defined in the **DNS** settings of your Cloudflare zone. Routes add Workers functionality to your existing proxied hostnames, in front of your application server. These allow your Workers to act as a proxy and perform any necessary work before reaching out to an application server behind Cloudflare. ![Routes work with your applications defined in Cloudflare DNS](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1053,height=111,format=webp/_astro/routes-diagram.CfGSi1RG.png) Routes can `fetch()` Custom Domains and take precedence if configured on the same hostname. If you would like to run a logging Worker in front of your application, for example, you can create a Custom Domain on your application Worker for `app.example.com`, and create a Route for your logging Worker at `app.example.com/*`. Calling `fetch()` will invoke the application Worker on your Custom Domain. Note that Routes cannot be the target of a same-zone `fetch()` call. ## Set up a route To add a route, you must have: 1. An [active Cloudflare zone](https://developers.cloudflare.com/dns/zone-setups/). 2. A Worker to invoke. 3. A DNS record set up for the [domain](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-zone-apex/) or [subdomain](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-subdomain/) proxied by Cloudflare (also known as [orange-clouded](https://developers.cloudflare.com/dns/proxy-status/#benefits)) you would like to route to. Caution Route setup will differ depending on if your application's origin is a Worker or not. If your Worker is your application's origin, use [Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/). If your Worker is not your application's origin, follow the instructions below to set up a route. Note Routes can also be created via the API. Refer to the [Workers Routes API documentation](https://developers.cloudflare.com/api/resources/workers/subresources/routes/methods/create/) for more information. ### Set up a route in the dashboard Before you set up a route, make sure you have a DNS record set up for the [domain](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-zone-apex/) or [subdomain](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-subdomain/) you would like to route to. To set up a route in the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Go to **Settings** \> **Domains & Routes** \> **Add** \> **Route**. 4. Select the zone and enter the route pattern. 5. Select **Add route**. ### Set up a route in the Wrangler configuration file Before you set up a route, make sure you have a DNS record set up for the [domain](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-zone-apex/) or [subdomain](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-subdomain/) you would like to route to. To configure a route using your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/), refer to the following example. ```jsonc { "routes": [ { "pattern": "subdomain.example.com/*", "zone_name": "example.com" }, // or { "pattern": "subdomain.example.com/*", "zone_id": "" } ] } ``` ```toml [[routes]] pattern = "subdomain.example.com/*" zone_name = "example.com" [[routes]] pattern = "subdomain.example.com/*" zone_id = "" ``` Add the `zone_name` or `zone_id` option after each route. The `zone_name` and `zone_id` options are interchangeable. If using `zone_id`, find your zone ID by: 1. Go to the Zone Overview page in the Cloudflare dashboard. [Go to **Overview** ↗](https://dash.cloudflare.com/?to=/:account/:zone/) 2. Find the **Zone ID** in the left-hand side of **Overview**. To add multiple routes: ```jsonc { "routes": [ { "pattern": "subdomain.example.com/*", "zone_name": "example.com" }, { "pattern": "subdomain-two.example.com/example", "zone_id": "" } ] } ``` ```toml [[routes]] pattern = "subdomain.example.com/*" zone_name = "example.com" [[routes]] pattern = "subdomain-two.example.com/example" zone_id = "" ``` ## Matching behavior Route patterns look like this: ```txt https://*.example.com/images/* ``` This pattern would match all HTTPS requests destined for a subhost of example.com and whose paths are prefixed by `/images/`. A pattern to match all requests looks like this: ```txt *example.com/* ``` While they look similar to a [regex ↗](https://en.wikipedia.org/wiki/Regular%5Fexpression) pattern, route patterns follow specific rules: * The only supported operator is the wildcard (`*`), which matches zero or more of any character. * Route patterns may not contain infix wildcards or query parameters. For example, neither `example.com/*.jpg` nor `example.com/?foo=*` are valid route patterns. * When more than one route pattern could match a request URL, the most specific route pattern wins. For example, the pattern `www.example.com/*` would take precedence over `*.example.com/*` when matching a request for `https://www.example.com/`. The pattern `example.com/hello/*` would take precedence over `example.com/*` when matching a request for `example.com/hello/world`. * Route pattern matching considers the entire request URL, including the query parameter string. Since route patterns may not contain query parameters, the only way to have a route pattern match URLs with query parameters is to terminate it with a wildcard, `*`. * The path component of route patterns is case sensitive, for example, `example.com/Images/*` and `example.com/images/*` are two distinct routes. * For routes created before October 15th, 2023, the host component of route patterns is case sensitive, for example, `example.com/*` and `Example.com/*` are two distinct routes. * For routes created on or after October 15th, 2023, the host component of route patterns is not case sensitive, for example, `example.com/*` and `Example.com/*` are equivalent routes. A route can be specified without being associated with a Worker. This will act to negate any less specific patterns. For example, consider this pair of route patterns, one with a Workers script and one without: ```txt *example.com/images/cat.png -> *example.com/images/* -> worker-script ``` In this example, all requests destined for example.com and whose paths are prefixed by `/images/` would be routed to `worker-script`, _except_ for `/images/cat.png`, which would bypass Workers completely. Requests with a path of `/images/cat.png?foo=bar` would be routed to `worker-script`, due to the presence of the query string. ## Validity The following set of rules govern route pattern validity. #### Route patterns must include your zone If your zone is `example.com`, then the simplest possible route pattern you can have is `example.com`, which would match `http://example.com/` and `https://example.com/`, and nothing else. As with a URL, there is an implied path of `/` if you do not specify one. #### Route patterns may not contain any query parameters For example, `https://example.com/?anything` is not a valid route pattern. #### Route patterns may optionally begin with `http://` or `https://` If you omit a scheme in your route pattern, it will match both `http://` and `https://` URLs. If you include `http://` or `https://`, it will only match HTTP or HTTPS requests, respectively. * `https://*.example.com/` matches `https://www.example.com/` but not `http://www.example.com/`. * `*.example.com/` matches both `https://www.example.com/` and `http://www.example.com/`. #### Hostnames may optionally begin with `*` If a route pattern hostname begins with `*`, then it matches the host and all subhosts. If a route pattern hostname begins with `*.`, then it only matches all subhosts. * `*example.com/` matches `https://example.com/` and `https://www.example.com/`. * `*.example.com/` matches `https://www.example.com/` but not `https://example.com/`. Caution Because `*` matches zero or more of **any character** (not just subdomains), `*example.com` will also match hostnames that are not subdomains of `example.com`. If you only want to match `example.com` and its subdomains, use two separate routes (`example.com/*` and `*.example.com/*`) instead. The following examples illustrate the difference between `*example.com/*` and `*.example.com/*`: | Request URL | \*example.com/\* | \*.example.com/\* | | ---------------------------- | ---------------- | ----------------- | | https://example.com/ | Matches | Does not match | | https://www.example.com/path | Matches | Matches | | https://myexample.com/ | Matches | Does not match | | https://not-example.com/ | Does not match | Does not match | #### Paths may optionally end with `*` If a route pattern path ends with `*`, then it matches all suffixes of that path. * `https://example.com/path*` matches `https://example.com/path` and `https://example.com/path2` and `https://example.com/path/readme.txt` Caution There is a well-known bug associated with path matching concerning wildcards (`*`) and forward slashes (`/`) that is documented in [Known issues](https://developers.cloudflare.com/workers/platform/known-issues/). #### Domains and subdomains must have a DNS Record All domains and subdomains must have a [DNS record](https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/) to be proxied on Cloudflare and used to invoke a Worker. For example, if you want to put a Worker on `myname.example.com`, and you have added `example.com` to Cloudflare but have not added any DNS records for `myname.example.com`, any request to `myname.example.com` will result in the error `ERR_NAME_NOT_RESOLVED`. Caution If you have previously used the Cloudflare dashboard to add an `AAAA` record for `myname` to `example.com`, pointing to `100::` (the [reserved IPv6 discard prefix ↗](https://tools.ietf.org/html/rfc6666)), Cloudflare recommends creating a [Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) pointing to your Worker instead. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/routing/routes/#page","headline":"Routes · Cloudflare Workers docs","description":"Map URL patterns to Cloudflare Workers to run your code on matching requests.","url":"https://developers.cloudflare.com/workers/configuration/routing/routes/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-01","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Deploy Cloudflare Workers on a workers.dev subdomain for quick testing and personal projects. title: workers.dev image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # workers.dev Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Cloudflare Workers accounts come with a `workers.dev` subdomain that is configurable in the Cloudflare dashboard. Your `workers.dev` subdomain allows you getting started quickly by deploying Workers without first onboarding your custom domain to Cloudflare. It's recommended to run production Workers on a [Workers route or custom domain](https://developers.cloudflare.com/workers/configuration/routing/), rather than on your `workers.dev` subdomain. Your `workers.dev` subdomain is treated as a [Free website ↗](https://www.cloudflare.com/plans/) and is intended for personal or hobby projects that aren't business-critical. ## Configure `workers.dev` `workers.dev` subdomains take the format: `.workers.dev`. To change your `workers.dev` subdomain: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select **Change** next to **Your subdomain**. All Workers are assigned a `workers.dev` route when they are created or renamed following the syntax `..workers.dev`. The [name](https://developers.cloudflare.com/workers/wrangler/configuration/#inheritable-keys) field in your Worker configuration is used as the subdomain for the deployed Worker. ## Manage access to `workers.dev` When enabled, your `workers.dev` URL is available publicly. You can use [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/) to require visitors to authenticate before accessing your `workers.dev` URL. You can limit access to yourself, your teammates, your organization, or anyone else you specify in your [access policy](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/). To limit your `workers.dev` URL to authorized emails only: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Go to **Settings** \> **Domains & Routes**. 4. For `workers.dev`, click **Enable Cloudflare Access**. 5. Optionally, to configure the Access application, click **Manage Cloudflare Access**. There, you can change the email addresses you want to authorize. View [Access policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/#selectors) to learn about configuring alternate rules. 6. [Validate the Access JWT ↗](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#cloudflare-workers-example) in your Worker script using the audience (`aud`) tag and JWKs URL provided. ## Disabling `workers.dev` ### Disabling `workers.dev` in the dashboard To disable the `workers.dev` route for a Worker: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Go to **Settings** \> **Domains & Routes**. 4. On `workers.dev` click "Disable". 5. Confirm you want to disable. ### Disabling `workers.dev` in the Wrangler configuration file To disable the `workers.dev` route for a Worker, include the following in your Worker's [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/): ```jsonc { "workers_dev": false } ``` ```toml workers_dev = false ``` When you redeploy your Worker with this change, the `workers.dev` route will be disabled. Preview URLs default to matching your `workers_dev` setting unless explicitly configured. If you explicitly enabled Preview URLs, [disable them separately](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/#toggle-preview-urls-enable-or-disable). If you do not specify `workers_dev = false` but add a [routes component](https://developers.cloudflare.com/workers/wrangler/configuration/#routes) to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/), the value of `workers_dev` will be inferred as `false` on the next deploy. Caution If you disable your `workers.dev` route in the Cloudflare dashboard but do not update your Worker's Wrangler file with `workers_dev = false`, the `workers.dev` route will be re-enabled the next time you deploy your Worker with Wrangler. ## Limitations When deploying a Worker with a `workers.dev` subdomain enabled, your Worker name must meet the following requirements: * Must be 63 characters or less * Must contain only alphanumeric characters (`a-z`, `A-Z`, `0-9`) and dashes (`-`) * Cannot start or end with a dash (`-`) These restrictions apply because the Worker name is used as a DNS label in your `workers.dev` URL. DNS labels have a maximum length of 63 characters and cannot begin or end with a dash. Note Worker names can be up to 255 characters when not using a `workers.dev` subdomain. If you need a longer name, you can disable `workers.dev` and use [routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) or [custom domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) instead. ## Related resources * [Announcing workers.dev ↗](https://blog.cloudflare.com/announcing-workers-dev) * [Wrangler routes configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#types-of-routes) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/routing/workers-dev/#page","headline":"workers.dev · Cloudflare Workers docs","description":"Deploy Cloudflare Workers on a workers.dev subdomain for quick testing and personal projects.","url":"https://developers.cloudflare.com/workers/configuration/routing/workers-dev/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Store sensitive information, like API keys and auth tokens, in your Worker. title: Secrets image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Secrets Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/secrets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Secrets are a type of binding that allow you to attach encrypted text values to your Worker. Secrets are used for storing sensitive information like API keys and auth tokens. You can access secrets in your Worker code through: * The [env parameter](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#parameters) passed to your Worker's [fetch event handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). * Importing `env` from [cloudflare:workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) to access secrets from anywhere in your code. * [process.env](https://developers.cloudflare.com/workers/configuration/environment-variables) in Workers that have [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) enabled. ## Access your secrets with Workers Secrets can be accessed from Workers as you would any other [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). For instance, given a `DB_CONNECTION_STRING` secret, you can access it in your Worker code through the `env` parameter: ```js import postgres from "postgres"; export default { async fetch(request, env, ctx) { const sql = postgres(env.DB_CONNECTION_STRING); const result = await sql`SELECT * FROM products;`; return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" }, }); }, }; ``` You can also import `env` from `cloudflare:workers` to access secrets from anywhere in your code, including outside of request handlers: ```js import { env } from "cloudflare:workers"; import postgres from "postgres"; // Initialize the database client at the top level using a secret const sql = postgres(env.DB_CONNECTION_STRING); export default { async fetch(request) { const result = await sql`SELECT * FROM products;`; return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" }, }); }, }; ``` ```ts import { env } from "cloudflare:workers"; import postgres from "postgres"; // Initialize the database client at the top level using a secret const sql = postgres(env.DB_CONNECTION_STRING); export default { async fetch(request: Request): Promise { const result = await sql`SELECT * FROM products;`; return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" }, }); }, }; ``` For more details on accessing `env` globally, refer to [Importing env as a global](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global). Secrets Store (beta) Secrets described on this page are defined and managed on a per-Worker level. If you want to use account-level secrets, refer to [Secrets Store](https://developers.cloudflare.com/secrets-store/). Account-level secrets are configured on your Worker as a [Secrets Store binding](https://developers.cloudflare.com/secrets-store/integrations/workers/). ## Local Development with Secrets Caution Do not use `vars` to store sensitive information in your Worker's Wrangler configuration file. Use secrets instead. Put secrets for use in local development in either a `.dev.vars` file or a `.env` file, in the same directory as the Wrangler configuration file. Note You can use the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) to declare which secret names your Worker requires. When defined, only the keys listed in `secrets.required` are loaded from `.dev.vars` or `.env`. Additional keys are excluded and missing keys produce a warning. Note Choose to use either `.dev.vars` or `.env` but not both. If you define a `.dev.vars` file, then values in `.env` files will not be included in the `env` object during local development. These files should be formatted using the [dotenv ↗](https://hexdocs.pm/dotenvy/dotenv-file-format.html) syntax. For example: ```bash SECRET_KEY="value" API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ``` Do not commit secrets to git The `.dev.vars` and `.env` files should not be committed to git. Add `.dev.vars*` and `.env*` to your project's `.gitignore` file. To set different secrets for each Cloudflare environment, create files named `.dev.vars.` or `.env.`. When you select a Cloudflare environment in your local development, the corresponding environment-specific file will be loaded ahead of the generic `.dev.vars` (or `.env`) file. * When using `.dev.vars.` files, all secrets must be defined per environment. If `.dev.vars.` exists then only this will be loaded; the `.dev.vars` file will not be loaded. * In contrast, all matching `.env` files are loaded and the values are merged. For each variable, the value from the most specific file is used, with the following precedence: * `.env..local` (most specific) * `.env.local` * `.env.` * `.env` (least specific) Controlling \`.env\` handling It is possible to control how `.env` files are loaded in local development by setting environment variables on the process running the tools. * To disable loading local dev vars from `.env` files without providing a `.dev.vars` file, set the `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV` environment variable to `"false"`. * To include every environment variable defined in your system's process environment as a local development variable, ensure there is no `.dev.vars` and then set the `CLOUDFLARE_INCLUDE_PROCESS_ENV` environment variable to `"true"`. This is not needed when using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property), which loads from `process.env` automatically. ## Secrets on deployed Workers ### Validate secrets before deploy You can declare the secret names your Worker requires using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) in your Wrangler configuration. When defined, `wrangler deploy` and `wrangler versions upload` will fail with a clear error if any required secrets are not configured on the Worker. ### Adding secrets to your project #### Via Wrangler Secrets can be added through [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) or [wrangler versions secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-secret-put) commands. `wrangler secret put` creates a new version of the Worker and deploys it immediately. ```sh npx wrangler secret put ``` If using [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/), instead use the `wrangler versions secret put` command. This will only create a new version of the Worker, that can then be deploying using [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-deploy). Note Wrangler versions before 3.73.0 require you to specify a `--x-versions` flag. ```sh npx wrangler versions secret put ``` #### Via the dashboard To add a secret via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker > **Settings**. 3. Under **Variables and Secrets**, select **Add**. 4. Select the type **Secret**, input a **Variable name**, and input its **Value**. This secret will be made available to your Worker but the value will be hidden in Wrangler and the dashboard. 5. (Optional) To add more secrets, select **Add variable**. 6. Select **Deploy** to implement your changes. #### Upload secrets alongside code You can upload secrets at the same time as your Worker code using the `--secrets-file` flag on [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy) or [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-upload). This accepts a path to a JSON or `.env` file — the same formats accepted by [wrangler secret bulk](https://developers.cloudflare.com/workers/wrangler/commands/workers/#secret-bulk). You can upload up to 100 secrets per bulk request for a single version. ```sh npx wrangler deploy --secrets-file .env.production ``` ```sh npx wrangler versions upload --secrets-file secrets.json ``` Secrets not included in the file are preserved from the previous version. This is useful in CI/CD pipelines where you want to deploy code and update secrets in a single operation. ### Delete secrets from your project #### Via Wrangler Secrets can be deleted through [wrangler secret delete](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret-delete) or [wrangler versions secret delete](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-secret-delete) commands. `wrangler secret delete` creates a new version of the Worker and deploys it immediately. ```sh npx wrangler secret delete ``` If using [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/), instead use the `wrangler versions secret delete` command. This will only create a new version of the Worker, that can then be deploying using [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-deploy). ```sh npx wrangler versions secret delete ``` #### Via the dashboard To delete a secret from your Worker project via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker > **Settings**. 3. Under **Variables and Secrets**, select **Edit**. 4. In the **Edit** drawer, select **X** next to the secret you want to delete. 5. Select **Deploy** to implement your changes. 6. (Optional) Instead of using the edit drawer, you can click the delete icon next to the secret. ## Compare secrets and environment variables Use secrets for sensitive information Do not use plaintext environment variables to store sensitive information. Use [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) or [Secrets Store bindings](https://developers.cloudflare.com/secrets-store/integrations/workers/) instead. [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) are [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). The difference is secret values are not visible within Wrangler or Cloudflare dashboard after you define them. This means that sensitive data, including passwords or API tokens, should always be encrypted to prevent data leaks. To your Worker, there is no difference between an environment variable and a secret. The secret's value is passed through as defined. ## Related resources * [Wrangler secret commands](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) \- Review the Wrangler commands to create, delete and list secrets. * [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) \- Declare required secret names in your Wrangler configuration. Used for validation during local development and deploy, and as the source of truth for type generation. * [Cloudflare Secrets Store](https://developers.cloudflare.com/secrets-store/) \- Encrypt and store sensitive information as secrets that are securely reusable across your account. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/secrets/#page","headline":"Secrets · Cloudflare Workers docs","description":"Store sensitive information, like API keys and auth tokens, in your Worker.","url":"https://developers.cloudflare.com/workers/configuration/secrets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use [Workers Static Assets](/workers/static-assets/) to host full-stack applications instead of Workers Sites. Do not use Workers Sites for new projects. title: Workers Sites image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Workers Sites Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/sites/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Use Workers Static Assets Instead You should use [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) to host full-stack applications instead of Workers Sites. It has been deprecated in Wrangler v4, and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) does not support Workers Sites. Do not use Workers Sites for new projects. Workers Sites enables developers to deploy static applications directly to Workers. It can be used for deploying applications built with static site generators like [Hugo ↗](https://gohugo.io) and [Gatsby ↗](https://www.gatsbyjs.org), or front-end frameworks like [Vue ↗](https://vuejs.org) and [React ↗](https://reactjs.org). To deploy with Workers Sites, select from one of these three approaches depending on the state of your target project: --- ## 1\. Start from scratch If you are ready to start a brand new project, this quick start guide will help you set up the infrastructure to deploy a HTML website to Workers. [Start from scratch](https://developers.cloudflare.com/workers/configuration/sites/start-from-scratch/) --- ## 2\. Deploy an existing static site If you have an existing project or static assets that you want to deploy with Workers, this quick start guide will help you install Wrangler and configure Workers Sites for your project. [Start from an existing static site](https://developers.cloudflare.com/workers/configuration/sites/start-from-existing/) --- ## 3\. Add static assets to an existing Workers project If you already have a Worker deployed to Cloudflare, this quick start guide will show you how to configure the existing codebase to use Workers Sites. [Start from an existing Worker](https://developers.cloudflare.com/workers/configuration/sites/start-from-worker/) Note Workers Sites is built on Workers KV, and usage rates may apply. Refer to [Pricing](https://developers.cloudflare.com/workers/platform/pricing/) to learn more. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/static-assets/#page","headline":"Workers Sites · Cloudflare Workers docs","description":"Use Workers Static Assets to host full-stack applications instead of Workers Sites. Do not use Workers Sites for new projects.","url":"https://developers.cloudflare.com/workers/static-assets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Configure Workers Sites settings for static asset hosting in your Wrangler configuration file. title: Workers Sites configuration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Workers Sites configuration Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/sites/configuration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Use Workers Static Assets Instead You should use [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) to host full-stack applications instead of Workers Sites. It has been deprecated in Wrangler v4, and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) does not support Workers Sites. Do not use Workers Sites for new projects. Workers Sites require the latest version of [Wrangler ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler). ## Wrangler configuration file There are a few specific configuration settings for Workers Sites in your Wrangler file: * `bucket` required * The directory containing your static assets, path relative to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). Example: `bucket = "./public"`. * `include` optional * A list of gitignore-style patterns for files or directories in `bucket` you exclusively want to upload. Example: `include = ["upload_dir"]`. * `exclude` optional * A list of gitignore-style patterns for files or directories in `bucket` you want to exclude from uploads. Example: `exclude = ["ignore_dir"]`. To learn more about the optional `include` and `exclude` fields, refer to [Ignoring subsets of static assets](#ignoring-subsets-of-static-assets). Note If your project uses [environments](https://developers.cloudflare.com/workers/wrangler/environments/), make sure to place `site` above any environment-specific configuration blocks. Example of a [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/): ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "docs-site-blah", "site": { "bucket": "./public" }, "env": { "production": { "name": "docs-site", "route": "https://example.com/docs*" }, "staging": { "name": "docs-site-staging", "route": "https://staging.example.com/docs*" } } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "docs-site-blah" [site] bucket = "./public" [env.production] name = "docs-site" route = "https://example.com/docs*" [env.staging] name = "docs-site-staging" route = "https://staging.example.com/docs*" ``` ## Storage limits For very exceptionally large pages, Workers Sites might not work for you. There is a 25 MiB limit per page or file. ## Ignoring subsets of static assets Workers Sites require [Wrangler ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) \- make sure to use the [latest version](https://developers.cloudflare.com/workers/wrangler/install-and-update/#update-wrangler). There are cases where users may not want to upload certain static assets to their Workers Sites. In this case, Workers Sites can also be configured to ignore certain files or directories using logic similar to [Cargo's optional include and exclude fields ↗](https://doc.rust-lang.org/cargo/reference/manifest.html#the-exclude-and-include-fields-optional). This means that you should use gitignore semantics when declaring which directory entries to include or ignore in uploads. ### Exclusively including files/directories If you want to include only a certain set of files or directories in your `bucket`, you can add an `include` field to your `[site]` section of your Wrangler file: ```jsonc { "site": { "bucket": "./public", "include": [ // must be an array. "included_dir" ] } } ``` ```toml [site] bucket = "./public" include = [ "included_dir" ] ``` Wrangler will only upload files or directories matching the patterns in the `include` array. ### Excluding files/directories If you want to exclude files or directories in your `bucket`, you can add an `exclude` field to your `[site]` section of your Wrangler file: ```jsonc { "site": { "bucket": "./public", "exclude": [ // must be an array. "excluded_dir" ] } } ``` ```toml [site] bucket = "./public" exclude = [ "excluded_dir" ] ``` Wrangler will ignore files or directories matching the patterns in the `exclude` array when uploading assets to Workers KV. ### Include > exclude If you provide both `include` and `exclude` fields, the `include` field will be used and the `exclude` field will be ignored. ### Default ignored entries Wrangler will always ignore: * `node_modules` * Hidden files and directories * Symlinks #### More about include/exclude patterns Learn more about the standard patterns used for include and exclude in the [gitignore documentation ↗](https://git-scm.com/docs/gitignore). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/#page","headline":"Workers Sites configuration · Cloudflare Workers docs","description":"Configure Workers Sites settings for static asset hosting in your Wrangler configuration file.","url":"https://developers.cloudflare.com/workers/static-assets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Deploy an existing static site project to Cloudflare using Workers Sites. title: Start from existing image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Start from existing Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/sites/start-from-existing/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Use Workers Static Assets Instead You should use [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) to host full-stack applications instead of Workers Sites. It has been deprecated in Wrangler v4, and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) does not support Workers Sites. Do not use Workers Sites for new projects. Workers Sites require [Wrangler ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) — make sure to use the [latest version](https://developers.cloudflare.com/workers/wrangler/install-and-update/#update-wrangler). To deploy a pre-existing static site project, start with a pre-generated site. Workers Sites works with all static site generators, for example: * [Hugo ↗](https://gohugo.io/getting-started/quick-start/) * [Gatsby ↗](https://www.gatsbyjs.org/docs/quick-start/), requires Node * [Jekyll ↗](https://jekyllrb.com/docs/), requires Ruby * [Eleventy ↗](https://www.11ty.io/#quick-start), requires Node * [WordPress ↗](https://wordpress.org) (refer to the tutorial on [deploying static WordPress sites with Pages](https://developers.cloudflare.com/pages/how-to/deploy-a-wordpress-site/)) ## Getting started 1. Run the `wrangler init` command in the root of your project's directory to generate a basic Worker: ```sh wrangler init -y ``` This command adds/update the following files: * `wrangler.jsonc`: The file containing project configuration. * `package.json`: Wrangler `devDependencies` are added. * `tsconfig.json`: Added if not already there to support writing the Worker in TypeScript. * `src/index.ts`: A basic Cloudflare Worker, written in TypeScript. 2. Add your site's build/output directory to the Wrangler file: ```jsonc { "site": { "bucket": "./public" // <-- Add your build directory name here. } } ``` ```toml [site] bucket = "./public" ``` The default directories for the most popular static site generators are listed below: * Hugo: `public` * Gatsby: `public` * Jekyll: `_site` * Eleventy: `_site` 3. Install the `@cloudflare/kv-asset-handler` package in your project: ```sh npm i -D @cloudflare/kv-asset-handler ``` 4. Replace the contents of `src/index.ts` with the following code snippet: ```js import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; import manifestJSON from "__STATIC_CONTENT_MANIFEST"; const assetManifest = JSON.parse(manifestJSON); export default { async fetch(request, env, ctx) { try { // Add logic to decide whether to serve an asset or run your original Worker code return await getAssetFromKV( { request, waitUntil: ctx.waitUntil.bind(ctx), }, { ASSET_NAMESPACE: env.__STATIC_CONTENT, ASSET_MANIFEST: assetManifest, }, ); } catch (e) { let pathname = new URL(request.url).pathname; return new Response(`"${pathname}" not found`, { status: 404, statusText: "not found", }); } }, }; ``` Service Workers are deprecated Service Workers are deprecated, but still supported. We recommend using [Module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) instead. New features may not be supported for Service Workers. ```js import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; addEventListener("fetch", (event) => { event.respondWith(handleEvent(event)); }); async function handleEvent(event) { try { // Add logic to decide whether to serve an asset or run your original Worker code return await getAssetFromKV(event); } catch (e) { let pathname = new URL(event.request.url).pathname; return new Response(`"${pathname}" not found`, { status: 404, statusText: "not found", }); } } ``` 1. Run `wrangler dev` or `npx wrangler deploy` to preview or deploy your site on Cloudflare. Wrangler will automatically upload the assets found in the configured directory. ```sh npx wrangler deploy ``` 2. Deploy your site to a [custom domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) that you own and have already attached as a Cloudflare zone. Add a `route` property to the Wrangler file. ```jsonc { "route": "https://example.com/*" } ``` ```toml route = "https://example.com/*" ``` Note Refer to the documentation on [Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) to configure a `route` properly. Learn more about [configuring your project](https://developers.cloudflare.com/workers/wrangler/configuration/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/#page","headline":"Start from existing · Cloudflare Workers docs","description":"Deploy an existing static site project to Cloudflare using Workers Sites.","url":"https://developers.cloudflare.com/workers/static-assets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Create a new Workers Sites project from scratch with Wrangler. title: Start from scratch image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Start from scratch Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/sites/start-from-scratch/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Use Workers Static Assets Instead You should use [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) to host full-stack applications instead of Workers Sites. It has been deprecated in Wrangler v4, and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) does not support Workers Sites. Do not use Workers Sites for new projects. This guide shows how to quickly start a new Workers Sites project from scratch. ## Getting started 1. Ensure you have the latest version of [git ↗](https://git-scm.com/downloads) and [Node.js ↗](https://nodejs.org/en/download/) installed. 2. In your terminal, clone the `worker-sites-template` starter repository. The following example creates a project called `my-site`: ```sh git clone --depth=1 --branch=wrangler2 https://github.com/cloudflare/worker-sites-template my-site ``` 3. Run `npm install` to install all dependencies. 4. You can preview your site by running the [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) command: ```sh wrangler dev ``` 5. Deploy your site to Cloudflare: ```sh npx wrangler deploy ``` ## Project layout The template project contains the following files and directories: * `public`: The static assets for your project. By default it contains an `index.html` and a `favicon.ico`. * `src`: The Worker configured for serving your assets. You do not need to edit this but if you want to see how it works or add more functionality to your Worker, you can edit `src/index.ts`. * `wrangler.jsonc`: The file containing project configuration. The `bucket` property tells Wrangler where to find the static assets (e.g. `site = { bucket = "./public" }`). * `package.json`/`package-lock.json`: define the required Node.js dependencies. ## Customize the `wrangler.jsonc` file: * Change the `name` property to the name of your project: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-site" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-site" ``` * Consider updating`compatibility_date` to today's date to get access to the most recent Workers features: ```jsonc { "compatibility_date": "yyyy-mm-dd" } ``` ```toml compatibility_date = "yyyy-mm-dd" ``` * Deploy your site to a [custom domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) that you own and have already attached as a Cloudflare zone: ```jsonc { "route": "https://example.com/*" } ``` ```toml route = "https://example.com/*" ``` Note Refer to the documentation on [Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) to configure a `route` properly. Learn more about [configuring your project](https://developers.cloudflare.com/workers/wrangler/configuration/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/#page","headline":"Start from scratch · Cloudflare Workers docs","description":"Create a new Workers Sites project from scratch with Wrangler.","url":"https://developers.cloudflare.com/workers/static-assets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Add static asset serving to an existing Cloudflare Worker using Workers Sites. title: Start from Worker image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Start from Worker Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/sites/start-from-worker/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Use Workers Static Assets Instead You should use [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/) to host full-stack applications instead of Workers Sites. It has been deprecated in Wrangler v4, and the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/) does not support Workers Sites. Do not use Workers Sites for new projects. Workers Sites require [Wrangler ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/wrangler) — make sure to use the [latest version](https://developers.cloudflare.com/workers/wrangler/install-and-update/#update-wrangler). If you have a pre-existing Worker project, you can use Workers Sites to serve static assets to the Worker. ## Getting started 1. Create a directory that will contain the assets in the root of your project (for example, `./public`) 2. Add configuration to your Wrangler file to point to it. ```jsonc { "site": { "bucket": "./public" // Add the directory with your static assets! } } ``` ```toml [site] bucket = "./public" ``` 3. Install the `@cloudflare/kv-asset-handler` package in your project: ```sh npm i -D @cloudflare/kv-asset-handler ``` 4. Import the `getAssetFromKV()` function into your Worker entry point and use it to respond with static assets. ```js import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; import manifestJSON from "__STATIC_CONTENT_MANIFEST"; const assetManifest = JSON.parse(manifestJSON); export default { async fetch(request, env, ctx) { try { // Add logic to decide whether to serve an asset or run your original Worker code return await getAssetFromKV( { request, waitUntil: ctx.waitUntil.bind(ctx), }, { ASSET_NAMESPACE: env.__STATIC_CONTENT, ASSET_MANIFEST: assetManifest, }, ); } catch (e) { let pathname = new URL(request.url).pathname; return new Response(`"${pathname}" not found`, { status: 404, statusText: "not found", }); } }, }; ``` ```js import { getAssetFromKV } from "@cloudflare/kv-asset-handler"; addEventListener("fetch", (event) => { event.respondWith(handleEvent(event)); }); async function handleEvent(event) { try { // Add logic to decide whether to serve an asset or run your original Worker code return await getAssetFromKV(event); } catch (e) { let pathname = new URL(event.request.url).pathname; return new Response(`"${pathname}" not found`, { status: 404, statusText: "not found", }); } } ``` For more information on the configurable options of `getAssetFromKV()` refer to [kv-asset-handler docs ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/kv-asset-handler). 1. Run `wrangler deploy` or `npx wrangler deploy` as you would normally with your Worker project. Wrangler will automatically upload the assets found in the configured directory. ```sh npx wrangler deploy ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/#page","headline":"Start from Worker · Cloudflare Workers docs","description":"Add static asset serving to an existing Cloudflare Worker using Workers Sites.","url":"https://developers.cloudflare.com/workers/static-assets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Review the interaction between various Page Rules and Workers. title: Page Rules image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Page Rules Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Page Rules trigger certain actions whenever a request matches one of the URL patterns you define. You can define a page rule to trigger one or more actions whenever a certain URL pattern is matched. Refer to [Page Rules](https://developers.cloudflare.com/rules/page-rules/) to learn more about configuring Page Rules. ## Page Rules with Workers Cloudflare acts as a [reverse proxy ↗](https://www.cloudflare.com/learning/what-is-cloudflare/) to provide services, like Page Rules, to Internet properties. Your application's traffic will pass through a Cloudflare data center that is closest to the visitor. There are hundreds of these around the world, each of which are capable of running services like Workers and Page Rules. If your application is built on Workers and/or Pages, the [Cloudflare global network ↗](https://www.cloudflare.com/learning/serverless/glossary/what-is-edge-computing/) acts as your origin server and responds to requests directly from the Cloudflare global network. When using Page Rules with Workers, the following workflow is applied. 1. Request arrives at Cloudflare data center. 2. Cloudflare decides if this request is a Worker route. Because this is a Worker route, Cloudflare evaluates and disabled a number of features, including some that would be set by Page Rules. 3. Page Rules run as part of normal request processing with some features now disabled. 4. Worker executes. 5. Worker makes a same-zone or other-zone subrequest. Because this is a Worker route, Cloudflare disables a number of features, including some that would be set by Page Rules. Page Rules are evaluated both at the client-to-Worker request stage (step 2) and the Worker subrequest stage (step 5). If you are experiencing Page Rule errors when running Workers, contact your Cloudflare account team or [Cloudflare Support](https://developers.cloudflare.com/support/contacting-cloudflare-support/). ## Affected Page Rules The following Page Rules may not work as expected when an incoming request is matched to a Worker route: * Always Online * [Always Use HTTPS](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#always-use-https) * [Automatic HTTPS Rewrites](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#automatic-https-rewrites) * [Browser Cache TTL](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#browser-cache-ttl) * [Browser Integrity Check](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#browser-integrity-check) * [Cache Deception Armor](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#cache-deception-armor) * [Cache Level](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#cache-level) * Disable Apps * [Disable Zaraz](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#disable-zaraz) * [Edge Cache TTL](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#edge-cache-ttl) * [Email Obfuscation](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#email-obfuscation) * [Forwarding URL](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#forwarding-url) * Host Header Override * [IP Geolocation Header](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#ip-geolocation-header) * [Origin Cache Control](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#origin-cache-control) * [Rocket Loader](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#rocket-loader) * [Security Level](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#security-level) * [SSL](https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#ssl) This is because the default setting of these Page Rules will be disabled when Cloudflare recognizes that the request is headed to a Worker. Testing Due to ongoing changes to the Workers runtime, detailed documentation on how these rules will be affected are updated following testing. To learn what these Page Rules do, refer to [Page Rules](https://developers.cloudflare.com/rules/page-rules/). Same zone versus other zone A same zone subrequest is a request the Worker makes to an orange-clouded hostname in the same zone the Worker runs on. Depending on your DNS configuration, any request that falls outside that definition may be considered an other zone request by the Cloudflare network. ### Always Use HTTPS | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Ignored | | Worker | Other Zone | Rule Ignored | ### Automatic HTTPS Rewrites | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Ignored | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Browser Cache TTL | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Ignored | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Browser Integrity Check | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Ignored | | Worker | Other Zone | Rule Ignored | ### Cache Deception Armor | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Cache Level | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Disable Zaraz | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Edge Cache TTL | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Email Obfuscation | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Ignored | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Forwarding URL | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Ignored | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### IP Geolocation Header | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Origin Cache Control | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | ### Rocket Loader | Source | Target | Behavior | | ------ | ---------- | ------------ | | Client | Worker | Rule Ignored | | Worker | Same Zone | Rule Ignored | | Worker | Other Zone | Rule Ignored | ### Security Level | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Ignored | | Worker | Other Zone | Rule Ignored | ### SSL | Source | Target | Behavior | | ------ | ---------- | -------------- | | Client | Worker | Rule Respected | | Worker | Same Zone | Rule Respected | | Worker | Other Zone | Rule Ignored | Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/#page","headline":"Page Rules with Workers · Cloudflare Workers docs","description":"Review the interaction between various Page Rules and Workers.","url":"https://developers.cloudflare.com/workers/configuration/workers-with-page-rules/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Understand how Workers tracks changes with versions and releases them with deployments. title: Versions & deployments image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Versions & deployments Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Every time you change your Worker's code or configuration, Workers creates a **version**. A **deployment** determines which version(s) are actively serving traffic. ![Versions and Deployments](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1338,height=878,format=webp/_astro/versions-and-deployments.Dnwtp7bX.png) ## Versions A version captures the complete state of your Worker at a point in time: its [bundled code](https://developers.cloudflare.com/workers/wrangler/bundling/), [static assets](https://developers.cloudflare.com/workers/static-assets/), [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), and [compatibility settings](https://developers.cloudflare.com/workers/configuration/compatibility-dates/). Each version has a unique ID and tracks who created it, when, and from where. You can optionally attach a message and tag to a version when you upload it. Note State changes for associated [storage resources](https://developers.cloudflare.com/workers/platform/storage-options/) such as [KV](https://developers.cloudflare.com/kv/), [R2](https://developers.cloudflare.com/r2/), [Durable Objects](https://developers.cloudflare.com/durable-objects/), and [D1](https://developers.cloudflare.com/d1/) are not tracked with versions. ## Deployments A deployment determines which version(s) of your Worker are actively serving traffic. A deployment can reference one version (serving 100% of traffic) or two versions (with traffic split between them during a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/)). Each deployment tracks who created it, when, and which version(s) it includes. ## Default behavior By default, these two concepts are coupled together - when you run [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy), Workers creates a new version and immediately deploys it to 100% of traffic in a single step. You can decouple them so that uploading a version and deploying it are independent actions. This gives you control over when new code goes live, and lets you use strategies like [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) or manual promotion. Refer to [Deployment management](https://developers.cloudflare.com/workers/versions-and-deployments/deployment-management/) for details. ## View versions and deployments ### Via Wrangler Wrangler allows you to view the 100 most recent versions and deployments. Refer to the [versions list](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-list) and [deployments list](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deployments-list) documentation for the commands. ### Via the Cloudflare dashboard 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker > **Deployments**. ## Next steps * [Deployment management](https://developers.cloudflare.com/workers/versions-and-deployments/deployment-management/) \- Upload versions without deploying them and control when they go live * [Preview URLs](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) \- Test new versions before deploying them to production * [Gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) \- Split traffic between two versions using percentage-based routing * [Version affinity](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/) \- Consistently route users to the same version across page loads during a gradual deployment * [Version overrides](https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/) \- Send a request to a specific version by ID for smoke testing and pinning between Workers * [Rollbacks](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/) \- Revert to a previously deployed version Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/#page","headline":"Versions & deployments · Cloudflare Workers docs","description":"Understand how Workers tracks changes with versions and releases them with deployments.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Upload versions independently and control when and how they are deployed to your Worker's traffic. title: Deployment management image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Deployment management Last updated Jul 15, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/deployment-management/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) By default, a new version is created and immediately deployed to 100% of traffic when you use any of the following: * [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy) * [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/) * The [Workers Script Upload API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/) You can separate these steps so that uploading a version and deploying it are independent actions. This lets you control exactly when a new version goes live. ## Upload a version without deploying ### Via Wrangler Use the [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-upload) command: npmyarnpnpm ``` npx wrangler versions upload ``` ``` yarn wrangler versions upload ``` ``` pnpm wrangler versions upload ``` Note Wrangler versions before 3.73.0 require you to specify a `--x-versions` flag. Note To apply changes to a Worker's triggers ([routes, domains](https://developers.cloudflare.com/workers/configuration/routing/), or [cron triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/)), use the [wrangler triggers deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#triggers-deploy) command. ### Via the Cloudflare dashboard 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker > **Edit code**. 3. Make your changes, then select the **down arrow** next to **Deploy** \> **Save**. Note New versions are not created when you make changes to [resources connected to your Worker](https://developers.cloudflare.com/workers/runtime-apis/bindings/). For example, if two Workers (Worker A and Worker B) are connected via a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/), changing the code of Worker B will not create a new version of Worker A. Changes to the service binding configuration (such as deleting the binding or updating the [environment](https://developers.cloudflare.com/workers/wrangler/environments/) it points to) on Worker A will also not create a new version of Worker B. ## Deploy an uploaded version Once you have uploaded a version, you can create a deployment that routes traffic to it. ### Via Wrangler Use the [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-deploy) command and follow the interactive prompts to select the version and set it to 100%: npmyarnpnpm ``` npx wrangler versions deploy ``` ``` yarn wrangler versions deploy ``` ``` pnpm wrangler versions deploy ``` You can also set the traffic percentage to less than 100% to start a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/). ### Via the Cloudflare dashboard 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker > **Deployments**. 3. Select **Promote deployment** and choose the version you want to deploy. ### Via Infrastructure as Code You can also create versions and deployments directly with the API, library SDKs, and Terraform. Refer to [Infrastructure as Code](https://developers.cloudflare.com/workers/platform/infrastructure-as-code/) for examples. ## Limits ### Deployments limit You can only create a deployment with the last 100 uploaded versions of your Worker. ### First upload You must use [C3](https://developers.cloudflare.com/workers/get-started/guide/#1-create-a-new-worker-project) or [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy) the first time you create a new Workers project. Using [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-upload) the first time you upload a Worker will fail. ### Service worker syntax Service worker syntax is not supported for versions that are uploaded through [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-upload). You must use ES modules format. Refer to [Migrate from Service Workers to ES modules](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/#advantages-of-migrating) to learn how to migrate your Workers from the service worker format to the ES modules format. ### Durable Object migrations Uploading a version that changes Durable Object class lifecycle is not supported. This applies to both the declarative [exports](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) field and the legacy [migrations](https://developers.cloudflare.com/durable-objects/reference/durable-object-class-migrations-legacy/) array - any change that creates, deletes, renames, or transfers a Durable Object class must be applied through [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/deployment-management/#page","headline":"Deployment management · Cloudflare Workers docs","description":"Upload versions independently and control when and how they are deployed to your Worker's traffic.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/deployment-management/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-15","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Incrementally deploy code changes to your Workers with gradual deployments. title: Gradual deployments image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Gradual deployments Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Gradual deployments let you incrementally deploy new [versions](https://developers.cloudflare.com/workers/versions-and-deployments/#versions) of your Worker by splitting traffic across versions. Instead of shifting all traffic to a new version at once, you can route a percentage of requests to the new version while the rest continue to be handled by the previous version. ![Gradual Deployments](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1260,height=590,format=webp/_astro/gradual-deployments.C6F9MQ6U.png) Using gradual deployments, you can: * Gradually shift traffic to a newer version of your Worker * Monitor error rates and exceptions across versions using [observability](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/#observability) tooling * [Roll back](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/) to a previously stable version if you notice issues ## Use gradual deployments The following section guides you through an example usage of gradual deployments. ### Via Wrangler Note Minimum required Wrangler version: 3.40.0\. Versions before 3.73.0 require you to specify a `--x-versions` flag. #### 1\. Create and deploy a new Worker Create a new `"Hello World"` Worker using the [create-cloudflare CLI (C3)](https://developers.cloudflare.com/pages/get-started/c3/) and deploy it. npmyarnpnpm ``` npm create cloudflare@latest -- -- --type=hello-world ``` ``` yarn create cloudflare -- --type=hello-world ``` ``` pnpm create cloudflare@latest -- --type=hello-world ``` Answer `yes` or `no` to using TypeScript. Answer `yes` to deploying your application. This is the first version of your Worker. #### 2\. Create a new version of the Worker Edit the Worker code by changing the `Response` content and upload the Worker using the [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-upload) command. npmyarnpnpm ``` npx wrangler versions upload ``` ``` yarn wrangler versions upload ``` ``` pnpm wrangler versions upload ``` This will create a new version of the Worker that is not automatically deployed. #### 3\. Create a new deployment Use the [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-deploy) command to create a new deployment that splits traffic between two versions. Follow the interactive prompts to select your desired percentages for each version. npmyarnpnpm ``` npx wrangler versions deploy ``` ``` yarn wrangler versions deploy ``` ``` pnpm wrangler versions deploy ``` #### 4\. Test the split deployment Run a cURL command on your Worker to test the split deployment. ```bash for j in {1..10} do curl -s https://$WORKER_NAME.$SUBDOMAIN.workers.dev done ``` You should see 10 responses. Responses will vary depending on the percentages configured in [step #3](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/#3-create-a-new-deployment). You can also target a specific version using [version overrides](https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/). #### 5\. Set your new version to 100% deployment Run `wrangler versions deploy` again and follow the interactive prompts. Select the new version and set it to 100%. npmyarnpnpm ``` npx wrangler versions deploy ``` ``` yarn wrangler versions deploy ``` ``` pnpm wrangler versions deploy ``` ### Via the Cloudflare dashboard 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select **Create application** \> **Hello World** template > deploy your Worker. 3. Once the Worker is deployed, go to the online code editor through **Edit code**. Edit the Worker code (change the `Response` content). 4. To save changes without deploying, select the **down arrow** next to **Deploy** \> **Save**. This will create a new version of your Worker. 5. Go to **Deployments** and select **Promote deployment** to create a split between the two versions. ## Version skew Because gradual deployments mean multiple versions of your Worker serve traffic simultaneously, clients and services can end up interacting with more than one version in ways that produce errors or inconsistent behavior. This is called **version skew**. ### Version skew within a Worker By default, each request is independently routed to a version based on the configured percentages. This means consecutive requests from the same user - including page reloads and asset fetches - can be handled by different versions. If you want to pin a user to a consistent version for the duration of the gradual deployment, you can use [version affinity](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/). ### Version skew between Workers When one Worker calls another via a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/), the two Workers may be at different points in their own gradual deployment. Worker A (on its new version) might call Worker B, but the request lands on Worker B's old version where the API contract is different. You can use [version overrides](https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/) to pin a downstream Worker to a specific version during a subrequest. ## Durable Objects Gradual deployments work differently for [Durable Objects](https://developers.cloudflare.com/durable-objects/) because only one version of each Durable Object can run at a time. Refer to [Gradual Deployments with Durable Objects](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/with-durable-objects/) for details on version assignment, guarantees, and migrations. ## Observability When using gradual deployments, you may want to attribute Workers invocations to a specific version in order to get visibility into the impact of deploying new versions. ### Logpush A new `ScriptVersion` object is available in [Workers Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/). `ScriptVersion` can only be added through the Logpush API right now. Sample API call: ```bash curl -X POST 'https://api.cloudflare.com/client/v4/accounts//logpush/jobs' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "workers-logpush", "output_options": { "field_names": ["Event", "EventTimestampMs", "Outcome", "Logs", "ScriptName", "ScriptVersion"] }, "destination_conf": "", "dataset": "workers_trace_events", "enabled": true }'| jq . ``` `ScriptVersion` is an object with the following structure: ```json { "ScriptVersion": { "id": "", "message": "", "tag": "" } } ``` ### Runtime binding Use the [Version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) to access version ID or version tag in your Worker. ## Limits ### Deployments limit You can only create a gradual deployment with the last 100 uploaded versions of your Worker. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/#page","headline":"Gradual deployments · Cloudflare Workers docs","description":"Incrementally deploy code changes to your Workers with gradual deployments.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Consistently route users to the same Worker version during gradual deployments using version affinity. title: Version affinity image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Version affinity Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) During a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/), each request has a random chance of routing to either version based on the specified percentages. This means the same user can be served content from a different version every time a request is made, which can cause **version skew** issues. Version affinity solves this by deterministically assigning users to a version based on a stable identifier, so they consistently hit the same version across page loads and subrequests for the duration of the gradual deployment. ## How it works Set the `Cloudflare-Workers-Version-Key` header on the incoming request to your Worker: ```sh curl -s https://example.com -H 'Cloudflare-Workers-Version-Key: foo' ``` For a given [deployment](https://developers.cloudflare.com/workers/versions-and-deployments/#deployments), all requests with a version key set to `foo` will be handled by the same version of your Worker. The platform hashes the key and uses the result with the configured percentages to deterministically assign a version - you do not choose which version a key maps to. As you progress a gradual deployment (for example, from 10% to 20% to 50%), users whose keys were already assigned to the new version will remain on it. Users on the old version will progressively move to the new version as the percentage increases, but will not flip back unless you roll back. You can set the `Cloudflare-Workers-Version-Key` header both when making an external request from the Internet to your Worker, as well as when making a subrequest from one Worker to another Worker using a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). ## Static assets Version affinity is particularly important when your Worker serves [static assets](https://developers.cloudflare.com/workers/static-assets/) with content-hashed filenames (like `index-a1b2c3d4.js`), which is the default behavior of most modern build tools and frameworks. During a gradual rollout, different versions of your application will have different asset filenames: * Version A's HTML references `assets/index-a1b2c3d4.js` * Version B's HTML references `assets/index-m3n4o5p6.js` Without version affinity, a user can receive HTML from version A, but when their browser requests `index-a1b2c3d4.js`, that request may be routed to version B - which does not have that file - resulting in a 404 error and a broken page. Configuring version affinity using any of the methods in [Choose a version key](#choose-a-version-key) prevents this entirely by ensuring all requests from the same user are routed to the same version. ## Choose a version key The right version key depends on what stable identifiers your application has available. You can set the header using a [Transform Rule](https://developers.cloudflare.com/rules/transform/request-header-modification/) on your zone, which extracts values from the request without modifying your application code. Note Transform Rules require your Worker to be on a route on a zone you control. They are not available for Workers served on `*.workers.dev` domains. For `*.workers.dev`, you would need to set the header from the client or from an upstream Worker using a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). ### Authenticated applications If your application has a user identifier in a cookie or header, this is the best option. Each user is deterministically assigned to a version and stays there across sessions, devices, and reloads. Text in **Expression Editor**: ```txt http.cookie contains "user_id" ``` Selected operation under **Modify request header**: _Set dynamic_ **Header name**: `Cloudflare-Workers-Version-Key` **Value**: `http.request.cookies["user_id"][0]` ### Applications with sessions If your application sets a session cookie, use the session identifier. This gives consistent routing for the duration of the session. If the session expires and a new one is created, the user may be assigned to a different version. Text in **Expression Editor**: ```txt http.cookie contains "session_id" ``` Selected operation under **Modify request header**: _Set dynamic_ **Header name**: `Cloudflare-Workers-Version-Key` **Value**: `http.request.cookies["session_id"][0]` ### Anonymous or cookieless applications If your application does not have any stable identifier in the request, you have two options: **Option 1: Use the client IP address.** This is the simplest approach and requires no application changes. Users behind the same NAT or VPN will be grouped together, and mobile users who switch networks may change version, but for most applications this significantly reduces version flip-flopping compared to random per-request routing. Text in **Expression Editor**: ```txt true ``` Selected operation under **Modify request header**: _Set dynamic_ **Header name**: `Cloudflare-Workers-Version-Key` **Value**: `ip.src` **Option 2: Set a long-lived cookie from your Worker.** On the first request (which will be randomly assigned), your Worker generates a stable identifier and sets it as a cookie. All subsequent requests use that cookie as the version key. This gives the best consistency for anonymous users, at the cost of a small amount of application code. ```js export default { async fetch(request, env) { const response = await handleRequest(request, env); // Set a long-lived cookie to use as a version affinity key. const COOKIE_NAME = "version-key"; // can be any name const cookieHeader = request.headers.get("Cookie") ?? ""; const hasAffinityCookie = new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=`).test( cookieHeader, ); if (!hasAffinityCookie) { const id = crypto.randomUUID(); response.headers.append( "Set-Cookie", `${COOKIE_NAME}=${id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`, ); } return response; }, }; ``` ```ts export default { async fetch(request: Request, env: Env): Promise { const response = await handleRequest(request, env); // Set a long-lived cookie to use as a version affinity key. const COOKIE_NAME = "version-key"; // can be any name const cookieHeader = request.headers.get("Cookie") ?? ""; const hasAffinityCookie = new RegExp(`(?:^|;\\s*)${COOKIE_NAME}=`).test(cookieHeader); if (!hasAffinityCookie) { const id = crypto.randomUUID(); response.headers.append( "Set-Cookie", `${COOKIE_NAME}=${id}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`, ); } return response; }, }; ``` Then create a Transform Rule to use this cookie as the version key: Text in **Expression Editor**: ```txt http.cookie contains "version-key" ``` Selected operation under **Modify request header**: _Set dynamic_ **Header name**: `Cloudflare-Workers-Version-Key` **Value**: `http.request.cookies["version-key"][0]` Note On the very first request from a new user, no cookie exists yet, so the request will be randomly assigned to a version based on the configured percentages. The cookie is set on the response, so all subsequent requests will be consistently routed. ## Testing You can verify that version affinity is working by sending multiple requests with the same version key and confirming they are handled by the same version: ```sh # Both requests should return responses from the same version curl -s https://example.com -H 'Cloudflare-Workers-Version-Key: test-user-123' curl -s https://example.com -H 'Cloudflare-Workers-Version-Key: test-user-123' ``` Use the [version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) to include the version ID in your Worker's response during testing. During gradual rollouts, monitor your Worker's analytics for increased 404 response rates, especially for asset files (`.js`, `.css`, `.png`). Use [Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) or [Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/) to track these metrics and catch version skew issues early. If you notice problems, you can [roll back](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/) to the previous version. ## Related resources * [Gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) \- How percentage-based traffic splitting works * [Version overrides](https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/) \- Send a request to a specific version by ID (for smoke testing and debugging, not for end-user routing) * [Version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) \- Access version ID and tag from within your Worker Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/#page","headline":"Version affinity · Cloudflare Workers docs","description":"Consistently route users to the same Worker version during gradual deployments using version affinity.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: How gradual deployments work with Durable Objects, including version assignment, migrations, and guarantees. title: With Durable Objects image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # With Durable Objects Last updated Jul 15, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/with-durable-objects/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) To provide [global uniqueness](https://developers.cloudflare.com/durable-objects/platform/known-issues/#global-uniqueness), only one version of each [Durable Object](https://developers.cloudflare.com/durable-objects/) can run at a time. This means that gradual deployments work slightly differently for Durable Objects. When you create a new gradual deployment for a Worker with Durable Objects, each Durable Object is assigned a Worker version based on the percentages you configured in your [deployment](https://developers.cloudflare.com/workers/versions-and-deployments/#deployments). This version will not change until you create a new deployment. ![Gradual Deployments Durable Objects](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1072,height=452,format=webp/_astro/durable-objects.D92CiuSQ.png) ## Example This example assumes that you have previously created three Durable Object instances with names "foo", "bar", and "baz". Your Worker is currently on a version that we will call version "A" and you want to gradually deploy a new version "B" of your Worker. Here is how the versions of your Durable Objects might change as you progress your gradual deployment: | Deployment config | "foo" | "bar" | "baz" | | ------------------------------ | ----- | ----- | ----- | | Version A: 100% | A | A | A | | Version B: 20% Version A: 80% | B | A | A | | Version B: 50% Version A: 50% | B | B | A | | Version B: 100% | B | B | B | This is only an example, so the versions assigned to your Durable Objects may be different. However, the following is guaranteed: * For a given deployment, requests to each Durable Object will always use the same Worker version. * When you specify each version in the same order as the previous deployment and increase the percentage of a version, Durable Objects which were previously assigned that version will not be assigned a different version. In this example, Durable Object "foo" would never revert from version "B" to version "A". * The Durable Object will only be [reset](https://developers.cloudflare.com/durable-objects/observability/troubleshooting/#durable-object-reset-because-its-code-was-updated) when it is assigned a different version, so each Durable Object will only be reset once in this example. Note Typically, a Worker bundle will define both the Durable Object class and a Worker that interacts with it. In this case, you cannot deploy changes to your Durable Object and its Worker independently. You should ensure that API changes between your Durable Object and its Worker are [forwards and backwards compatible](https://developers.cloudflare.com/durable-objects/platform/known-issues/#code-updates) whether you are using gradual deployments or not. However, using gradual deployments will make it even more likely that different versions of your Durable Objects and its Worker will interact with each other. ## Durable Object class lifecycle changes Versions of Worker bundles that change Durable Object class lifecycle cannot be uploaded. This applies to both the declarative [exports](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) field and the legacy [migrations](https://developers.cloudflare.com/durable-objects/reference/durable-object-class-migrations-legacy/) array. This is because Durable Object lifecycle changes are atomic operations. Once a lifecycle change is deployed, rollbacks cannot take place to any version prior to the one that included the change. Durable Object lifecycle changes can be deployed with the following command: npmyarnpnpm ``` npx wrangler deploy ``` ``` yarn wrangler deploy ``` ``` pnpm wrangler deploy ``` To limit the blast radius of these deployments, Durable Object lifecycle changes should be deployed independently of other code changes. To understand why Durable Object lifecycle changes are atomic operations, consider the hypothetical example of gradually deploying a class deletion. If a delete were applied to 50% of Durable Object instances, then Workers requesting those Durable Object instances would fail because they would have been deleted. To do this without producing errors, a version of the Worker which does not depend on any of the Durable Objects to be deleted would have to have already been rolled out. Then, you can deploy the class deletion without affecting any traffic and there is no reason to do so gradually. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/with-durable-objects/#page","headline":"With Durable Objects · Cloudflare Workers docs","description":"How gradual deployments work with Durable Objects, including version assignment, migrations, and guarantees.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/with-durable-objects/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-15","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Preview URLs allow you to preview new versions of your project without deploying it to production. title: Preview URLs image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Preview URLs Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Preview URLs allow you to preview new versions of your Worker without deploying it to production. There are two types of preview URLs: * **Versioned Preview URLs**: A unique URL generated automatically for each new version of your Worker. * **Aliased Preview URLs**: A static, human-readable alias that you can manually assign to a Worker version. Both preview URL types follow the format: `-..workers.dev`. Preview URLs can be: * Integrated into CI/CD pipelines, allowing automatic generation of preview environments for every pull request. * Used for collaboration between teams to test code changes in a live environment and verify updates. * Used to test new API endpoints, validate data formats, and ensure backward compatibility with existing services. When testing zone level performance or security features for a version, we recommend using [version overrides](https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/) so that your zone's performance and security settings apply. Note Preview URLs are only available for Worker versions uploaded after 2024-09-25. ## Types of Preview URLs ### Versioned Preview URLs Every time you create a new [version](https://developers.cloudflare.com/workers/versions-and-deployments/#versions) of your Worker, a unique static version preview URL is generated automatically. These URLs use a version prefix and follow the format `-..workers.dev`. New versions of a Worker are created when you run: * [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) * [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-upload) * Or when you make edits via the Cloudflare dashboard If Preview URLs have been enabled, they are public and available immediately after version creation. Note Minimum required Wrangler version: 3.74.0\. Check your version by running `wrangler --version`. To update Wrangler, refer to [Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). #### View versioned preview URLs using Wrangler The [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-upload) command uploads a new [version](https://developers.cloudflare.com/workers/versions-and-deployments/#versions) of your Worker and returns a preview URL for each version uploaded. #### View versioned preview URLs on the Workers dashboard 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker. 3. Go to the **Deployments** tab, and find the version you would like to view. ### Aliased preview URLs Aliased preview URLs let you assign a persistent, readable alias to a specific Worker version. These are useful for linking to stable previews across many versions (e.g. to share an upcoming but still actively being developed new feature). A common workflow would be to assign an alias for the branch that you're working on. These types of preview URLs follow the same pattern as other preview URLs: `-..workers.dev` Note Minimum required Wrangler version: `4.21.0`. Check your version by running `wrangler --version`. To update Wrangler, refer to [Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/). #### Create an Alias Aliases may be created during `versions upload`, by providing the `--preview-alias` flag with a valid alias name: ```bash wrangler versions upload --preview-alias staging ``` The resulting alias would be associated with this version, and immediately available at: `staging-..workers.dev` #### Rules and limitations * Aliases may only be created during version upload. * Aliases must use only lowercase letters, numbers, and dashes. * Aliases must begin with a lowercase letter. * The alias and Worker name combined (with a dash) must not exceed 63 characters due to DNS label limits. * Only the 1000 most recently deployed aliases are retained. When a new alias is created beyond this limit, the least recently deployed alias is deleted. ## Manage access to Preview URLs When enabled, all preview URLs are available publicly. You can use [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/) to require visitors to authenticate before accessing preview URLs. You can limit access to yourself, your teammates, your organization, or anyone else you specify in your [access policy](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/). To limit your preview URLs to authorized emails only: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Go to **Settings** \> **Domains & Routes**. 4. For Preview URLs, click **Enable Cloudflare Access**. 5. Optionally, to configure the Access application, click **Manage Cloudflare Access**. There, you can change the email addresses you want to authorize. View [Access policies](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/#selectors) to learn about configuring alternate rules. 6. [Validate the Access JWT ↗](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#cloudflare-workers-example) in your Worker script using the audience (`aud`) tag and JWKs URL provided. ## Toggle Preview URLs (Enable or Disable) Note: * Preview URLs are enabled by default when `workers_dev` is enabled. * Preview URLs are disabled by default when `workers_dev` is disabled. * Disabling Preview URLs will disable routing to both versioned and aliased preview URLs. ### From the Dashboard To toggle Preview URLs for a Worker: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Go to **Settings** \> **Domains & Routes**. 4. For Preview URLs, click **Enable** or **Disable**. 5. Confirm your action. ### From the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) Note Wrangler 3.91.0 or higher is required to use this feature. Note Older Wrangler versions will default to Preview URLs being enabled. To toggle Preview URLs for a Worker, include any of the following in your Worker's Wrangler file: ```jsonc { "preview_urls": true } ``` ```toml preview_urls = true ``` ```jsonc { "preview_urls": false } ``` ```toml preview_urls = false ``` If not given, `preview_urls = workers_dev` is the default. Caution If you enable or disable Preview URLs in the Cloudflare dashboard, but do not update your Worker's Wrangler file accordingly, the Preview URLs status will change the next time you deploy your Worker with Wrangler. ## Limitations * Preview URLs are not generated for Workers that implement a [Durable Object](https://developers.cloudflare.com/durable-objects/), including [Containers](https://developers.cloudflare.com/containers/) and [Sandbox](https://developers.cloudflare.com/sandbox/) Workers. For Containers testing options, refer to [Deploy Containers](https://developers.cloudflare.com/containers/deploy/#before-production). * Preview URLs are not currently generated for [Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/) [user Workers](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/how-workers-for-platforms-works/#user-workers). This is a temporary limitation, we are working to remove it. * You cannot currently configure Preview URLs to run on a subdomain other than [workers.dev](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/). * You cannot view logs for Preview URLs today, this includes Workers Logs, Wrangler tail and Logpush. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/#page","headline":"Preview URLs · Cloudflare Workers docs","description":"Preview URLs allow you to preview new versions of your project without deploying it to production.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Revert to an older version of your Worker. title: Rollbacks image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Rollbacks Last updated Jul 15, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can roll back to a previously deployed [version](https://developers.cloudflare.com/workers/versions-and-deployments/#versions) of your Worker using [Wrangler](https://developers.cloudflare.com/workers/wrangler/commands/general/#rollback) or the Cloudflare dashboard. Rolling back to a previous version of your Worker will immediately create a new [deployment](https://developers.cloudflare.com/workers/versions-and-deployments/#deployments) with the version specified and become the active deployment across all your deployed routes and domains. You can roll back from any deployment, including: * A single-version deployment (rolling back replaces the current version with the selected version). * A [split deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) with two versions (rolling back replaces both versions with the selected version at 100% traffic). ## Via Wrangler To roll back to a specified version of your Worker via Wrangler, use the [wrangler rollback](https://developers.cloudflare.com/workers/wrangler/commands/general/#rollback) command. ## Via the Cloudflare Dashboard To roll back to a specified version of your Worker via the Cloudflare dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker > **Deployments**. 3. Select the three dot icon on the right of the version you would like to roll back to and select **Rollback**. ## Rolling back from a split deployment If you are using a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) with two versions splitting traffic, rolling back will: 1. Replace the split deployment with a single-version deployment. 2. Route 100% of traffic to the version you selected for rollback. This effectively promotes one version to handle all traffic, which is useful if you notice issues with one of the versions in your split deployment and want to revert to a stable version immediately. To roll back from a split deployment: 1. Identify which version in your split deployment is stable and performing correctly. 2. Use the [rollback procedure](#via-wrangler) or [dashboard rollback](#via-the-cloudflare-dashboard) to roll back to that version. 3. The split deployment will be replaced with the selected version at 100% traffic. Caution **[Resources connected to your Worker](https://developers.cloudflare.com/workers/runtime-apis/bindings/) will not be changed during a rollback.** Errors could occur if using code for a prior version if the structure of data has changed between the version in the active deployment and the version selected to rollback to. ## Limits ### Rollbacks limit You can only roll back to the 100 most recently published versions. Note When using Wrangler in interactive mode, you can select from up to 100 recent versions. To roll back to a specific version, you can also specify the version ID directly on the command line. Refer to the [wrangler rollback](https://developers.cloudflare.com/workers/wrangler/commands/general/#rollback) documentation for details on specifying version IDs. ### Bindings You cannot roll back to a previous version of your Worker if the [Cloudflare Developer Platform resources](https://developers.cloudflare.com/workers/runtime-apis/bindings/) (such as [KV](https://developers.cloudflare.com/kv/) and [D1](https://developers.cloudflare.com/d1/)) have been deleted or modified between the version selected to roll back to and the version in the active deployment. Specifically, rollbacks will not be allowed if: * A Durable Object class lifecycle change (via [exports](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/) or the legacy [migrations](https://developers.cloudflare.com/durable-objects/reference/durable-object-class-migrations-legacy/) array) has occurred between the version in the active deployment and the version selected to roll back to. * If the target deployment has a [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) to an R2 bucket, KV namespace, or queue that no longer exists. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/#page","headline":"Rollbacks · Cloudflare Workers docs","description":"Revert to an older version of your Worker.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/rollbacks/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-15","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Send requests to a specific version of your Worker in a gradual deployment using version overrides. title: Version overrides image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Version overrides Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can use version overrides to send a request to a specific version of your Worker in the current deployment, even those set to serve 0% of traffic. ## How to set version overrides To specify a version override in your request, set the `Cloudflare-Workers-Version-Overrides` header on the request to your Worker. `Cloudflare-Workers-Version-Overrides` is a [Dictionary Structured Header ↗](https://www.rfc-editor.org/rfc/rfc8941#name-dictionaries) that can contain multiple key-value pairs. Each **key** indicates the name of the Worker the override should be applied to. The **value** indicates the version ID that should be used and must be a [String ↗](https://www.rfc-editor.org/rfc/rfc8941#name-strings). For example: ```sh curl -s https://example.com -H 'Cloudflare-Workers-Version-Overrides: my-worker-name="dc8dcd28-271b-4367-9840-6c244f84cb40"' ``` Version must be in current deployment A version override will only be applied if the specified version is in the current deployment. The versions in the current deployment can be found using the [wrangler deployments list](https://developers.cloudflare.com/workers/wrangler/commands/general/#deployments-list) command or on the [**Workers & Pages** page of the Cloudflare dashboard > select your Worker > **Deployments** \> **Active Deployment** ↗](https://dash.cloudflare.com/?to=/:account/workers/services/view/:worker/production/deployments). Workers currently only supports serving **two** different versions in one deployment. Follow the instructions for [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/#use-gradual-deployments) to ensure the requested version is in the current deployment. You can set the new version to 0% traffic to avoid normal traffic being routed to it. ### Verify that version overrides were applied There are a number of reasons why a request's version override may not be applied. For example: * The deployment may not contain the specified version. It can take up to a couple of seconds to be available globally after a recent change. * The header value may not be a valid [Dictionary ↗](https://www.rfc-editor.org/rfc/rfc8941#name-dictionaries). In the case that a request's version override is not applied, the request will be routed according to the percentages set in the gradual deployment configuration. You can observe the version of your Worker that was invoked using [Observability ↗](https://developers.cloudflare.com/workers/observability/), including in features such as [Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/). Alternatively, if you want to inform clients about the version they ran (e.g. for faster and more transparent debugging), you could use the [version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) and return the version ID in the Worker's response. ## Smoke test example You may want to test a new version in production before gradually deploying it to an increasing proportion of external traffic. This is commonly referred to as a "smoke test". In this example, your deployment is initially configured to route all traffic to a single version: | Version ID | Percentage | | ------------------------------------ | ---------- | | db7cd8d3-4425-4fe7-8c81-01bf963b6067 | 100% | Create a new deployment using [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-deploy) and specify 0% for the new version whilst keeping the previous version at 100%. | Version ID | Percentage | | ------------------------------------ | ---------- | | dc8dcd28-271b-4367-9840-6c244f84cb40 | 0% | | db7cd8d3-4425-4fe7-8c81-01bf963b6067 | 100% | Now test the new version with a version override before gradually progressing the new version to 100%: ```sh curl -s https://example.com -H 'Cloudflare-Workers-Version-Overrides: my-worker-name="dc8dcd28-271b-4367-9840-6c244f84cb40"' ``` ## Service bindings You can set the `Cloudflare-Workers-Version-Overrides` header when making a subrequest from one Worker to another using a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/). This lets you test a specific version of a downstream Worker from an upstream Worker. If you forward the original request object, the override header carries through automatically: ```js // The override header from the inbound request is forwarded to the downstream Worker. export default { async fetch(request, env) { return env.MY_SERVICE.fetch(request); }, }; ``` ```ts // The override header from the inbound request is forwarded to the downstream Worker. export default { async fetch(request: Request, env: Env): Promise { return env.MY_SERVICE.fetch(request); }, }; ``` Alternatively, you can set an override header explicitly: ```js // Replace the version ID with the target version from `wrangler versions list`. export default { async fetch(request, env) { const response = await env.MY_SERVICE.fetch("https://example.com/", { headers: { "Cloudflare-Workers-Version-Overrides": 'my-downstream-worker="dc8dcd28-271b-4367-9840-6c244f84cb40"', }, }); return response; }, }; ``` ```ts // Replace the version ID with the target version from `wrangler versions list`. export default { async fetch(request: Request, env: Env): Promise { const response = await env.MY_SERVICE.fetch("https://example.com/", { headers: { "Cloudflare-Workers-Version-Overrides": 'my-downstream-worker="dc8dcd28-271b-4367-9840-6c244f84cb40"', }, }); return response; }, }; ``` Note Version overrides only apply to `fetch()`\-based service binding calls. There is currently no way to specify version overrides when calling a service binding via RPC (`env.MY_SERVICE.someMethod()`), because RPC calls do not support attaching headers. ## Related resources * [Version affinity](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/version-affinity/) \- Use cookies & headers to pin users to a specific version during a gradual deployment. * [Gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) \- Learn how percentage-based traffic splitting works. * [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) \- How Workers communicate with each other. * [Version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) \- Access version ID and tag from within your Worker. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/#page","headline":"Version overrides · Cloudflare Workers docs","description":"Send requests to a specific version of your Worker in a gradual deployment using version overrides.","url":"https://developers.cloudflare.com/workers/versions-and-deployments/version-overrides/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Workers Cache lets you cache Worker responses to reduce latency and Workers usage. title: Workers Cache image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Workers Cache Last updated Jul 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Workers Cache lets Cloudflare return cached HTTP responses from your Worker without executing your Worker code. When an incoming request matches a cached response, Cloudflare serves the response directly from its edge cache — reducing latency and Workers CPU usage. Caching works for any `fetch()` invocation of the Worker — eyeball requests (requests from browsers and API clients), requests sent through [service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/), and loopback `fetch()` calls between entrypoints via [ctx.exports](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/). You control caching with standard HTTP `Cache-Control` directives on your responses. ## Your Worker's cache Workers Cache is **your Worker's cache**. It is owned by your Worker, operated by your Worker, and private to your Worker. A Worker is a zoneless entity — a Worker can be bound to any number of [zones](https://developers.cloudflare.com/fundamentals/concepts/accounts-and-zones/#zones), run on `workers.dev`, or be invoked entirely through service bindings without ever touching a zone. The cache follows the Worker, not a zone, so: * **No zone configuration for caching applies to Workers Caching.** [Cache Rules](https://developers.cloudflare.com/cache/how-to/cache-rules/), [Cache Response Rules](https://developers.cloudflare.com/cache/how-to/cache-response-rules/), [Page Rules](https://developers.cloudflare.com/rules/page-rules/), cache level settings, the zone's [default cached-file-extensions](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#default-cached-file-extensions) list, and every other zone-level cache control have no effect on a Worker's cache. * **Your Worker is in full control.** You set `Cache-Control` headers on your responses, and Cloudflare honors them per [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111). That is the entire configuration surface. * **The cache is shared across every way the Worker can be invoked.** A Worker bound to `api.example.com`, `api.example.net`, and invoked over a service binding serves the same cached responses to all three — the cache is keyed by the request path, entrypoint, `ctx.props`, and (by default) the Worker version, not by hostname. See [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/). ### The Worker is the configuration surface A Worker is **already infinitely customizable**. You can change response bodies, rewrite headers, branch on any request attribute, call out to other Workers via service bindings or [ctx.exports](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/), and compose logic across an entire system. Workers Caching leans on that. Instead of introducing a separate configuration layer for caching behavior, it lets your Worker express that intent directly — through the `Cache-Control` headers it returns, the `ctx.props` it accepts, and the programmatic purges it issues. Anything you might want to configure about caching, you can configure in code: * Want a longer TTL for certain paths? Branch on the path in your Worker and set a different `max-age`. * Want to strip a tracking query parameter before caching? Rewrite the URL or `ctx.props` in a gateway Worker before dispatching. * Want per-tenant cache partitioning? Set the tenant identifier in `ctx.props` — that is in the cache key. * Want to bypass the cache for authenticated users? Return `Cache-Control: private`, or rely on the [automatic bypass](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass) triggered by `Set-Cookie` and `Authorization`. The Worker you already wrote is the configuration mechanism. Workers Caching runs in front of it and honors whatever headers the Worker returns. ## When caching helps Caching is a good fit for Workers that: * Perform CPU-intensive work whose result can be reused across requests — content generation, template rendering, data transformation. * Fetch data from a slow origin or third-party API and want to absorb that latency for subsequent requests. * Power a server-rendered or statically generated site where many requests produce identical responses. Caching is not useful for per-user responses that change on every request, non-idempotent operations (`POST`, `PUT`, `DELETE`), or responses that must be computed fresh every time. ## How it works With caching enabled, Cloudflare checks the cache before running your Worker. On a hit, the cached response is returned directly. On a miss, your Worker runs, and if the response is cacheable per its `Cache-Control` header, Cloudflare stores it for the next request. flowchart LR accTitle: Cache before a Worker request flow accDescr: Request arrives at Cloudflare, cache is consulted before Worker execution. Request["Request"] --> Cache{"Cache"} Cache -- Hit --> Response["Cached response returned"] Cache -- Miss --> Worker["Worker runs"] Worker --> Store["Response stored in cache"] Store --> Response2["Response returned"] ## Tiered cache Workers Caching is **tiered by default**. Cloudflare operates two layers of cache for your Worker: * **Lower tier** — a cache in the Cloudflare data center closest to the eyeball. Every data center that receives traffic for your Worker has its own lower-tier cache. * **Upper tier** — a smaller set of data centers that every lower tier consults on a miss. The upper tier aggregates cache fills across the whole network. A request is served from the lower tier if it is a hit there. If it is a miss, the lower tier asks the upper tier. If the upper tier also misses, your Worker finally runs to generate the response — and that response is stored in **both** tiers on the way back out, so subsequent requests from any data center benefit. flowchart LR accTitle: Tiered cache for Workers accDescr: A request hits the lower-tier cache first, then the upper-tier cache, then the Worker. Request["Request"] --> Lower{"Lower-tier cache
(near eyeball)"} Lower -- Hit --> Response["Cached response returned"] Lower -- Miss --> Upper{"Upper-tier cache"} Upper -- Hit --> Lower Upper -- Miss --> Worker["Worker runs"] Worker --> Upper This is the same topology that powers [Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/) for zones, applied automatically to your Worker. You do not configure it, and the tiering runs regardless of whether your Worker uses [Smart Placement](#smart-placement-and-the-cache). **Why this matters:** the first request for a given cache key anywhere on Earth populates the upper tier. Every later request, from any Cloudflare data center, can be served from the upper tier without running your Worker — even if the lower tier at that location has never seen the request before. Cache hit ratios are substantially higher than a single flat cache layer. ## Request collapsing When many requests for the same cache key arrive simultaneously at a Cloudflare data center and the response is not yet cached, Cloudflare runs your Worker **once** and serves the resulting response to every waiting request. This is the same [request collapsing](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#request-collapsing) mechanism the zone cache uses, applied automatically to Workers Caching. The waiting requests block on a per-cache-key [cache lock](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#request-collapsing) until the first request produces a response. flowchart LR accTitle: Cache request collapsing for Workers accDescr: Many simultaneous requests for the same cache key produce one Worker invocation; all requests receive the same response. R1["Request 1"] --> Lock R2["Request 2"] --> Lock R3["Request 3"] --> Lock Rn["..."] --> Lock Lock{"Cache lock
(per cache key, per data center)"} Lock -- "first request" --> Worker["Worker runs once"] Worker --> Response["Response
streamed to all
waiting requests"] **Why this matters:** without request collapsing, a sudden burst of traffic to a fresh URL would invoke your Worker once per request, multiplying CPU billing and load on any backend the Worker calls. With request collapsing, that burst still produces one Worker invocation. A few details to keep in mind: * **Collapsing is per cache key, per data center.** Requests that produce different cache keys do not collapse with each other. Two data centers that both miss simultaneously each run your Worker once (the upper tier consolidates further; refer to [Tiered cache](#tiered-cache)). * **Streaming responses are collapsed too.** Waiting requests are joined to the in-flight response stream so they receive the body as it is produced — they do not have to wait for the full response before any bytes are returned. * **Collapsing does not apply to uncacheable responses.** If the Worker's response is uncacheable (`BYPASS`, `DYNAMIC`), each request gets its own invocation. The cache only collapses requests that produce a response the cache is allowed to store. This is one of the most significant differences between Workers Caching and the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) — the Cache API does not collapse concurrent requests, so a burst of traffic to a fresh URL invokes your Worker once per request. ## Quickstart This quickstart walks you through enabling caching, deploying, and observing the cache in action. ### 1\. Enable caching in your Wrangler configuration ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true ``` ### 2\. Return a cacheable response from your Worker Use `max-age` to control how long Cloudflare caches each response: ```js export default { async fetch(request) { const body = JSON.stringify({ timestamp: new Date().toISOString(), random: Math.random(), }); return new Response(body, { headers: { "Content-Type": "application/json", // Cache for 1 hour; serve stale for up to 5 minutes while revalidating. "Cache-Control": "public, max-age=3600, stale-while-revalidate=300", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const body = JSON.stringify({ timestamp: new Date().toISOString(), random: Math.random(), }); return new Response(body, { headers: { "Content-Type": "application/json", // Cache for 1 hour; serve stale for up to 5 minutes while revalidating. "Cache-Control": "public, max-age=3600, stale-while-revalidate=300", }, }); }, } satisfies ExportedHandler; ``` ### 3\. Deploy and observe the cache Deploy your Worker: ```sh npx wrangler deploy ``` Then send two requests and look at the `Cf-Cache-Status` response header: ```sh curl -I https://my-worker.example.workers.dev/ ``` ```txt HTTP/2 200 cache-control: public, max-age=3600, stale-while-revalidate=300 cf-cache-status: MISS ``` ```sh curl -I https://my-worker.example.workers.dev/ ``` ```txt HTTP/2 200 cache-control: public, max-age=3600, stale-while-revalidate=300 cf-cache-status: HIT ``` The second request receives the cached response. The `timestamp` and `random` values in the body are identical between the two requests, even though the Worker generates fresh ones on every run — confirming that the second request did not execute your Worker. ## What gets cached * HTTP invocations of the Worker's [fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/) handler are eligible for caching, including eyeball requests, service binding `fetch()` calls, and loopback `fetch()` calls via `ctx.exports`. * Only `GET` and `HEAD` requests are cached. Other methods always invoke your Worker. `GET` and `HEAD` for the same URL share a single cache entry — see [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/#what-goes-into-the-cache-key). * **Only `fetch()` invocations go through the cache.** Custom [RPC methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/) on a `WorkerEntrypoint` (for example `ctx.exports.Backend.getUser(id)`) bypass the cache entirely and always run the callee. To cache a piece of work, expose it as a `fetch` handler on its own entrypoint. * **WebSocket upgrade requests bypass the cache.** A `GET` request carrying `Upgrade: websocket` always invokes your Worker. * Other invocation types — [scheduled](https://developers.cloudflare.com/workers/configuration/cron-triggers/) (Cron Triggers), [queue](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer) consumers, [Workflows](https://developers.cloudflare.com/workflows/), [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/), [Durable Object](https://developers.cloudflare.com/durable-objects/) invocations, [Email Workers](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/) — always run without cache involvement. * Cacheability is determined by the response headers your Worker returns. Workers Caching follows the semantics defined in [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111), including [heuristic freshness ↗](https://www.rfc-editor.org/rfc/rfc9111#name-calculating-heuristic-fresh) for responses that do not carry `Cache-Control`. Refer to [Cache-Control](https://developers.cloudflare.com/cache/concepts/cache-control/) for the full list of directives Cloudflare respects. * Cloudflare's standard [cache bypass conditions](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass) apply. In particular, responses with a `Set-Cookie` header and requests with an `Authorization` header trigger automatic bypass. * [Preview URLs](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) are supported. Each preview caches independently of your production deployment, so testing a cache-affecting change in a preview never touches production's cached responses. * [Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/) is supported. Each user Worker has its own cache, isolated from the dispatcher and from other user Workers in the namespace. The `Cf-Cache-Status` response header tells you what happened for each request. The values you will see most often are `HIT`, `MISS`, `EXPIRED`, `REVALIDATED`, `UPDATING`, `STALE`, and `BYPASS`. Refer to [Cloudflare cache responses](https://developers.cloudflare.com/cache/concepts/cache-responses/) for the full set of values. ## Content negotiation with `Vary` Workers Caching honors the [Vary ↗](https://www.rfc-editor.org/rfc/rfc9110.html#name-vary) response header as defined in [RFC 9110 ↗](https://www.rfc-editor.org/rfc/rfc9110.html) and [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-cache-keys-with). When your Worker returns a `Vary` header, Cloudflare stores a separate cached variant per distinct combination of the listed request header values, and only returns a cached variant when the incoming request's headers match the ones the variant was stored under. This lets a single URL cache multiple representations — for example, different encodings, different content types, or different languages — without your Worker coordinating content negotiation by hand: ```js export default { async fetch(request) { const accept = request.headers.get("Accept") ?? ""; const wantsWebp = accept.includes("image/webp"); const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage(); return new Response(body, { headers: { "Content-Type": wantsWebp ? "image/webp" : "image/jpeg", "Cache-Control": "public, max-age=3600", // Cache a separate variant per distinct Accept header value. Vary: "Accept", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const accept = request.headers.get("Accept") ?? ""; const wantsWebp = accept.includes("image/webp"); const body = wantsWebp ? await fetchWebpImage() : await fetchJpegImage(); return new Response(body, { headers: { "Content-Type": wantsWebp ? "image/webp" : "image/jpeg", "Cache-Control": "public, max-age=3600", // Cache a separate variant per distinct Accept header value. Vary: "Accept", }, }); }, } satisfies ExportedHandler; ``` Notes: * `Vary: *` disables caching for the response. A wildcard variance cannot be satisfied deterministically from request headers, so Cloudflare does not store the response. * Variants share a single cache entry for purge purposes — [purging](https://developers.cloudflare.com/workers/cache/purge/) a tag or path prefix that matches any variant invalidates all variants of that URL. All variants of a URL must therefore use the same `Cache-Tag` values. * `Vary` is not compatible with image-transformation features that already produce their own variants (Polish, Image Resizing). Responses rewritten by those features ignore `Vary`. * Variants are stored per exact request-header value. Clients that send semantically equivalent but textually different values — for example `Accept-Encoding: gzip, br` and `Accept-Encoding: br, gzip` — produce separate variants. Shape the headers your Worker sees (for example, by normalizing them in a gateway Worker before passing the request on) if you need to reduce variant fan-out. ## Caching between Workers When one Worker calls another over a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/), the **callee's** cache is consulted. If the callee has caching enabled and has a matching cached response, the caller receives it without invoking the callee. flowchart LR accTitle: Cache between Workers accDescr: Worker A calls Worker B; Worker B's cache is consulted before Worker B runs. Request["Request"] --> WorkerA["Worker A"] WorkerA --> CacheB{"Worker B's cache"} CacheB -- Hit --> WorkerA CacheB -- Miss --> WorkerB["Worker B"] WorkerB --> CacheB The cache key for service binding calls includes the caller's [ctx.props](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#ctxprops), so different callers with different authorization context are cached separately. For details, refer to [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/). For same-account calls, the calling Worker can also tailor the callee's caching for an individual request by setting [cf.cacheKey](https://developers.cloudflare.com/workers/cache/cache-keys/#custom-cache-keys) to override the cache key or [cf.cacheControl](https://developers.cloudflare.com/workers/cache/configuration/#override-cache-control-from-the-calling-worker) to supply a `Cache-Control` directive. ## Cache Durable Object responses [Durable Objects](https://developers.cloudflare.com/durable-objects/) are never cached directly by Workers Caching. However, since Workers Caching runs in front of any Worker entrypoint, you can cache a Durable Object's HTTP responses by wrapping the Durable Object behind a [named Worker entrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) and caching the entrypoint. The wrapper entrypoint forwards the request into the Durable Object and sets `Cache-Control` on the response it returns. Because Workers Caching sits in front of the entrypoint, subsequent requests are served from cache without re-entering the Durable Object. The default entrypoint here is a gateway that should run on every request, so disable caching on it and enable it on `CachedCounter` (see [Per-entrypoint caching](https://developers.cloudflare.com/workers/cache/configuration/#per-entrypoint-caching)): ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "CachedCounter": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.CachedCounter] type = "worker" [exports.CachedCounter.cache] enabled = true ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; // Cached entrypoint. Requests to this entrypoint are served from cache // when possible; on a miss, the Durable Object is invoked and its // response is stored. export class CachedCounter extends WorkerEntrypoint { async fetch(request) { const id = this.env.COUNTER.idFromName("global"); const stub = this.env.COUNTER.get(id); const response = await stub.fetch(request); // Attach cache headers. Clone into a new Response so the headers // are mutable. return new Response(response.body, { status: response.status, headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=30", }, }); } } // Default entrypoint. Delegates to the cached entrypoint via ctx.exports, // which routes through the cache. export default { async fetch(request, env, ctx) { return ctx.exports.CachedCounter.fetch(request); }, }; ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; interface Env { COUNTER: DurableObjectNamespace; } // Cached entrypoint. Requests to this entrypoint are served from cache // when possible; on a miss, the Durable Object is invoked and its // response is stored. export class CachedCounter extends WorkerEntrypoint { async fetch(request: Request): Promise { const id = this.env.COUNTER.idFromName("global"); const stub = this.env.COUNTER.get(id); const response = await stub.fetch(request); // Attach cache headers. Clone into a new Response so the headers // are mutable. return new Response(response.body, { status: response.status, headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=30", }, }); } } // Default entrypoint. Delegates to the cached entrypoint via ctx.exports, // which routes through the cache. export default { async fetch(request, env, ctx): Promise { return ctx.exports.CachedCounter.fetch(request); }, } satisfies ExportedHandler; ``` For more patterns that combine a gateway entrypoint with cached inner entrypoints, refer to [Examples](https://developers.cloudflare.com/workers/cache/examples/). ## Smart placement and the cache [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) moves **where your Worker runs** when it runs — typically closer to a slow origin or database. It does not move the cache. Workers Caching always has a lower tier near the eyeball and an upper tier aggregating the network, exactly as described in [Tiered cache](#tiered-cache) above, whether or not Smart Placement is enabled. The cache is always consulted before Smart Placement is considered. Concretely: * **Lower-tier hit:** the response is returned from the data center nearest the eyeball. Your Worker does not run. Smart Placement is not consulted. * **Lower-tier miss, upper-tier hit:** the response is returned from the upper tier. Your Worker does not run. Smart Placement is not consulted. * **Both tiers miss:** Smart Placement routes execution of your Worker to the placement target (for example, near your origin). The resulting response is stored in both cache tiers on the way back to the eyeball. Importantly, the **upper tier and the Smart Placement target are independent locations**. The upper tier is chosen by Cloudflare to aggregate cache fills across the network; the Smart Placement target is chosen to minimize latency between your Worker and its backend. They are generally not in the same data center. flowchart LR accTitle: Tiered cache with Smart Placement across three locations accDescr: The eyeball, the upper-tier cache, and the Smart Placement target are three independent locations. Requests traverse them in order on a full cache miss. subgraph EyeballColo["Data center near eyeball"] Request["Request"] --> Lower{"Lower-tier cache"} end subgraph UpperColo["Upper-tier data center"] Upper{"Upper-tier cache"} end subgraph PlacedColo["Smart Placement target"] Placed["Worker runs"] Origin["Origin / backend"] Placed <--> Origin end Lower -- Hit --> Response["Response"] Lower -- Miss --> Upper Upper -- Hit --> Lower Upper -- Miss --> Placed Placed --> Upper On a full cache miss, a request therefore traverses three locations: the lower-tier data center near the eyeball, the upper-tier data center, and the Smart Placement target. The cache tiers absorb this cost so that the slow trip to the placement target is only paid once for the whole network — the upper tier shields the placement target from every lower-tier miss. Future optimization Because the upper tier and the Smart Placement target are chosen independently today, a cache miss pays for an extra hop between them. Future updates may bring these choices closer together — for example, by co-locating the upper tier with the placement target, or by having a single placement decision account for both — so that full cache misses incur one wide-area trip instead of two. ## Purging the cache Your Worker can invalidate its own cache at any time using `ctx.cache.purge()`. Tags are the most flexible mechanism — tag responses with `Cache-Tag` when returning them, and purge those tags later: ```js export default { async fetch(request, env, ctx) { await ctx.cache.purge({ tags: ["blog-posts"] }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { await ctx.cache.purge({ tags: ["blog-posts"] }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` You can also [import cache from cloudflare:workers](https://developers.cloudflare.com/workers/cache/purge/#two-ways-to-call-purge) and call `cache.purge({...})` when you do not have `ctx` in scope — for example, from a utility module. For all purge modes and patterns, refer to [Purging the cache](https://developers.cloudflare.com/workers/cache/purge/). ## Pricing Workers Cache has no separate pricing. When you enable Workers Cache, all requests to your Worker are billed at the standard [Workers request rate](https://developers.cloudflare.com/workers/platform/pricing/) — the same per-request rate as any other request to your Worker — whether the response comes from cache or from your Worker. There is no charge beyond the standard request rate. **CPU time is only billed when your Worker runs** — cache hits do not consume CPU time. Caching bills requests that are normally free When caching is enabled, every request to your Worker is charged at the standard Workers request rate, including requests that are normally free: [static asset requests](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/) and [worker-to-worker invocations](https://developers.cloudflare.com/workers/platform/pricing/#service-bindings) through service bindings or `ctx.exports`. | Request type | Request charge | CPU time charge | | ----------------------------------------------------------------------------------------------------------- | -------------- | --------------------- | | Cache HIT (Worker does not run) | Standard rate | Not billed | | Cache MISS (Worker runs) | Standard rate | Billed | | Cache BYPASS (Worker runs) | Standard rate | Billed | | [Static asset request](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/) | Standard rate | Not billed | | [Worker-to-worker invocation](https://developers.cloudflare.com/workers/platform/pricing/#service-bindings) | Standard rate | Billed if Worker runs | For an example, refer to [Pricing example: Worker with caching](https://developers.cloudflare.com/workers/platform/pricing/#example-5-worker-with-caching). ## Next steps * [Configuration](https://developers.cloudflare.com/workers/cache/configuration/) * [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/) * [Purging the cache](https://developers.cloudflare.com/workers/cache/purge/) * [Examples](https://developers.cloudflare.com/workers/cache/examples/) * [Debugging](https://developers.cloudflare.com/workers/cache/debugging/) * [Limitations](https://developers.cloudflare.com/workers/cache/limitations/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/#page","headline":"Workers Cache · Cloudflare Workers docs","description":"Workers Cache lets you cache Worker responses to reduce latency and Workers usage.","url":"https://developers.cloudflare.com/workers/cache/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: How Workers Caching builds cache keys, with guidance for service bindings, multi-tenant Workers, and gradual deployments. title: Cache keys image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cache keys Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/cache-keys/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Every cached response is stored under a **cache key**. When a request arrives, Cloudflare computes a cache key for it and looks it up — on a hit, the stored response is returned; on a miss, your Worker runs and its response is stored under that key for next time. Two requests that produce the same cache key share the same cached response. Two requests that produce different cache keys get independent cached entries. This page explains what Workers Caching puts into the cache key, why each component is there, and how to reason about it when designing your Worker. ## What goes into the cache key Workers Caching keys responses by: * The **target entrypoint** — which specific [named entrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) of the Worker received the request. A `default` export and an exported class are different entrypoints and do not share a cache even if they produce identical responses. * The **path and query string** of the request URL. Query parameter order matters — `?a=1&b=2` and `?b=2&a=1` are different cache keys. Trailing slashes matter too. * The **Worker version**, by default. Each deployed version has its own cache, so a new deployment does not serve responses written by a previous version. You can turn this off with [cache.cross\_version\_cache](https://developers.cloudflare.com/workers/cache/configuration/#cross-version-caching) to share cached responses across versions. See [Invalidating cache across deployments](#invalidating-cache-across-deployments). * The invocation's [ctx.props](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#ctxprops), when the Worker is invoked through a service binding or RPC. See [Multi-tenant safety with ctx.props](#multi-tenant-safety-with-ctxprops). As anti-cache-poisoning measures, the key also includes: * The `x-http-method-override`, `x-http-method`, and `x-method-override` request headers. * The `x-forwarded-host`, `x-host`, `x-forwarded-scheme` (unless its value is `http` or `https`), `x-original-url`, `x-rewrite-url`, and `forwarded` request headers. * The value of the `Cloudflare-Workers-Version-Key` request header. This header is not set by Cloudflare automatically — it is only meaningful if a caller (for example, an upstream Worker or proxy) chooses to include it to explicitly partition the cache further. This is independent of the automatic per-version keying described above, which is controlled by [cache.cross\_version\_cache](https://developers.cloudflare.com/workers/cache/configuration/#cross-version-caching). These three bullets are not something you should normally need to reason about. Some frameworks interpret the method-override and URL-rewrite headers as overriding the effective method or URL of a request, which can lead to [cache poisoning ↗](https://portswigger.net/research/practical-web-cache-poisoning) if two requests differ only in those headers but produce materially different responses. Including them in the cache key ensures a poisoned entry only affects requests that carry the same poisoned header. Requests that differ only in request headers that are not part of the cache key (for example, `User-Agent`, `Accept-Language`, `Cookie`, or `Authorization`) return the same cached response. This is usually what you want — you do not want every user agent string or language preference producing a separate cache entry. If you do need content negotiation, set [Vary](https://developers.cloudflare.com/workers/cache/configuration/#vary) on the response, or handle it inside your Worker and produce a canonical response per URL. Notably, the cache key does **not** include: * **The HTTP method.** `GET` and `HEAD` requests for the same URL share a single cache entry. A `HEAD` request can be served from a `GET` fill (Cloudflare returns the cached headers without the body). In the other direction, a `HEAD` request on a cold cache is converted to a `GET` internally so the full asset is fetched and stored — a subsequent `GET` then hits the entry that `HEAD` populated. (`POST`, `PUT`, `PATCH`, and `DELETE` are never cached at all, so the question does not arise for them.) * **The request's host.** The Worker's cache is keyed by path and query string, not the full URL. See [The cache belongs to the Worker, not to a domain](#the-cache-belongs-to-the-worker-not-to-a-domain). * **The request body.** Since only `GET` and `HEAD` are cacheable, this is rarely relevant — but worth noting if your Worker reads `request.body` on a cacheable method, the body does not partition the cache. At launch, you cannot inspect the exact cache key Cloudflare computed for a request. The primary signals you have for understanding cache behavior are the `Cf-Cache-Status` response header and per-invocation cache-hit information in the [Workers observability dashboard](https://developers.cloudflare.com/workers/observability/). See [Inspecting the cache key](#inspecting-the-cache-key). ## The cache belongs to the Worker, not to a domain A Worker is a zoneless entity. It can be invoked through several different paths: * Directly on a `workers.dev` subdomain. * Through a [route](https://developers.cloudflare.com/workers/configuration/routing/routes/) on any zone you control. * Through a [custom domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/) — and you can bind the same Worker to many custom domains. * Through a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) from another Worker, with an arbitrary placeholder hostname in the URL. Workers Caching treats all of these as the same Worker and uses a single shared cache across them. The cache key does not include the host, so a request to `/api/users/42` hits the same cached entry whether it came in through `api.example.com`, `api.example.net`, a service binding, or a `workers.dev` URL. This is the behavior you almost always want. A Worker's responses are a function of its code and its inputs, not of which domain the request arrived through — so caching them once and serving that response back to every ingress path maximizes the cache hit rate without losing correctness. If you genuinely need different cached responses for the same path on different hostnames — for example, white-labeled tenants where `tenant-a.example.com/index` and `tenant-b.example.com/index` must produce different content — the cache key does not do this for you automatically. Instead, distinguish the tenants at your gateway Worker and pass the tenant identifier via `ctx.props`, which _is_ part of the cache key. ## Invalidating cache across deployments By default, the currently invoked Worker version **is** part of the cache key. Each deployed version has its own cache, so: * A new deployment starts from a cold cache and never serves responses that a previous version wrote. * Cache-affecting changes apply immediately when the new version goes live — you do not have to purge anything to stop serving old content. * During a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/), the old and new versions populate independent caches, so a slice of traffic on the new version never receives the old version's responses. This is the default because it is the simplest behavior to reason about. The trade-off is that **cache hit rate resets on every deployment** — the first requests to a new version are misses while its cache fills. This is the most common reason a Worker's cache hit rate drops right after a deploy. ### Share the cache across versions If you deploy frequently and your responses rarely change between deployments, throwing away a warm cache on every deploy is wasteful. Set [cache.cross\_version\_cache](https://developers.cloudflare.com/workers/cache/configuration/#cross-version-caching) to `true` to drop the version from the cache key and share cached responses across versions. A response written by version A is then still served after version B is deployed, as long as its TTL has not expired. This maximizes cache hit rate at the expense of slower rollouts: because a deployment no longer invalidates the cache, a change that alters response content will not take effect for already-cached entries until they expire or you purge them. When you have `cross_version_cache` enabled and need a deployment to take effect immediately, use one of the two tools below. ### Tag responses by version, purge the tag on rollback If you want fine-grained control, tag each cached response with the Worker version that produced it. Later, purging that version tag removes every entry that version wrote, without affecting cached responses from other versions. This uses the [version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) to read the current version ID at request time, and prepends it as a `Cache-Tag` value. See [Version-specific purging](https://developers.cloudflare.com/workers/cache/purge/#version-specific-purging) for the full pattern with code. This is the best option if you have enabled `cross_version_cache` and might need to roll back a specific version without blowing away cached content from working versions. ### Purge everything after deploy The simpler approach: after each deploy, hit a small Worker endpoint from your CI that calls [ctx.cache.purge({ purgeEverything: true })](https://developers.cloudflare.com/workers/cache/purge/#purge-everything). The next request after the purge re-populates the cache from whichever Worker version is live at that moment. This is coarser but requires zero in-Worker logic. Use it if you have enabled `cross_version_cache` but still want specific deployments to invalidate the cache. With the default per-version cache, deployments already start from a cold cache, so this is unnecessary. ## Multi-tenant safety with `ctx.props` When your Worker is invoked through a [service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) or [RPC](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/), the caller's [ctx.props](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#ctxprops) is part of the cache key. Two callers that invoke your Worker with different `ctx.props` get **separate cached entries** — one caller can never receive another caller's cached response. This is the mechanism that makes caching safe for multi-tenant Workers invoked over a service binding. If you use `ctx.props` to carry per-caller authorization context — user ID, tenant ID, organization, role — caching is safe by default. Responses that logically belong to one caller cannot leak to another through the cache. ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class Backend extends WorkerEntrypoint { async fetch(request) { // ctx.props.userId is set by the caller (for example, an auth gateway). // Because it is part of the cache key, User A and User B requesting the // same URL get separate cache entries — there is no way for one to // see the other's response. const { userId } = this.ctx.props; const data = { userId, timestamp: Date.now() }; return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=300", }, }); } } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; interface Props { userId: string; } export default class Backend extends WorkerEntrypoint { async fetch(request: Request): Promise { // ctx.props.userId is set by the caller (for example, an auth gateway). // Because it is part of the cache key, User A and User B requesting the // same URL get separate cache entries — there is no way for one to // see the other's response. const { userId } = this.ctx.props; const data = { userId, timestamp: Date.now() }; return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=300", }, }); } } ``` Caution If you authenticate callers through some mechanism other than `ctx.props` — for example, by reading a custom header your gateway Worker attaches — that input is **not** automatically part of the cache key. Two callers authenticated by different header values but otherwise identical requests will share a single cached entry, which means one caller can receive another caller's response. The fix is to move per-caller authorization state into `ctx.props`. Your gateway Worker should populate `ctx.props` with whatever distinguishes callers before invoking the cached Worker. Cloudflare's standard [automatic bypass rules](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass) (`Set-Cookie`, `Authorization`) can also prevent the problem by disabling caching entirely for authenticated requests, but `ctx.props` is the recommended approach because it still lets you cache. ### Service binding URL Service binding calls deserve a specific note because the URL you pass does not mean what you might think it means. When you call a service binding with [fetch()](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/#use-the-fetch-method), the hostname in the URL is a placeholder. The request is routed via the binding, not by DNS — the hostname is never resolved. And because the host is not part of the cache key (as described in [The cache belongs to the Worker, not to a domain](#the-cache-belongs-to-the-worker-not-to-a-domain)), the placeholder has no effect on caching either. Only the **path** (and query string) contribute to the cache key, alongside the target entrypoint and `ctx.props`: ```js export default { async fetch(request, env, ctx) { // "internal" here is just a placeholder — it is not routed anywhere // and is not part of the cache key. // // What identifies this cached response is: // - the BACKEND entrypoint // - the path "/api/users/42" // - whatever ctx.props the gateway passes along return env.BACKEND.fetch("http://internal/api/users/42"); }, }; ``` ```ts interface Env { BACKEND: Fetcher; } export default { async fetch(request, env, ctx): Promise { // "internal" here is just a placeholder — it is not routed anywhere // and is not part of the cache key. // // What identifies this cached response is: // - the BACKEND entrypoint // - the path "/api/users/42" // - whatever ctx.props the gateway passes along return env.BACKEND.fetch("http://internal/api/users/42"); }, } satisfies ExportedHandler; ``` If you want cached responses to differ for different callers, vary `ctx.props`. If you want them to differ by request, vary the path or query string. Varying the hostname does nothing. ## Inspecting the cache key At launch, two signals give you visibility into cache behavior: 1. **The `Cf-Cache-Status` response header.** The values you will see most often are `HIT`, `MISS`, `EXPIRED`, `REVALIDATED`, `UPDATING`, `STALE`, and `BYPASS`. `HIT` means Cloudflare returned a cached response without running your Worker. `MISS` means your Worker ran and the response was stored. `UPDATING` means the cached response was stale and your Worker ran in the background to refresh it. `BYPASS` means caching was disabled for this request. Refer to [Cloudflare cache responses](https://developers.cloudflare.com/cache/concepts/cache-responses/) for the full set of values. 2. **Cache hits in the [Workers observability dashboard](https://developers.cloudflare.com/workers/observability/).** Each invocation surfaces whether it was served from cache, so you can filter and aggregate cache-hit behavior across your Worker's traffic. Cloudflare does not currently expose the cache key composition itself. If two requests you expected to share a cached response do not, you have to reason about what part of the key differed from the components listed in [What goes into the cache key](#what-goes-into-the-cache-key). For a walkthrough of common caching problems and how to diagnose them, refer to [Debugging](https://developers.cloudflare.com/workers/cache/debugging/). ## Custom cache keys By default, the path and query string of the request URL form the URL component of the cache key. When one entrypoint invokes another cached entrypoint through a [ctx.exports](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) loopback, the calling entrypoint can override that component by setting `cf.cacheKey` on the request. In the example below, the `Backend` entrypoint is the cached one. The default entrypoint forwards requests to it through `ctx.exports`, choosing the cache key itself: ```js import { WorkerEntrypoint } from "cloudflare:workers"; // Cached entrypoint. Requests routed here through ctx.exports are served // from cache when possible. export class Backend extends WorkerEntrypoint { async fetch(request) { return new Response("Hello from the backend", { headers: { "Content-Type": "text/html", "Cache-Control": "public, max-age=3600", }, }); } } // Gateway entrypoint. Calls the cached Backend entrypoint via ctx.exports, // which routes through the cache, and chooses the cache key for the call. export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Strip a tracking parameter so that requests differing only by // `utm_source` resolve to the same cached entry. url.searchParams.delete("utm_source"); return ctx.exports.Backend.fetch(request, { cf: { cacheKey: url.pathname + url.search }, }); }, }; ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; // Cached entrypoint. Requests routed here through ctx.exports are served // from cache when possible. export class Backend extends WorkerEntrypoint { async fetch(request: Request): Promise { return new Response("Hello from the backend", { headers: { "Content-Type": "text/html", "Cache-Control": "public, max-age=3600", }, }); } } // Gateway entrypoint. Calls the cached Backend entrypoint via ctx.exports, // which routes through the cache, and chooses the cache key for the call. export default { async fetch(request, env, ctx): Promise { const url = new URL(request.url); // Strip a tracking parameter so that requests differing only by // `utm_source` resolve to the same cached entry. url.searchParams.delete("utm_source"); return ctx.exports.Backend.fetch(request, { cf: { cacheKey: url.pathname + url.search }, }); }, } satisfies ExportedHandler; ``` A custom cache key **replaces the path and query string** in the cache key. Everything else described in [What goes into the cache key](#what-goes-into-the-cache-key) still applies: * The target entrypoint and the caller's [ctx.props](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#ctxprops) remain part of the key. A custom cache key cannot reach across entrypoints or across `ctx.props`, so the [multi-tenant isolation](#multi-tenant-safety-with-ctxprops) described above still holds even when callers choose their own keys. A custom key only ever addresses entries within the callee's own cache namespace. * Two requests with **different URLs but the same `cf.cacheKey`** resolve to the same cache entry. This is how you collapse several URLs onto a single cached response. * Two requests with the **same URL but different `cf.cacheKey`** resolve to separate cache entries. Set `cf.cacheKey` to an empty string, or omit it, to fall back to the default URL-derived key. In this pattern the default entrypoint is a gateway that should run on every request, so disable caching on it and leave it on for `Backend` (see [Per-entrypoint caching](https://developers.cloudflare.com/workers/cache/configuration/#per-entrypoint-caching)): ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "Backend": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.Backend] type = "worker" [exports.Backend.cache] enabled = true ``` ### What you can do with a custom cache key * **Ignore parts of the URL.** Strip tracking parameters (`utm_source`, `gclid`), or drop a query string entirely, so that variations that do not change the response share one cache entry. * **Key on something other than the URL.** Build the key from a value your gateway Worker trusts — for example, a normalized resource identifier — so that several equivalent URLs map to one entry. * **Partition the cache yourself.** Append a discriminating value to the key (for example, a content version) to force separate entries for requests that would otherwise collide. For per-caller isolation, continue to use [ctx.props](#multi-tenant-safety-with-ctxprops) rather than encoding caller identity into the cache key — `ctx.props` is part of the key automatically and cannot be bypassed. ### Custom keys apply to same-account calls only `cf.cacheKey` is honored only when the call stays within your account. Cloudflare drops the `cf` object whenever a request crosses an account boundary — for example, a service binding to a Worker owned by a different account. When that happens, the custom key is disregarded and the cache key falls back to the request URL, so a caller in one account can never influence (or probe) the cache of a Worker in another account. This also means `cf.cacheKey` has no effect on eyeball requests. The `cf` object on an inbound request from a browser or API client is populated by Cloudflare, not by the client, so a client cannot set its own cache key. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/cache-keys/#page","headline":"Cache keys · Cloudflare Workers docs","description":"How Workers Caching builds cache keys, with guidance for service bindings, multi-tenant Workers, and gradual deployments.","url":"https://developers.cloudflare.com/workers/cache/cache-keys/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Enable and configure Workers Caching. title: Configuration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Configuration Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/configuration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Workers Caching is configured per Worker, in your Wrangler configuration file. When enabled, caching applies to every `fetch()` invocation — eyeball requests, service binding `fetch()` calls, and loopback `fetch()` calls between entrypoints via [ctx.exports](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) — unless you [disable it for a specific entrypoint](#per-entrypoint-caching). Custom [RPC methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/) bypass the cache. This is **your Worker's cache** — configured through your Worker's code and Wrangler file. Your Worker controls its cache entirely through: * The `cache.enabled` flag in your Wrangler configuration, which turns caching on or off. You can override it [per entrypoint](#per-entrypoint-caching) and control [cross-version behavior](#cross-version-caching). * The `Cache-Control` (and `cdn-cache-control`, `cloudflare-cdn-cache-control`) headers your Worker sets on its responses, per [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111). * The optional `Cache-Tag` response header for bulk purging, and [ctx.cache.purge()](https://developers.cloudflare.com/workers/cache/purge/) for programmatic invalidation. That is the entire configuration surface. ## Enable caching Note Requires Wrangler 4.69.0 or above. Add a `cache` block to your Wrangler configuration: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true ``` Setting `cache.enabled` to `true` causes Cloudflare to check the cache before invoking your Worker on every HTTP request. This is the default for every entrypoint; you can override it per entrypoint with [exports](#per-entrypoint-caching). The `cache` block accepts two fields: `enabled` (required) and [cross\_version\_cache](#cross-version-caching) (optional). Any other fields are reserved for future use and may cause validation errors in future versions of Wrangler. ## Disable caching To turn caching off, set `cache.enabled` to `false` (or remove the `cache` block) and redeploy: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": false, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = false ``` Disabling caching does not purge previously cached responses — it only stops Cloudflare from consulting or populating the cache on subsequent requests. If you re-enable caching later, any entries that are still within their TTL become usable again. If you need cached responses to stop being served immediately, [purge the cache](https://developers.cloudflare.com/workers/cache/purge/) after disabling. ## Per-entrypoint caching Note Requires Wrangler 4.107.0 or above. `cache.enabled` sets the default for the whole Worker, but a Worker can expose several [entrypoints](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) — the default export and any number of named `WorkerEntrypoint` classes — and you can turn caching on or off for each one independently. Use the `exports` map, keyed by entrypoint name, with `"default"` referring to the default export: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true, }, "exports": { // Opt the default entrypoint out of caching. "default": { "type": "worker", "cache": { "enabled": false } }, // Keep caching on for the Admin entrypoint. "Admin": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.Admin] type = "worker" [exports.Admin.cache] enabled = true ``` Each entry is `{ "type": "worker", "cache": { "enabled": } }`. A per-entrypoint `cache.enabled` overrides the top-level `cache.enabled` for that entrypoint; entrypoints you do not list inherit the top-level value. You can also enable caching for a single entrypoint without a top-level `cache` block by listing only that entrypoint. This lets you **opt specific entrypoints in and out** without changing your Worker code: * **Opt an entrypoint out** to keep it running on every request — the natural fit for a gateway or router entrypoint that authenticates, normalizes, or dispatches, and should never itself be served from cache. This is the recommended way to build the [gateway pattern](https://developers.cloudflare.com/workers/cache/examples/): disable caching on the gateway entrypoint and enable it on the inner entrypoint the gateway calls through `ctx.exports`. * **Opt an entrypoint in** to cache only the specific entrypoints that return reusable responses, leaving the rest of the Worker uncached. For lowest latency, disable cache for an entrypoint instead of returning \`Cache-Control: no-store\` for all responses Do not enable caching on a gateway entrypoint and then return `Cache-Control: no-store` on every response to keep it running. With caching enabled, each request to that entrypoint still consults the lower and upper cache tiers first — incurring the [tiered-cache](https://developers.cloudflare.com/workers/cache/#tiered-cache) round trip — only to run the Worker and discard an uncacheable response. That adds latency for no benefit. Disable caching on the entrypoint in the `exports` map instead, so requests skip the cache lookup and go straight to your gateway logic. ## Versioned deployments The `cache` configuration is part of your Worker version: * Each version uploaded with [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/#deploy) or [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/#versions-upload) captures whatever `cache.enabled` value is in its Wrangler configuration. * Rolling back to a previous version also rolls back the `cache` setting attached to that version. * You can use [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) to turn caching on for a percentage of traffic before applying it to 100%. During a gradual rollout from a version with caching disabled to a version with caching enabled, traffic routed to the old version runs uncached as it did before, and traffic routed to the new version consults and populates the cache. By default, the Worker version is part of the cache key, so the two versions populate independent cache entries and do not serve each other's responses — see [Cross-version caching](#cross-version-caching). ## Cross-version caching Note The `cross_version_cache` option requires Wrangler 4.107.0 or above. By default, the **Worker version is part of the cache key**. Each deployed version has its own isolated cache, so a new deployment starts from an empty cache and never serves responses written by a previous version. This is the default because it is the simplest behavior to reason about: a new deployment applies immediately, and you never serve a response that a superseded version produced. The trade-off is that **cache hit rate resets on every deployment**. Because a new version cannot reuse the previous version's cached responses, the first requests after a deploy are misses while the new version's cache fills. This is the most common reason a Worker's cache hit rate drops right after a deployment. If you want to maximize cache hit rate and are willing to accept slower rollouts of cache-affecting changes, set `cross_version_cache` to `true`. Cached responses are then shared across versions — a response written by one version can be served by a later version as long as its TTL has not expired: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true, "cross_version_cache": true, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true cross_version_cache = true ``` Advanced users who deploy frequently and whose responses do not change between most deployments should consider enabling `cross_version_cache` — it avoids throwing away a warm cache on every deploy. The cost is that a deployment no longer invalidates the cache: after a change that alters response content, older cached responses continue to be served until they expire or you [purge](https://developers.cloudflare.com/workers/cache/purge/) them, and during a [gradual deployment](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/) both versions share one cache. When you need a deployment to take effect immediately with `cross_version_cache` enabled, purge the cache after deploying, or tag responses by version — see [Invalidating cache across deployments](https://developers.cloudflare.com/workers/cache/cache-keys/#invalidating-cache-across-deployments). `cross_version_cache` only has an effect when caching is enabled. It applies to every entrypoint whose cache is on. ## Environment-specific configuration The `cache` block can be set at the top level and overridden per [environment](https://developers.cloudflare.com/workers/wrangler/environments/). The typical pattern is to turn caching on in production once you are confident it is safe, while keeping staging uncached for easier debugging: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": false, }, "env": { "production": { "cache": { "enabled": true, }, }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = false [env.production.cache] enabled = true ``` ## Cache-Control semantics With caching enabled, your Worker is the origin for Cloudflare's cache. Standard HTTP `Cache-Control` directives on the response your Worker returns determine whether and for how long Cloudflare caches it. For the full list of directives and how they interact, refer to [Cache-Control](https://developers.cloudflare.com/cache/concepts/cache-control/). Responses with no \`Cache-Control\` header are still cached If your Worker returns a response with **no** `Cache-Control` header (and no `Expires` header), Cloudflare applies [RFC 9111 heuristic freshness ↗](https://www.rfc-editor.org/rfc/rfc9111#name-calculating-heuristic-fresh) with the per-status default TTLs in the following table. Responses whose status is not in the table are not cached by default. | Status code | Description | Default TTL | | ----------- | ----------------------------- | ------------------------- | | 200 | OK | 7200 seconds (2 hours) | | 203 | Non-Authoritative Information | 7200 seconds (2 hours) | | 204 | No Content | 7200 seconds (2 hours) | | 300 | Multiple Choices | 1200 seconds (20 minutes) | | 301 | Moved Permanently | 1200 seconds (20 minutes) | | 404 | Not Found | 180 seconds (3 minutes) | | 405 | Method Not Allowed | 60 seconds (1 minute) | | 410 | Gone | 180 seconds (3 minutes) | | 414 | URI Too Long | 60 seconds (1 minute) | | 501 | Not Implemented | 60 seconds (1 minute) | For example, a `200` response that does not set `Cache-Control` is cached for 2 hours. A `404` is cached for 3 minutes. If you want a response not to be cached, return `Cache-Control: no-store` (or `private`) explicitly. Omitting `Cache-Control` is not the same as opting out of the cache. If you set `Cache-Control` (or `Expires`) on the response, these defaults do not apply — your directives are honored instead. Cache Deception Armor When a response falls back to heuristic freshness (no `Cache-Control` set), Workers Caching also runs [Cache Deception Armor](https://developers.cloudflare.com/cache/cache-security/cache-deception-armor/) to defend against [cache deception attacks ↗](https://owasp.org/www-community/attacks/Cache%5FPoisoning). Cache Deception Armor only inspects responses whose `Content-Type` starts with `text/` or `application/` — the high-risk types for accidentally caching generated, user-specific content under a static-looking URL. For those responses, if the request URI has a file extension that maps to a known MIME type (for example, `.css` → `text/css`) and the actual `Content-Type` does not match, the response is not cached and `Cf-Cache-Status` is `BYPASS`. For example, a Worker that serves `/style.css` with `Content-Type: text/html` will hit this. Responses with `Content-Type` outside `text/*` and `application/*` (for example, `image/png`, `video/mp4`) are not subject to the check — those types are rarely used to deliver private data, so the trade-off of a stricter check is not worth the false positives. A request URI with no file extension (for example, `/api/users/42`) also does not trigger the check. Setting any explicit `Cache-Control` directive skips the check entirely — Cache Deception Armor only activates when freshness is being inferred heuristically. ### Set the freshness window with `max-age` Use `max-age` to control how long the response is treated as fresh: ```js export default { async fetch(request) { const body = await renderPage(request); return new Response(body, { headers: { "Content-Type": "text/html", // Cached for 1 hour at Cloudflare's edge and in the browser. "Cache-Control": "public, max-age=3600", }, }); }, }; // Replace with your own rendering logic. async function renderPage(request) { return `Home

Hello

`; } ``` ```ts export default { async fetch(request): Promise { const body = await renderPage(request); return new Response(body, { headers: { "Content-Type": "text/html", // Cached for 1 hour at Cloudflare's edge and in the browser. "Cache-Control": "public, max-age=3600", }, }); }, } satisfies ExportedHandler; // Replace with your own rendering logic. async function renderPage(request: Request): Promise { return `Home

Hello

`; } ``` If you need browsers and the edge to cache for different durations, use `cdn-cache-control` (or `cloudflare-cdn-cache-control`) for the edge-only directive and keep `Cache-Control` for what browsers see. Refer to [Header precedence](#header-precedence) below. ### Use `stale-while-revalidate` for low-latency refreshes When a cached response becomes stale, `stale-while-revalidate` lets Cloudflare return the stale response immediately and refresh it in the background: ```js export default { async fetch(request) { const data = { timestamp: Date.now() }; return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", // Fresh for 10 minutes; may be served stale for up to 1 minute // while a background revalidation runs. "Cache-Control": "public, max-age=600, stale-while-revalidate=60", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const data = { timestamp: Date.now() }; return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", // Fresh for 10 minutes; may be served stale for up to 1 minute // while a background revalidation runs. "Cache-Control": "public, max-age=600, stale-while-revalidate=60", }, }); }, } satisfies ExportedHandler; ``` \`s-maxage\`, \`must-revalidate\`, and \`proxy-revalidate\` disable \`stale-while-revalidate\` If your response includes any of `s-maxage`, `must-revalidate`, or `proxy-revalidate`, the stale-serving behavior is disabled and Cloudflare will block on a fresh revalidation when the response expires. The same is true for `stale-if-error`. This follows [RFC 9111 §4.2.4 ↗](https://www.rfc-editor.org/rfc/rfc9111#section-4.2.4): those three directives forbid serving stale content. When you want `stale-while-revalidate` to take effect at the edge, use `max-age` for the freshness window — not `s-maxage`. If you need a longer edge TTL than browsers should honor while still using `stale-while-revalidate`, use [cdn-cache-control](#header-precedence) for the edge directive. ### Choose TTL and stale-while-revalidate values High cache hit rate and high freshness are in tension. Background revalidation hides the latency of refreshing the cache, but your Worker still runs once per revalidation — it is not free. Two common patterns: * **Mostly static content with a small tolerance for staleness.** Use a short `max-age` (for example, 60 seconds) and a longer `stale-while-revalidate` window (for example, 3600 seconds). Most requests are `HIT`s; occasional requests trigger a background refresh. * **"Always serve from cache" for high-traffic endpoints.** Use `max-age=0, stale-while-revalidate=`. Every request returns the previously cached response immediately and triggers a background refresh. Your Worker runs once per request to revalidate, so CPU costs are close to running the Worker every time. Freshness drops as request volume drops — if no request arrives for a long time, the next request will see stale content. ### Serve stale on error with `stale-if-error` `stale-if-error` lets Cloudflare return a previously cached response when the Worker fails while refreshing an expired cache entry — for example, when it throws, times out, or returns a `5xx` response. This insulates clients from transient Worker failures. ```ts "Cache-Control": "public, max-age=600, stale-if-error=86400", ``` When the Worker is producing a fresh response, `stale-if-error` has no effect. When the Worker fails while refreshing an expired entry, Cloudflare serves the last successful cached response (with `Cf-Cache-Status: STALE`) for up to the `stale-if-error` window. A true cache miss (no prior entry) cannot benefit from `stale-if-error` because there is nothing stale to serve — Worker errors flow through to clients in that case. Note If you do not set `stale-if-error` explicitly, **and your response does not carry `s-maxage`, `must-revalidate`, or `proxy-revalidate`**, Cloudflare's default behavior is to serve stale responses on Worker error indefinitely (as long as the cached entry has not been purged). This is helpful for resilience but can mask real failures from your monitoring. If you want Worker errors to surface to clients quickly, set `stale-if-error=0`. If your response **does** carry `s-maxage`, `must-revalidate`, or `proxy-revalidate`, `stale-if-error` is disabled regardless of the directive's value or the default. Worker errors flow through to clients immediately. This follows [RFC 9111 §4.2.4 ↗](https://www.rfc-editor.org/rfc/rfc9111#section-4.2.4) and applies to `stale-while-revalidate` as well. ### Header precedence When multiple cache headers are present, the most specific wins: 1. `cloudflare-cdn-cache-control` — Cloudflare-specific, highest precedence. Consumed by Cloudflare and stripped from the response returned to clients. 2. `cdn-cache-control` — standard header for CDN-only directives. Respected by Cloudflare and passed through to downstream CDNs. 3. `Cache-Control` — standard HTTP header. Respected by Cloudflare and passed through to clients. Use `cloudflare-cdn-cache-control` when you want a longer edge TTL than you expose to browsers without leaking the directive downstream. ### Override `Cache-Control` from the calling Worker Normally the callee decides how its responses are cached by setting `Cache-Control` on them. When one entrypoint invokes another cached entrypoint through a [ctx.exports](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) loopback, the **calling** entrypoint can instead supply the `Cache-Control` directive for that call by setting `cf.cacheControl` on the request. Here the `Backend` entrypoint returns no `Cache-Control` of its own; the default entrypoint decides the caching policy when it calls `Backend` through `ctx.exports`: ```js import { WorkerEntrypoint } from "cloudflare:workers"; // Cached entrypoint. It does not set Cache-Control itself. export class Backend extends WorkerEntrypoint { async fetch(request) { return new Response("Hello from the backend", { headers: { "Content-Type": "text/html" }, }); } } // Gateway entrypoint. Caches the Backend's response for this call for // 5 minutes, without the Backend needing to set Cache-Control itself. export default { async fetch(request, env, ctx) { return ctx.exports.Backend.fetch(request, { cf: { cacheControl: "public, max-age=300" }, }); }, }; ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; // Cached entrypoint. It does not set Cache-Control itself. export class Backend extends WorkerEntrypoint { async fetch(request: Request): Promise { return new Response("Hello from the backend", { headers: { "Content-Type": "text/html" }, }); } } // Gateway entrypoint. Caches the Backend's response for this call for // 5 minutes, without the Backend needing to set Cache-Control itself. export default { async fetch(request, env, ctx): Promise { return ctx.exports.Backend.fetch(request, { cf: { cacheControl: "public, max-age=300" }, }); }, } satisfies ExportedHandler; ``` Cloudflare treats `cf.cacheControl` as a trusted `Cache-Control` directive for caching the callee's response on that call. The value is a standard `Cache-Control` string and follows the [same directive semantics](https://developers.cloudflare.com/cache/concepts/cache-control/) described throughout this page — `max-age`, `stale-while-revalidate`, `no-store`, and so on. This lets a calling entrypoint decide how a cached entrypoint's responses are cached without modifying that entrypoint's code. Like [custom cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/#custom-cache-keys), `cf.cacheControl` is honored only for calls that stay within your account. Cloudflare drops the `cf` object whenever a request crosses an account boundary, so a caller in one account cannot change how a Worker in another account caches its responses. The directive also has no effect on eyeball requests, because the `cf` object on an inbound request is populated by Cloudflare rather than by the client. ## Response headers ### `Cf-Cache-Status` Every response carries a `Cf-Cache-Status` header indicating what happened for that request. The values you will see most often are `HIT`, `MISS`, `EXPIRED`, `REVALIDATED`, `UPDATING`, `STALE`, and `BYPASS`. For the full set of values and their meanings, refer to [Cloudflare cache responses](https://developers.cloudflare.com/cache/concepts/cache-responses/). ### `Cache-Tag` The [Cache-Tag](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/) response header attaches tags to a cached response so you can purge it later in bulk. Cloudflare consumes this header and strips it before the response reaches the client. ```js export default { async fetch(request) { const html = `Post`; return new Response(html, { headers: { "Content-Type": "text/html", "Cache-Control": "public, max-age=3600", "Cache-Tag": "blog,posts,post-123", }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const html = `Post`; return new Response(html, { headers: { "Content-Type": "text/html", "Cache-Control": "public, max-age=3600", "Cache-Tag": "blog,posts,post-123", }, }); }, } satisfies ExportedHandler; ``` The `Cache-Tag` header value is a comma-separated list of tags. The same limits as the zone cache apply — refer to [Cache tag limits](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#a-few-things-to-remember) for the full list. The most common constraints to keep in mind: * Tag values must be **printable ASCII** (`0x21`–`0x7E`) — no spaces, no Unicode, no control characters. * Each tag is at most **1024 characters** long. * A response can carry up to **1000 tags** for purge purposes. * Tag matching at purge time is **case-insensitive**. `Foo` and `foo` purge the same set of responses. Invalid tags (over-length, containing spaces, or containing non-ASCII characters) are silently dropped during cache storage — the response is still cached with the remaining valid tags, but you have no way to detect which tags were dropped. Validate tags in your Worker before returning them if this matters. ### Automatic bypass conditions Workers Caching inherits Cloudflare's standard [cache bypass rules](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass). The most common triggers: * The response includes a `Set-Cookie` header (unless `Cache-Control` includes `private="set-cookie"` or `no-cache="set-cookie"`, in which case the `Set-Cookie` is stripped from the cached copy). * The request includes an `Authorization` header. The response is only stored if `Cache-Control` includes `public`, `must-revalidate`, or `s-maxage`, per [RFC 9111 §3.5 ↗](https://www.rfc-editor.org/rfc/rfc9111#name-storing-responses-to-authen). * The response `Cache-Control` header includes `private` or `no-store`. When any of these apply, `Cf-Cache-Status` is `BYPASS` and your Worker runs on every request. \`no-cache\` is not a bypass `Cache-Control: no-cache` does not bypass the cache. The response is stored, but the cached entry is treated as stale immediately — so the next request always needs to consult your Worker before serving. The exact behavior depends on what other directives accompany `no-cache`: * **`no-cache` alone** — every subsequent request is revalidated **inline** with your Worker before any bytes are served. If your Worker returns `304 Not Modified` for a conditional request, `Cf-Cache-Status` is `REVALIDATED` and the cached body is served. If your Worker returns a fresh `200`, `Cf-Cache-Status` is `EXPIRED` and the cached body is replaced. Either way, the Worker runs and CPU time is billed. * **`no-cache, stale-while-revalidate=N`** — Cloudflare serves the cached body immediately and refreshes it in the background by invoking your Worker. `Cf-Cache-Status` is `UPDATING` for the duration of `N` seconds after the entry was last refreshed. After the SWR window elapses, behavior reverts to inline revalidation. In both cases, your Worker is invoked on every request, so CPU time is billed each time. The cached body is preserved across revalidations. Refer to [Understand no-store and no-cache directives](https://developers.cloudflare.com/cache/concepts/cache-control/#understand-no-store-and-no-cache-directives) for the full distinction between these directives. ### Status codes that are never cached A few status codes are never stored, even with explicit `Cache-Control` directives: * **`520`–`526`** (Cloudflare failsafe responses) are treated as transient errors and never cached. ### `Range` requests Workers Caching serves `Range` requests from a cached full response — your Worker does not have to implement byte-range slicing. When a client sends a `Range` request, Cloudflare **strips the `Range` header before invoking your Worker** and asks your Worker for the full body. Your Worker returns a normal `200` response with a `Cache-Control` header (as it would for any other request), Cloudflare stores that full response, and then slices out the requested byte range and returns it to the client as a `206 Partial Content` response (or `416 Range Not Satisfiable` if the range is invalid). Subsequent `Range` requests to the same URL are satisfied entirely from the cached entry — your Worker is not invoked, and `Cf-Cache-Status` is `HIT`. For example, a `GET` with `Range: bytes=0-9` against a cold cache produces a `MISS` on the way in (your Worker runs and returns the full body), then returns `206` with the first 10 bytes. A follow-up `GET Range: bytes=10-19` for the same URL is a `HIT` and returns those 10 bytes from cache without invoking your Worker. If your Worker returns a `206` response of its own — for example, because you implemented `Range` handling inside the Worker — Cloudflare treats it as an uncacheable response and it is not stored. Return a full `200` and let Workers Caching handle range slicing. ### `Vary` When your Worker returns a `Vary` response header, Cloudflare stores a separate cached variant per distinct combination of the listed request header values, and only returns a variant whose stored values match the incoming request. This implements [RFC 9110 ↗](https://www.rfc-editor.org/rfc/rfc9110.html#name-vary) and the cache-key calculation in [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-cache-keys-with). For an introduction with example code, refer to [Content negotiation with Vary](https://developers.cloudflare.com/workers/cache/#content-negotiation-with-vary). How `Vary` is processed for Workers Caching: * **All header names are honored.** Any header name your Worker lists in `Vary` participates in the variant key. There is no allowlist. * **Values are compared verbatim.** Cloudflare does not normalize the listed request headers before keying. `Accept-Encoding: gzip, br` and `Accept-Encoding: br, gzip` produce two separate variants even though they are semantically identical. If you need to fold equivalent values onto the same variant, normalize the headers your Worker sees in a gateway Worker before passing the request on, or canonicalize them inside the Worker that sets `Vary`. * **`Vary: *` disables caching.** A wildcard variance cannot be satisfied deterministically from request headers, so the response is treated as uncacheable and `Cf-Cache-Status` is `BYPASS`. * **Variants share a single purge identity.** [Purging](https://developers.cloudflare.com/workers/cache/purge/) by tag or path prefix invalidates every variant of a URL together. All variants must therefore use the same `Cache-Tag` values — assigning different tags to different variants results in inconsistent purges. * **Image transformation features take precedence.** Responses produced by Polish or Image Resizing already generate their own variants, and `Vary` on those responses is ignored. ### `Accept-Encoding` and `Content-Encoding` Your Worker controls its own content negotiation. Whatever `Content-Encoding` your Worker sets on the response is what Cloudflare stores and serves to subsequent requests. If your Worker needs to return different encodings to different clients, you have two options: * **Pick one canonical encoding inside your Worker.** Decide based on the `Accept-Encoding` request header, encode the body once, and return a single representation. Subsequent requests for that URL hit the same cached entry regardless of what they accept. This produces the highest cache hit rate but requires you to decide which clients you serve which encoding to. * **Vary on `Accept-Encoding`.** Return a different `Content-Encoding` per request and set `Vary: Accept-Encoding`. Cloudflare stores one variant per distinct `Accept-Encoding` value the Worker has seen. Because comparison is verbatim, clients that send semantically equivalent values in different orders or with different quality factors produce separate variants — keep cache fan-out under control by normalizing `Accept-Encoding` (for example, in a gateway Worker) before the response is generated. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/configuration/#page","headline":"Configuration · Cloudflare Workers docs","description":"Enable and configure Workers Caching.","url":"https://developers.cloudflare.com/workers/cache/configuration/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Diagnose why Workers Caching is not behaving the way you expect. title: Debugging image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Debugging Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/debugging/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) If caching is not behaving the way you expect, the `Cf-Cache-Status` response header is the first place to look. Every response carries it, and its value tells you exactly what happened for that request. Note Workers Caching is **your Worker's cache**. When debugging, look at your Worker's traces and responses. Zone-level cache controls and dashboards operate on a separate cache and will not affect what Workers Caching stores or serves. ## Inspect `Cf-Cache-Status` Send two requests to the same URL and compare the headers: ```sh curl -I https://my-worker.example.workers.dev/api/users/42 curl -I https://my-worker.example.workers.dev/api/users/42 ``` Match the status value against the scenarios below. ## My Worker runs on every request `Cf-Cache-Status` is not present. Check that your wrangler version is 4.69.0 or above, and that wrangler.toml or wrangler.jsonc has `cache.enabled = true` for that worker. `Cf-Cache-Status` is `MISS` on every request, or `DYNAMIC`, or `BYPASS`. Caching is not storing anything, or a bypass rule is firing. **Check your `Cache-Control` header.** The response must carry directives that make it cacheable: * `public, max-age=N` — cached in Cloudflare and browsers for `N` seconds. A response with `Cache-Control: private` or `no-store` is not stored, and `Cf-Cache-Status` is `BYPASS`. A response with `Cache-Control: no-cache` _is_ stored, but Cloudflare treats every subsequent request as stale and consults your Worker before serving. The exact `Cf-Cache-Status` depends on whether `stale-while-revalidate` is also set: * With just `Cache-Control: no-cache`, every subsequent request triggers an inline revalidation. `Cf-Cache-Status` is `REVALIDATED` if your Worker returns `304 Not Modified` (body served from cache), or `EXPIRED` if your Worker returns a fresh `200` (body replaced). * With `Cache-Control: no-cache, stale-while-revalidate=N`, the cached body is served immediately and the Worker runs in the background. `Cf-Cache-Status` is `UPDATING` for the SWR window. If you wanted long-lived cache hits, use `max-age` instead. Refer to [no-cache is not a bypass](https://developers.cloudflare.com/workers/cache/configuration/#automatic-bypass-conditions). If the response carries **no** `Cache-Control` header at all, behavior depends on the status code: Workers Caching applies [RFC 9111 heuristic freshness ↗](https://www.rfc-editor.org/rfc/rfc9111#name-calculating-heuristic-fresh) and caches default-cacheable status codes for a heuristic TTL — for example, `200` is cached for 2 hours and `404` for 3 minutes. For the full table of default TTLs, refer to [Responses with no Cache-Control header are still cached](https://developers.cloudflare.com/workers/cache/configuration/#cache-control-semantics) in the configuration reference. If you do not want any of these defaults to apply, set `Cache-Control` explicitly on the response. **Check the request method.** Only `GET` and `HEAD` requests are cached. Everything else is `BYPASS`. `GET` and `HEAD` requests for the same URL share the same cache entry — refer to [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/#what-goes-into-the-cache-key) for how Cloudflare handles populating the cache from either method. **Check for automatic bypass conditions.** Cloudflare bypasses the cache when: * The response includes a `Set-Cookie` header. * The request includes an `Authorization` header, unless the response explicitly sets `Cache-Control: public`, `must-revalidate`, or `s-maxage`. If your Worker unconditionally sets `Set-Cookie` (for example, a session cookie on every response), the response is never cached. Either remove the cookie from cacheable responses, or separate cookie-setting and cacheable responses into different routes. **Check the status code.** Workers Caching follows [RFC 9111 ↗](https://www.rfc-editor.org/rfc/rfc9111). Responses with status codes that are not cacheable by default (for example, `401`, `403`, `500`) are not stored unless you explicitly mark them with cacheable directives. A few status codes are never cached, even with explicit `Cache-Control`: * **`520`–`526`** are treated as Cloudflare failsafe responses and are never stored. * **`206 Partial Content`** returned by your Worker is not stored. Workers Caching handles `Range` requests itself by fetching the full body from your Worker and slicing from the cached entry — if your Worker returns its own `206`, that response is treated as uncacheable. Return a full `200` instead. Refer to [Range requests](https://developers.cloudflare.com/workers/cache/configuration/#range-requests). ## My Worker runs even after the first request `Cf-Cache-Status` is `MISS` on the first request but still `MISS` on subsequent requests. **The cache is likely partitioned.** The cache key includes the request path, the target entrypoint, and the invocation's `ctx.props`. Two requests that look the same to you may produce different cache keys if any of these differ. Common causes: * The URL path or query string differs between requests (even trailing slashes matter). * The calling Worker passes different `ctx.props` for each request — for example, a different user ID. * Requests are hitting different [named entrypoints](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) of the same Worker. Cloudflare does not currently expose the cache key composition, so you cannot see the computed key directly. Instead, walk through the components listed in [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/#what-goes-into-the-cache-key) and verify each one is the same for both requests. ## My cache hit rate dropped after a deployment This is expected with the default configuration. By default, the [Worker version is part of the cache key](https://developers.cloudflare.com/workers/cache/cache-keys/#invalidating-cache-across-deployments), so each new version starts from a cold cache and cannot reuse the previous version's cached responses. The first requests after a deploy are misses while the new version's cache fills, then the hit rate recovers. If you deploy frequently and your responses rarely change between deployments, enable [cache.cross\_version\_cache](https://developers.cloudflare.com/workers/cache/configuration/#cross-version-caching) to share cached responses across versions and avoid resetting the cache on every deploy. The trade-off is that cache-affecting changes no longer apply immediately — see below. ## My cache still serves old content after a deployment By default a deployment takes effect immediately, because the Worker version is part of the cache key and the new version starts from a cold cache. If you are still seeing responses from a previous version, you have [cache.cross\_version\_cache](https://developers.cloudflare.com/workers/cache/configuration/#cross-version-caching) enabled, which shares cached entries across versions. To force a deployment to take effect while keeping `cross_version_cache` on: * **Call [ctx.cache.purge({ purgeEverything: true })](https://developers.cloudflare.com/workers/cache/purge/#purge-everything)** after deploying. This is the simplest approach. * **Tag each cached response with the producing version** using the [version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/), then purge that tag on rollback. Refer to [Version-specific purging](https://developers.cloudflare.com/workers/cache/purge/#version-specific-purging). ## My cache never updates after content changes If your origin data changed but requests still return stale content: * **Check the TTL.** The response stays cached for `max-age` seconds. You may be looking at a response that is still within its freshness window. * **Purge the affected responses.** Use `ctx.cache.purge()` with tags or a path prefix to invalidate specific entries. Refer to [Purging the cache](https://developers.cloudflare.com/workers/cache/purge/). * **Add tags at write time.** If you did not set `Cache-Tag` headers, you cannot purge by tag. Add tags to your cached responses, deploy, and once new entries are written they become purgeable. ## Two callers receive each other's cached responses This should not happen if you use `ctx.props` for per-caller authorization context. If it does, one of the following is true: * You are authenticating callers with a header or query parameter that is not part of the cache key. Move the authorization input into `ctx.props`. Refer to [Multi-tenant safety with ctx.props](https://developers.cloudflare.com/workers/cache/cache-keys/#multi-tenant-safety-with-ctxprops). * You are calling a service binding with a user-specific query parameter that is not present. The query string is part of the cache key; make sure each caller's request path actually differs. ## `Cf-Cache-Status: UPDATING` appears constantly `UPDATING` means the response was served from cache while stale and your Worker is running in the background to refresh it. This is expected behavior when using `stale-while-revalidate`. If you see `UPDATING` more often than you expect: * Your `max-age` is shorter than your request arrival rate. Every request that arrives after `max-age` elapses triggers a revalidation. * With `max-age=0, stale-while-revalidate=`, **every** request triggers a revalidation. This is "always serve from cache" behavior, not "don't run the Worker." Refer to [Choose TTL and stale-while-revalidate values](https://developers.cloudflare.com/workers/cache/configuration/#choose-ttl-and-stale-while-revalidate-values). ## `Cf-Cache-Status: UPDATING` never appears `UPDATING` is emitted only when **all** of the following are true: * A cached entry exists and is past its freshness window (stale). * The response carries `stale-while-revalidate=N`, and the request arrives within `N` seconds of the entry going stale. * The response does **not** also carry `s-maxage`, `must-revalidate`, or `proxy-revalidate`. If any of those is false, requests for stale entries fall through to inline revalidation instead, producing `EXPIRED` (Worker returned a fresh body) or `REVALIDATED` (Worker returned `304 Not Modified`). Common reasons `UPDATING` does not appear: * **No `stale-while-revalidate` directive on the response.** The default SWR window is `0`, so without an explicit directive every stale request is foreground-revalidated. * **`s-maxage`, `must-revalidate`, or `proxy-revalidate` is present.** Per [RFC 9111 §4.2.4 ↗](https://www.rfc-editor.org/rfc/rfc9111#section-4.2.4), these directives forbid serving stale content, so Cloudflare disables `stale-while-revalidate` (and `stale-if-error`) when any of them is present. Use `max-age` for the edge freshness window if you want stale-serving to work. * **The SWR window has elapsed.** If your response uses `max-age=60, stale-while-revalidate=120`, you will see `UPDATING` for requests arriving in the 120 seconds after the entry goes stale. Requests arriving after that revert to inline revalidation. ## `Cf-Cache-Status: STALE` appears unexpectedly `STALE` means Cloudflare served a previously cached response because your Worker errored on the request that would have refreshed it — for example, the Worker threw, timed out, or returned a `5xx` response. This is `stale-if-error` behavior. Refer to [Serve stale on error with stale-if-error](https://developers.cloudflare.com/workers/cache/configuration/#serve-stale-on-error-with-stale-if-error). If you see `STALE` and did not expect it: * **Your Worker is failing on cache fills or revalidations.** Check the [Workers observability dashboard](https://developers.cloudflare.com/workers/observability/) for errors on the requests that should have produced a fresh response. The fact that clients see a stale response instead of a `5xx` is masking a real failure. * **You did not set `stale-if-error` explicitly, and your response does not include `s-maxage`/`must-revalidate`/`proxy-revalidate`.** In that case, Cloudflare's default behavior is to serve stale responses on Worker error indefinitely, as long as the cached entry has not been purged. If you want errors to surface to clients quickly, set `stale-if-error=0` in `Cache-Control`. For details, refer to [Serve stale on error with stale-if-error](https://developers.cloudflare.com/workers/cache/configuration/#serve-stale-on-error-with-stale-if-error). * **A previously deployed version is being served.** If you deployed a fix but `STALE` keeps appearing, the cached entry from the broken version is still being served on every error. [Purge](https://developers.cloudflare.com/workers/cache/purge/) the affected entries to force a fresh fill from the current version. To distinguish a `STALE` from a normal `HIT` in client-side observability, log `Cf-Cache-Status` alongside the response — `STALE` is the only signal that the Worker is failing and clients are not seeing it. ## My response is larger than the size limit If a response is too large to cache, Cloudflare does not store it. You will see `Cf-Cache-Status: MISS` on every request even though the response otherwise looks cacheable. For per-plan response size limits, refer to [Cacheable size limits](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cacheable-size-limits). Note that at launch all Workers Caching responses are subject to the Free plan size limit — refer to [Response size](https://developers.cloudflare.com/workers/cache/limitations/#response-size) for details. ## I need more visibility At launch, the primary debugging surfaces are the `Cf-Cache-Status` response header and per-invocation cache-hit information in the [Workers observability dashboard](https://developers.cloudflare.com/workers/observability/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/debugging/#page","headline":"Debugging · Cloudflare Workers docs","description":"Diagnose why Workers Caching is not behaving the way you expect.","url":"https://developers.cloudflare.com/workers/cache/debugging/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Patterns for combining Workers Caching with authentication, request normalization, and Durable Objects. title: Examples image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Examples Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/examples/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Workers Caching is **a cache that is itself a Worker primitive**. It sits in front of every Worker entrypoint — the default export and every named [WorkerEntrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) — and it also sits in front of `fetch()` calls between entrypoints in the same Worker via [ctx.exports](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/). That second fact is the one that makes the rest of this page possible. When one entrypoint invokes another's `fetch()` via `ctx.exports`, the cache evaluates that call the same way it would evaluate a request from a browser. A hit returns the cached response without the callee running. A miss runs the callee and stores the response under its own cache key, keyed by the callee's entrypoint, path, query string, and [ctx.props](https://developers.cloudflare.com/workers/cache/cache-keys/#multi-tenant-safety-with-ctxprops). The caller still runs on every request — but anything the caller hands off to the callee is cacheable independently. Note Only `fetch()` invocations go through the cache. Calling a custom RPC method on an entrypoint (for example `ctx.exports.Backend.getUser(id)`) bypasses the cache entirely. To cache a piece of work, expose it as a `fetch` handler. That gives you a primitive you can compose. You can author a Worker as a chain of small entrypoints — auth, normalization, routing, the expensive read, the data layer — and let Workers Caching slot in wherever you want it. Each cached entrypoint is a unit of memoization with its own key, its own TTL, and its own tag namespace for purging. Anything you would want to configure about caching — when it runs, what it keys on, when it invalidates — is expressed as ordinary Worker code: which entrypoint you call, what request you forward, what `ctx.props` you pass, what `Cache-Control` you set. The examples on this page all use the same shape: an outer (gateway) entrypoint that runs every request, plus one or more inner entrypoints that are cached. The outer entrypoint does something cheap (authenticate, rewrite a header, pick a route); the inner entrypoint does something expensive (look up data, transform it, run a Durable Object). They are written as classes in one source file, deployed as one Worker, billed as one Worker — connected by a cache stage that sits in front of the inner entrypoint. ### Two rules to keep in mind Two facts shape every pattern below. They follow directly from "the cache is in front of every entrypoint": **Disable caching on the gateway entrypoint.** Because the cache sits in front of every entrypoint by default, the outer entrypoint would itself be cached — and the next request would be served from that outer cache without ever entering your gateway logic. Turn caching off for the gateway entrypoint in your Wrangler configuration, and leave it on for the inner entrypoint the gateway forwards to. Using `"default"` for the default export: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { // The gateway runs on every request — no caching in front of it. "default": { "type": "worker", "cache": { "enabled": false } }, // The inner entrypoint is the one that gets cached. "Inner": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.Inner] type = "worker" [exports.Inner.cache] enabled = true ``` Do not use \`Cache-Control: no-store\` to keep the gateway running It might seem simpler to leave caching on for the gateway and return `Cache-Control: no-store` from it on every request. Do not do this. With caching enabled, each request to the gateway still consults the lower and upper [cache tiers](https://developers.cloudflare.com/workers/cache/#tiered-cache) before your Worker runs — so every request pays the tiered-cache round trip only to produce an uncacheable response. That adds significant latency for no benefit. Disabling caching on the entrypoint in `exports` makes requests skip the cache lookup entirely and go straight to your gateway logic. **Strip request headers that would force a bypass.** Cloudflare's standard [bypass rules](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass) apply to the inner entrypoint's cache too — an `Authorization` header on the forwarded request will turn every inner call into a `BYPASS`, and nothing will ever be stored. When the outer entrypoint authenticates the request and decides it is safe to cache, it must strip `Authorization` (and anything else that triggers automatic bypass) before invoking the inner entrypoint. Both rules apply to every example below. ## Cache authenticated responses Caching authenticated APIs has historically been awkward. The standard [bypass rules](https://developers.cloudflare.com/cache/concepts/cache-responses/#bypass) treat any request with an `Authorization` header as private and refuse to cache it — which is the safe default, but means a token-authenticated endpoint that returns identical responses to thousands of users runs your Worker every single time. The pattern below lets you authenticate every request and still serve cache hits without running the cacheable handler: 1. The outer (default) entrypoint receives the request and authenticates it. 2. On success, it strips the `Authorization` header and forwards the request to a named entrypoint via `ctx.exports`. 3. Workers Caching sits in front of the named entrypoint. On a hit, the cached response is returned to the outer entrypoint, which returns it to the client — without the named entrypoint ever running. Disable caching on the default entrypoint so it runs on every request to authenticate, and keep it on for `CachedAPI`: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "CachedAPI": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.CachedAPI] type = "worker" [exports.CachedAPI.cache] enabled = true ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; // Cached entrypoint. Workers Caching sits in front of this — on a hit, // the cached response is returned and `fetch` below is never invoked. export class CachedAPI extends WorkerEntrypoint { async fetch(request) { const data = await loadExpensiveData(request); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", // All authenticated callers see this same response on a hit. "Cache-Control": "public, max-age=60", }, }); } } // Default entrypoint. Runs on every request to authenticate the caller, // then forwards to the cached entrypoint. export default { async fetch(request, env, ctx) { if (!(await authenticate(request, env))) { return new Response("Unauthorized", { status: 401 }); } // Strip the Authorization header before forwarding. Otherwise the // request would trigger Cloudflare's automatic bypass for // authenticated requests, and nothing would ever be cached. const forwarded = new Request(request); forwarded.headers.delete("Authorization"); // Caching is disabled for this gateway entrypoint (see the Wrangler // configuration above), so it runs on every request. Forward to the // cached CachedAPI entrypoint and return its response directly. return ctx.exports.CachedAPI.fetch(forwarded); }, }; async function authenticate(request, env) { const token = request.headers.get("Authorization")?.replace(/^Bearer\s+/, ""); return token === env.API_TOKEN; } async function loadExpensiveData(request) { // Replace with your real data source — D1, KV, an origin, and so on. return { timestamp: Date.now() }; } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; interface Env { API_TOKEN: string; } // Cached entrypoint. Workers Caching sits in front of this — on a hit, // the cached response is returned and `fetch` below is never invoked. export class CachedAPI extends WorkerEntrypoint { async fetch(request: Request): Promise { const data = await loadExpensiveData(request); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", // All authenticated callers see this same response on a hit. "Cache-Control": "public, max-age=60", }, }); } } // Default entrypoint. Runs on every request to authenticate the caller, // then forwards to the cached entrypoint. export default { async fetch(request, env, ctx): Promise { if (!(await authenticate(request, env))) { return new Response("Unauthorized", { status: 401 }); } // Strip the Authorization header before forwarding. Otherwise the // request would trigger Cloudflare's automatic bypass for // authenticated requests, and nothing would ever be cached. const forwarded = new Request(request); forwarded.headers.delete("Authorization"); // Caching is disabled for this gateway entrypoint (see the Wrangler // configuration above), so it runs on every request. Forward to the // cached CachedAPI entrypoint and return its response directly. return ctx.exports.CachedAPI.fetch(forwarded); }, } satisfies ExportedHandler; async function authenticate(request: Request, env: Env): Promise { const token = request.headers.get("Authorization")?.replace(/^Bearer\s+/, ""); return token === env.API_TOKEN; } async function loadExpensiveData(request: Request): Promise { // Replace with your real data source — D1, KV, an origin, and so on. return { timestamp: Date.now() }; } ``` A few things to notice: * **The cache is in the right place.** It sits between the outer entrypoint and the cached entrypoint, so cache hits skip the expensive work entirely. Only the auth check runs. * **`Authorization` is stripped before forwarding.** This is what makes the response cacheable — Cloudflare's bypass rule fires on the inbound request, not on the response, so removing the header before the request reaches the cached entrypoint is what lets the cached entrypoint's `Cache-Control: public` take effect. It also prevents tokens from contributing to any future cache key. * **The cached response is shared across users.** Every caller who passes the auth check sees the same cached body. Caution This example is intentionally minimal to show the shape of the pattern. Sharing a single cached response across every authenticated caller is only correct when the response really is the same for all of them — for example, a public catalog behind a token gate, or an internal endpoint where every caller is equally privileged. If different callers should see different data, the next section shows how to partition the cache per user with `ctx.props`. Workers Caching does not infer per-user isolation for you; you have to ask for it. ### Per-user authenticated responses If your endpoint returns user-specific data, pass the user identifier via `ctx.props`. Workers Caching includes `ctx.props` in the cache key, so each user gets their own cache entry and one user can never receive another user's cached response. This uses the same Wrangler configuration as the previous example — caching disabled on `default`, enabled on `CachedAPI`: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class CachedAPI extends WorkerEntrypoint { async fetch(request) { // ctx.props.userId is part of the cache key, so this response // is cached separately for every userId. const { userId } = this.ctx.props; const data = await loadUserData(userId); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=60", }, }); } } export default { async fetch(request, env, ctx) { const userId = await authenticate(request, env); if (!userId) { return new Response("Unauthorized", { status: 401 }); } const forwarded = new Request(request); forwarded.headers.delete("Authorization"); // The gateway's cache is disabled, so it runs on every request. // Pass the authenticated userId to the cached entrypoint via props — // this becomes part of the cache key. return ctx.exports.CachedAPI.fetch(forwarded, { props: { userId }, }); }, }; async function authenticate(request, env) { // Replace with your real auth — JWT verification, token lookup, and so on. return "user-42"; } async function loadUserData(userId) { return { userId, timestamp: Date.now() }; } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; interface Env { API_TOKEN: string; } interface Props { userId: string; } export class CachedAPI extends WorkerEntrypoint { async fetch(request: Request): Promise { // ctx.props.userId is part of the cache key, so this response // is cached separately for every userId. const { userId } = this.ctx.props; const data = await loadUserData(userId); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=60", }, }); } } export default { async fetch(request, env, ctx): Promise { const userId = await authenticate(request, env); if (!userId) { return new Response("Unauthorized", { status: 401 }); } const forwarded = new Request(request); forwarded.headers.delete("Authorization"); // The gateway's cache is disabled, so it runs on every request. // Pass the authenticated userId to the cached entrypoint via props — // this becomes part of the cache key. return ctx.exports.CachedAPI.fetch(forwarded, { props: { userId }, }); }, } satisfies ExportedHandler; async function authenticate( request: Request, env: Env, ): Promise { // Replace with your real auth — JWT verification, token lookup, and so on. return "user-42"; } async function loadUserData(userId: string): Promise { return { userId, timestamp: Date.now() }; } ``` For more on cache isolation between callers, refer to [Multi-tenant safety with ctx.props](https://developers.cloudflare.com/workers/cache/cache-keys/#multi-tenant-safety-with-ctxprops). The shape of this example — the outer entrypoint shapes a value (the user's identity) into the cache key by passing it through `ctx.props` — is the same shape the next example uses to influence a different part of the key. ## Normalize `Accept-Encoding` for `Vary` [Vary](https://developers.cloudflare.com/workers/cache/#content-negotiation-with-vary) lets a single URL cache multiple representations — for example, a Brotli-encoded and gzip-encoded variant of the same asset. Cloudflare keys variants on the **verbatim value** of each `Vary`\-listed request header, so two requests with semantically equivalent but textually different `Accept-Encoding` headers produce two separate variants. For requests routed through Cloudflare's front line, this matters even more: the `Accept-Encoding` request header your Worker sees has typically been rewritten by Cloudflare to a canonical value (such as `gzip, br`) for cache efficiency. The original value is preserved at [request.cf.clientAcceptEncoding](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties), but if your Worker varies on `Accept-Encoding` without restoring the eyeball's value first, every cached variant ends up keyed on the rewritten string — so the cache returns a Brotli variant to clients that only accept gzip, or the other way around. The fix is a gateway entrypoint that restores `Accept-Encoding` from `request.cf.clientAcceptEncoding` before forwarding to the cached entrypoint. Disable caching on the gateway and enable it on `CachedAssets`: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "CachedAssets": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.CachedAssets] type = "worker" [exports.CachedAssets.cache] enabled = true ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class CachedAssets extends WorkerEntrypoint { async fetch(request) { const accept = request.headers.get("Accept-Encoding") ?? ""; const wantsBrotli = accept.includes("br"); const { body, encoding } = wantsBrotli ? await loadBrotli(request) : await loadGzip(request); return new Response(body, { headers: { "Content-Type": "application/javascript", "Content-Encoding": encoding, "Cache-Control": "public, max-age=86400, immutable", // One variant per distinct Accept-Encoding value the cached // entrypoint sees. The gateway below normalizes that value. Vary: "Accept-Encoding", }, }); } } export default { async fetch(request, env, ctx) { // On Cloudflare, the eyeball's Accept-Encoding is usually rewritten // to a canonical value before the Worker runs. Restore it from // request.cf.clientAcceptEncoding so the cached entrypoint sees // what the client actually sent — and so Vary keys variants on // the real value. const original = request.cf?.clientAcceptEncoding; const forwarded = new Request(request); if (original) { forwarded.headers.set("Accept-Encoding", original); } // The gateway's cache is disabled (see the Wrangler configuration // above), so it runs on every request and always restores // Accept-Encoding before forwarding to the cached entrypoint. return ctx.exports.CachedAssets.fetch(forwarded); }, }; async function loadBrotli(request) { // Replace with your real asset loader (R2, KV, fetch, and so on). return { body: new ArrayBuffer(0), encoding: "br" }; } async function loadGzip(request) { return { body: new ArrayBuffer(0), encoding: "gzip" }; } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export class CachedAssets extends WorkerEntrypoint { async fetch(request: Request): Promise { const accept = request.headers.get("Accept-Encoding") ?? ""; const wantsBrotli = accept.includes("br"); const { body, encoding } = wantsBrotli ? await loadBrotli(request) : await loadGzip(request); return new Response(body, { headers: { "Content-Type": "application/javascript", "Content-Encoding": encoding, "Cache-Control": "public, max-age=86400, immutable", // One variant per distinct Accept-Encoding value the cached // entrypoint sees. The gateway below normalizes that value. Vary: "Accept-Encoding", }, }); } } export default { async fetch(request, env, ctx): Promise { // On Cloudflare, the eyeball's Accept-Encoding is usually rewritten // to a canonical value before the Worker runs. Restore it from // request.cf.clientAcceptEncoding so the cached entrypoint sees // what the client actually sent — and so Vary keys variants on // the real value. const original = request.cf?.clientAcceptEncoding; const forwarded = new Request(request); if (original) { forwarded.headers.set("Accept-Encoding", original); } // The gateway's cache is disabled (see the Wrangler configuration // above), so it runs on every request and always restores // Accept-Encoding before forwarding to the cached entrypoint. return ctx.exports.CachedAssets.fetch(forwarded); }, } satisfies ExportedHandler; async function loadBrotli( request: Request, ): Promise<{ body: ArrayBuffer; encoding: string }> { // Replace with your real asset loader (R2, KV, fetch, and so on). return { body: new ArrayBuffer(0), encoding: "br" }; } async function loadGzip( request: Request, ): Promise<{ body: ArrayBuffer; encoding: string }> { return { body: new ArrayBuffer(0), encoding: "gzip" }; } ``` Things to notice: * **The gateway runs on every request, but it is small.** It only restores one header and calls `ctx.exports`. The expensive work — picking the encoding, loading the asset — runs only on cache misses. * **Variants share a single purge identity.** Purging by tag or path prefix invalidates every variant of a URL together, so all variants must use the same [Cache-Tag](https://developers.cloudflare.com/workers/cache/configuration/#cache-tag) values. Refer to the notes in [Content negotiation with Vary](https://developers.cloudflare.com/workers/cache/#content-negotiation-with-vary). * **The same pattern applies to other normalizable headers.** If you want to vary on `Accept-Language` and you receive a long, complex value from browsers, normalize it in the gateway (for example, fold it down to the primary language tag) before forwarding. This keeps the cache fan-out bounded. If you do not need per-encoding variants — for example, if your Worker always returns Brotli when the client accepts it and otherwise falls back to gzip — you do not need `Vary` at all. Pick a canonical encoding inside the cached entrypoint based on the restored `Accept-Encoding`, and let the cache store a single variant. Refer to [Accept-Encoding and Content-Encoding](https://developers.cloudflare.com/workers/cache/configuration/#accept-encoding-and-content-encoding) for that variant of the pattern. So far the inner entrypoint has been a function of the request. The next example puts a stateful component — a Durable Object — behind the same cache stage, with the same shape. ## Cache Durable Object responses [Durable Objects](https://developers.cloudflare.com/durable-objects/) are never cached directly by Workers Caching — they are stateful, and caching their responses would defeat the point. But many Durable Object endpoints serve read-heavy traffic where a short cache TTL is perfectly acceptable: leaderboards, counters, aggregated stats, configuration that changes a few times an hour. You can cache those responses by wrapping the Durable Object behind a named entrypoint and letting Workers Caching sit in front of the entrypoint. On a cache hit, the wrapper never runs and the Durable Object is never touched. Disable caching on the default (router) entrypoint and enable it on the `CachedLeaderboard` wrapper — the Durable Object itself is never cached and needs no cache configuration: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "CachedLeaderboard": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.CachedLeaderboard] type = "worker" [exports.CachedLeaderboard.cache] enabled = true ``` ```js import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; // A Durable Object that maintains an expensive-to-compute leaderboard. export class Leaderboard extends DurableObject { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/top") { const top = await this.computeTop(); return new Response(JSON.stringify(top), { headers: { "Content-Type": "application/json" }, }); } if (url.pathname === "/record" && request.method === "POST") { const { userId, score } = await request.json(); await this.record(userId, score); return new Response("Recorded"); } return new Response("Not found", { status: 404 }); } async computeTop() { // Pretend this is expensive — a sorted scan of stored state, an // aggregation across many keys, a call to another service. return { top: [], computedAt: Date.now() }; } async record(userId, score) { await this.ctx.storage.put(`score:${userId}`, score); } } // Cached entrypoint. Forwards GET /top to the Durable Object and tags // the response so it can be purged when scores change. export class CachedLeaderboard extends WorkerEntrypoint { async fetch(request) { const id = this.env.LEADERBOARD.idFromName("global"); const stub = this.env.LEADERBOARD.get(id); const response = await stub.fetch(request); // Copy the body and headers into a new Response so we can attach // cache headers. The DO's body stream is consumed once here. return new Response(response.body, { status: response.status, headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=30", "Cache-Tag": "leaderboard", }, }); } // Invalidate this entrypoint's cached leaderboard. purge() is scoped to // the entrypoint that calls it, so it must run inside CachedLeaderboard — // the entrypoint that owns the cached response. The gateway invokes this // over ctx.exports after a write. async invalidate() { await this.ctx.cache.purge({ tags: ["leaderboard"] }); } } // Default entrypoint. Routes reads through the cached entrypoint // and writes directly to the Durable Object, invalidating the cache on write. export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (request.method === "GET" && url.pathname === "/top") { // Read path — goes through Workers Caching. The router's cache is // disabled (see the Wrangler configuration above), so it runs on // every request. On a hit, CachedLeaderboard never runs and the // Durable Object is never touched. return ctx.exports.CachedLeaderboard.fetch(request); } if (request.method === "POST" && url.pathname === "/record") { // Write path — bypass the cached entrypoint, hit the Durable // Object directly, then ask CachedLeaderboard to invalidate its // own cache so the next read returns fresh data. The purge must // run inside CachedLeaderboard because purges are scoped to the // entrypoint that owns the cached response — a purge from this // gateway would target the gateway's (disabled) cache instead. const id = env.LEADERBOARD.idFromName("global"); const stub = env.LEADERBOARD.get(id); const result = await stub.fetch(request); await ctx.exports.CachedLeaderboard.invalidate(); return result; } return new Response("Not found", { status: 404 }); }, }; ``` ```ts import { DurableObject, WorkerEntrypoint } from "cloudflare:workers"; interface Env { LEADERBOARD: DurableObjectNamespace; } // A Durable Object that maintains an expensive-to-compute leaderboard. export class Leaderboard extends DurableObject { async fetch(request: Request): Promise { const url = new URL(request.url); if (url.pathname === "/top") { const top = await this.computeTop(); return new Response(JSON.stringify(top), { headers: { "Content-Type": "application/json" }, }); } if (url.pathname === "/record" && request.method === "POST") { const { userId, score } = await request.json<{ userId: string; score: number; }>(); await this.record(userId, score); return new Response("Recorded"); } return new Response("Not found", { status: 404 }); } private async computeTop(): Promise { // Pretend this is expensive — a sorted scan of stored state, an // aggregation across many keys, a call to another service. return { top: [], computedAt: Date.now() }; } private async record(userId: string, score: number): Promise { await this.ctx.storage.put(`score:${userId}`, score); } } // Cached entrypoint. Forwards GET /top to the Durable Object and tags // the response so it can be purged when scores change. export class CachedLeaderboard extends WorkerEntrypoint { async fetch(request: Request): Promise { const id = this.env.LEADERBOARD.idFromName("global"); const stub = this.env.LEADERBOARD.get(id); const response = await stub.fetch(request); // Copy the body and headers into a new Response so we can attach // cache headers. The DO's body stream is consumed once here. return new Response(response.body, { status: response.status, headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=30", "Cache-Tag": "leaderboard", }, }); } // Invalidate this entrypoint's cached leaderboard. purge() is scoped to // the entrypoint that calls it, so it must run inside CachedLeaderboard — // the entrypoint that owns the cached response. The gateway invokes this // over ctx.exports after a write. async invalidate(): Promise { await this.ctx.cache.purge({ tags: ["leaderboard"] }); } } // Default entrypoint. Routes reads through the cached entrypoint // and writes directly to the Durable Object, invalidating the cache on write. export default { async fetch(request, env, ctx): Promise { const url = new URL(request.url); if (request.method === "GET" && url.pathname === "/top") { // Read path — goes through Workers Caching. The router's cache is // disabled (see the Wrangler configuration above), so it runs on // every request. On a hit, CachedLeaderboard never runs and the // Durable Object is never touched. return ctx.exports.CachedLeaderboard.fetch(request); } if (request.method === "POST" && url.pathname === "/record") { // Write path — bypass the cached entrypoint, hit the Durable // Object directly, then ask CachedLeaderboard to invalidate its // own cache so the next read returns fresh data. The purge must // run inside CachedLeaderboard because purges are scoped to the // entrypoint that owns the cached response — a purge from this // gateway would target the gateway's (disabled) cache instead. const id = env.LEADERBOARD.idFromName("global"); const stub = env.LEADERBOARD.get(id); const result = await stub.fetch(request); await ctx.exports.CachedLeaderboard.invalidate(); return result; } return new Response("Not found", { status: 404 }); }, } satisfies ExportedHandler; ``` Why this works: * **Reads pay nothing on a cache hit.** Workers Caching sits in front of `CachedLeaderboard`, so a hit returns the cached body without invoking the wrapper, without invoking the Durable Object, and without doing the expensive aggregation. The default entrypoint still runs to dispatch the request, but it is a thin router. * **Writes invalidate the cache immediately.** The POST handler updates the Durable Object and then calls `ctx.exports.CachedLeaderboard.invalidate()`, which runs [purge({ tags: \["leaderboard"\] })](https://developers.cloudflare.com/workers/cache/purge/#purge-by-tag) _inside_ `CachedLeaderboard`. This matters because [purges are scoped to the entrypoint that calls them](https://developers.cloudflare.com/workers/cache/purge/#purge-modes) — the gateway's cache is disabled, so a purge issued from the gateway would not touch the entries `CachedLeaderboard` stored. The very next GET misses the cache, reruns the wrapper, and stores a fresh response. * **The cached entrypoint owns the cache contract.** All cache-control headers are set in `CachedLeaderboard`, including the `Cache-Tag`, and `CachedLeaderboard` also exposes the `invalidate()` method that purges them. The Durable Object stays unaware of caching. If you have many independent Durable Object instances — for example, one per tenant — pass the tenant identifier via `ctx.props` when invoking the cached entrypoint, the same way [Per-user authenticated responses](#per-user-authenticated-responses) does. Each tenant gets its own cache entry, and a purge on one tenant does not invalidate any other. ## Cache an origin you do not control Sometimes the origin you depend on is not yours. A third-party API, a SaaS endpoint, a public dataset, a vendor service behind a slow CDN — its caching headers are whatever the owner decided to ship, and you cannot change them. Maybe it sends `Cache-Control: no-store` to be safe. Maybe it sends nothing at all. Maybe it caches aggressively in a way that does not match your application's read patterns. Either way, you pay the latency and the request cost on every call. Workers Caching lets you put your own cache layer in front of that origin without changing anything on the origin side. The pattern is the same outer-plus-inner shape as the rest of this page: a thin entrypoint that forwards to the origin, with Workers Caching sitting in front of it and applying the `Cache-Control` directives you choose. The origin keeps its own caching contract with the rest of the world; your Worker just adds a second, user-controlled layer between your application and that origin. As with the other patterns, disable caching on the gateway and enable it on `CachedOrigin`: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true }, "exports": { "default": { "type": "worker", "cache": { "enabled": false } }, "CachedOrigin": { "type": "worker", "cache": { "enabled": true } }, }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true [exports.default] type = "worker" [exports.default.cache] enabled = false [exports.CachedOrigin] type = "worker" [exports.CachedOrigin.cache] enabled = true ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; const ORIGIN = "https://api.example.com"; // Cached entrypoint. Fetches the upstream origin and overlays your own // Cache-Control on the response. Workers Caching sits in front of this, // so on a hit the upstream origin is never contacted. export class CachedOrigin extends WorkerEntrypoint { async fetch(request) { const url = new URL(request.url); const upstream = new URL(url.pathname + url.search, ORIGIN); // Forward the request to the third-party origin. The origin's own // caching headers (or lack of them) are about to be overwritten — // they apply to the origin's relationship with the public internet, // not to your cache layer. const response = await fetch(upstream, { method: request.method, headers: request.headers, body: request.body, }); // Replace the origin's Cache-Control with your own. This is the // whole point of the pattern: you decide how long Workers Caching // stores this response, regardless of what the origin says. const headers = new Headers(response.headers); headers.set("Cache-Control", "public, max-age=300"); headers.set("Cache-Tag", "origin:example"); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); } } // Default entrypoint. Forwards every request through the cached entrypoint. export default { async fetch(request, env, ctx) { // The gateway's cache is disabled (see the Wrangler configuration // above), so it runs on every request and forwards to the cached // CachedOrigin entrypoint. return ctx.exports.CachedOrigin.fetch(request); }, }; ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; const ORIGIN = "https://api.example.com"; // Cached entrypoint. Fetches the upstream origin and overlays your own // Cache-Control on the response. Workers Caching sits in front of this, // so on a hit the upstream origin is never contacted. export class CachedOrigin extends WorkerEntrypoint { async fetch(request: Request): Promise { const url = new URL(request.url); const upstream = new URL(url.pathname + url.search, ORIGIN); // Forward the request to the third-party origin. The origin's own // caching headers (or lack of them) are about to be overwritten — // they apply to the origin's relationship with the public internet, // not to your cache layer. const response = await fetch(upstream, { method: request.method, headers: request.headers, body: request.body, }); // Replace the origin's Cache-Control with your own. This is the // whole point of the pattern: you decide how long Workers Caching // stores this response, regardless of what the origin says. const headers = new Headers(response.headers); headers.set("Cache-Control", "public, max-age=300"); headers.set("Cache-Tag", "origin:example"); return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }); } } // Default entrypoint. Forwards every request through the cached entrypoint. export default { async fetch(request, env, ctx): Promise { // The gateway's cache is disabled (see the Wrangler configuration // above), so it runs on every request and forwards to the cached // CachedOrigin entrypoint. return ctx.exports.CachedOrigin.fetch(request); }, } satisfies ExportedHandler; ``` What is happening here: * **The cache layer is yours.** The origin's `Cache-Control` is replaced before the response reaches Workers Caching, so the TTL, freshness directives, and `Cache-Tag` namespace are all controlled by your code. You decide when the cache holds onto a response, and you decide when to purge it via [ctx.cache.purge()](https://developers.cloudflare.com/workers/cache/purge/). * **The origin's own caching model is untouched.** Your Worker is the only thing that sees the rewritten `Cache-Control`. The origin still serves its other clients with whatever caching contract it published — you have not changed its behaviour or its security model, you have only added a layer in front of it for your application. * **Cache hits never touch the origin.** Workers Caching sits in front of `CachedOrigin`, so a hit returns the stored response without invoking `fetch` against the upstream. This is what cuts the origin request volume and the latency of every cached call. A few common extensions to this pattern: * **Per-resource TTLs.** If different paths on the upstream should have different freshness, branch on `url.pathname` inside `CachedOrigin` and set a different `max-age` (and a different `Cache-Tag`) for each. The cache key already includes the path and query string, so each resource gets its own entry. * **Per-user caching.** If your application authenticates the caller and the upstream returns user-specific data, authenticate in the outer entrypoint and pass the user identifier via `ctx.props` to `CachedOrigin` — the same shape as [Per-user authenticated responses](#per-user-authenticated-responses). Each user gets their own cache entry, and one user can never receive another user's cached response. * **Stale-while-revalidate.** If the origin is slow or flaky, set `Cache-Control: public, max-age=60, stale-while-revalidate=600` on the cached response. Most requests return the cached body immediately, and Workers Caching refreshes the origin in the background. Refer to [Use stale-while-revalidate for low-latency refreshes](https://developers.cloudflare.com/workers/cache/configuration/#use-stale-while-revalidate-for-low-latency-refreshes). * **Targeted invalidation.** Tag responses with `Cache-Tag` values that reflect your application's data model (for example, `Cache-Tag: origin:example, product:42`). When you know the upstream has changed — a webhook fires, an admin action runs — call `ctx.cache.purge({ tags: ["product:42"] })` and the next request repopulates the cache. This is the same building block as every other example on this page. The only difference is that the "expensive work" the cached entrypoint does on a miss is a `fetch` to somebody else's server. The control over how long that response lives, how it is keyed, and when it is invalidated stays entirely in your Worker. ## Composing the patterns All four examples are the same architecture seen through four lenses: | Outer entrypoint | What the cache stage is doing | Inner entrypoint | | ------------------------- | --------------------------------------------- | ----------------------------------------------- | | Authenticate the request | Caching an expensive computation per user | Loads or computes the user's data | | Restore Accept-Encoding | Caching one variant per real encoding | Loads the correctly-encoded asset | | Route reads vs. writes | Caching reads, invalidating them on writes | Wraps a Durable Object behind a Cache-Tag | | Forward the request as-is | Caching a third-party origin under your terms | Fetches the upstream and overlays Cache-Control | The only thing that changes between rows is what the outer entrypoint does before the call and what the inner entrypoint does on a miss. The cache stage in the middle is the same primitive every time — keyed by the inner entrypoint, the request path and query string, and `ctx.props`; configured by the inner entrypoint's `Cache-Control` and `Cache-Tag`; invalidated by `ctx.cache.purge()` from whichever entrypoint owns the data. That uniformity is what makes the patterns compose. Nothing stops you from stacking them in a single Worker: * An outer entrypoint that authenticates and routes. * A normalization entrypoint that strips tracking query parameters, restores `Accept-Encoding`, and shapes the request into a canonical form. * A cached entrypoint that fronts a Durable Object, tagged for purging. * A separate cached entrypoint for an unauthenticated public endpoint, also reachable through the same outer entrypoint, with its own cache key and `Cache-Tag` namespace. Each call between these entrypoints goes through its own cache stage. The chain is built out of the same three building blocks — `WorkerEntrypoint`, `ctx.exports`, and a `Cache-Control` header — and the cache is a stage of the chain rather than a separate system bolted on. Whatever you would have configured in a cache rules engine, you now write as code: which entrypoint runs, what request gets forwarded, what props get passed, what `Cache-Control` gets returned, what gets purged. There is no fixed list of patterns. Workers Caching gives you a cache between every Worker entrypoint — what you build with that is up to you. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/examples/#page","headline":"Examples · Cloudflare Workers docs","description":"Patterns for combining Workers Caching with authentication, request normalization, and Durable Objects.","url":"https://developers.cloudflare.com/workers/cache/examples/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Current limitations, unsupported scenarios, and how Workers Caching relates to other Cloudflare caches. title: Limitations image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Limitations Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/limitations/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This page lists the scenarios where Workers Caching does not apply, followed by notes on how it relates to other caches you may already be using. ## Unsupported scenarios ### HTTP methods Only `GET` and `HEAD` requests are cached. `POST`, `PUT`, `PATCH`, `DELETE`, and other methods always invoke your Worker. `GET` and `HEAD` for the same URL share a single cache entry. A `HEAD` request that arrives on a cold cache is converted to a `GET` internally so the cache is populated with the full asset. Refer to [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/#what-goes-into-the-cache-key). If you need to cache responses to non-idempotent requests, do so explicitly in your Worker — for example, by hashing the request body into a synthetic URL and making an internal `GET` subrequest. ### WebSocket upgrades WebSocket upgrade requests (`GET` with `Upgrade: websocket`) bypass the cache and always invoke your Worker. WebSocket sessions are stateful by definition and are not a meaningful unit of caching. ### Custom RPC methods Only `fetch()` invocations on a [WorkerEntrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) go through Workers Caching. Custom RPC methods like `ctx.exports.Backend.getUser(id)` bypass the cache and always run the callee, regardless of the entrypoint's `cache.enabled` setting. To cache a piece of work that is currently exposed as an RPC method, refactor it to a `fetch` handler on its own entrypoint and call it with `fetch()`. ### Status codes that are never cached Workers Caching never stores the following responses, even with explicit `Cache-Control` directives: * **`520`–`526`** (Cloudflare failsafe responses) are treated as transient errors and always re-run the Worker. * **`206 Partial Content`** returned by your Worker is not stored — Workers Caching expects your Worker to return the full `200` response and does the range slicing itself. Refer to [Range requests](https://developers.cloudflare.com/workers/cache/configuration/#range-requests) for the supported pattern. ### Other invocation types Workers Caching only applies to HTTP requests handled by a `fetch` handler on a [Worker entrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints). The following invocation types always run without cache involvement: * [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) — scheduled invocations via the `scheduled` handler. * [Queue consumers](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer) — messages delivered via the `queue` handler. * [Workflows](https://developers.cloudflare.com/workflows/) — workflow step execution. * [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) — trace event handlers. * [Durable Objects](https://developers.cloudflare.com/durable-objects/) — Durable Object invocations are never cached, regardless of handler or method. To cache a Durable Object's HTTP responses, wrap it behind a Worker entrypoint with caching enabled. See [Cache Durable Object responses](https://developers.cloudflare.com/workers/cache/#cache-durable-object-responses). ### Purge by host There is no "purge by host" mode. The cache [belongs to the Worker, not to a domain](https://developers.cloudflare.com/workers/cache/cache-keys/#the-cache-belongs-to-the-worker-not-to-a-domain) — the host is not part of the cache key, so purging by host would not map onto anything the cache stores. Use [purge by tag](https://developers.cloudflare.com/workers/cache/purge/#purge-by-tag), [purge by path prefix](https://developers.cloudflare.com/workers/cache/purge/#purge-by-path-prefix), or [purgeEverything](https://developers.cloudflare.com/workers/cache/purge/#purge-everything) instead. ### Cache pre-warming There is no API to pre-populate the cache with responses generated at build time. A response is only cached once it has been served at least once. If you need pre-rendered content available to the first requester, use [Static Assets](https://developers.cloudflare.com/workers/static-assets/). ## Limits ### Response size Response size limits are the same as Cloudflare's zone cache. For per-plan limits, refer to [Cacheable size limits](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cacheable-size-limits). Caution At launch, all Workers Caching responses are subject to the Free plan size limit regardless of your account's plan. This restriction is temporary and will be lifted in a future update, after which the limits in [Cacheable size limits](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cacheable-size-limits) will apply based on your account. ### `Cache-Tag` limits Limits on the number, length, and character set of `Cache-Tag` values are the same as Cloudflare's zone cache. Refer to [Cache tag limits](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#a-few-things-to-remember) for the full list. ### Purge rate limits `ctx.cache.purge()` uses the same rate-limiting system as the zone purge API. Refer to [Availability and limits](https://developers.cloudflare.com/cache/how-to/purge-cache/#availability-and-limits) for the rates that apply to your account. ## Relationship to other caches ### Zone-level cache configuration Workers Caching is **your Worker's cache**, not your zone's cache. It uses your Worker itself as the configuration surface, so there is no separate layer of rules or settings to configure alongside it. None of the following applies to Workers Caching: | Zone-level feature | Equivalent in Workers Caching | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Cache Rules](https://developers.cloudflare.com/cache/how-to/cache-rules/) and [Cache Response Rules](https://developers.cloudflare.com/cache/how-to/cache-response-rules/) | Set Cache-Control headers in your Worker, or branch on the request and return different headers per path. | | [Cache key customization in Cache Rules](https://developers.cloudflare.com/cache/how-to/cache-rules/settings/#cache-key) | Workers Caching has its own key composition; see [Cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/). Shape the key by shaping the request (for example, by rewriting the URL or setting ctx.props in a gateway Worker). | | Zone-level cache level settings (bypass / standard / aggressive / ignore query string) | Cache-Control headers on the response express the same intent at a per-request level. | | The zone's default cached-file-extensions list | Workers Caching caches any response whose headers say it is cacheable, regardless of file extension. | | Custom tiered cache topologies | Workers Caching uses a generic tiered cache topology by default. Because a Worker can execute anywhere, a fixed custom topology does not apply — future integrations with [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) may tailor tiering further. | | [Rulesets](https://developers.cloudflare.com/ruleset-engine/) that modify request or response before cache | Transform the request or response in your Worker's code before returning it. | To influence your Worker's cache, change your Worker. `Cache-Control` headers, `ctx.props`, service binding composition, and [ctx.cache.purge()](https://developers.cloudflare.com/workers/cache/purge/) cover the configuration surface. ### Cache API (`caches.default`) The [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) is a separate programmatic cache store. It is independent of Workers Caching — operations on one do not affect the other, and `ctx.cache.purge()` is what invalidates Workers-Caching entries. For new Workers, prefer Workers Caching. The Cache API, by design, is a lower-level primitive: * It does not read through — responses are only cached when your Worker explicitly calls `put()`, and every request still executes your Worker on the way in. * It does not [collapse concurrent requests](https://developers.cloudflare.com/workers/cache/#request-collapsing) for the same resource. A burst of traffic to a fresh URL invokes your Worker once per request. * It does not participate in [tiered caching](https://developers.cloudflare.com/cache/how-to/tiered-cache/). Workers Caching provides all three automatically. The Cache API remains useful when you need fine-grained programmatic control. ### `fetch()` subrequest caching Workers Caching is a server-side cache **in front of** your Worker. It is a separate cache from the one that sits in front of outgoing [fetch()](https://developers.cloudflare.com/workers/runtime-apis/fetch/) subrequests your Worker makes to its own origins. The two operate independently: a `fetch()` subrequest hit saves a trip to your origin, while a Workers Caching hit saves your Worker from running at all. The `cf` properties on a `Request` behave differently across the two: | cf property | On outgoing fetch() to your origin | On ctx.exports..fetch() | | ------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | cf.cacheKey | Supported | Supported — see [Custom cache keys](https://developers.cloudflare.com/workers/cache/cache-keys/#custom-cache-keys) | | cf.cacheControl | Supported | Supported — see [Override Cache-Control from the calling Worker](https://developers.cloudflare.com/workers/cache/configuration/#override-cache-control-from-the-calling-worker) | | cf.cacheTtl | Supported | Not supported — set the TTL by returning Cache-Control: max-age=N (or s-maxage=N) from the callee, or by overriding it with cf.cacheControl from the caller | | cf.cacheEverything | Supported | Not supported — Workers Caching decides cacheability from the response's Cache-Control; there is no override to force-cache an otherwise uncacheable response | ## Coming soon The following surfaces are in development: * **Dashboard UI** for enabling caching without Wrangler. * **Cache Analytics in Workers Observability**. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/limitations/#page","headline":"Limitations · Cloudflare Workers docs","description":"Current limitations, unsupported scenarios, and how Workers Caching relates to other Cloudflare caches.","url":"https://developers.cloudflare.com/workers/cache/limitations/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Invalidate cached responses using ctx.cache.purge() — purge by tag, by path prefix, or purge everything. title: Purging the cache image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Purging the cache Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/cache/purge/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Your Worker can invalidate its own cached responses at any time using the purge API. Purging is useful when data changes and the new value is more important than the performance benefit of continuing to serve the cached response — for example, after a content update, a user action, or a webhook from an upstream system. Because Workers Caching is **your Worker's cache**, purging is scoped to the Worker that owns the cache. Within a Worker, purges are further scoped to the [entrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) that called `purge()`. A Worker cannot reach into another Worker's cache, an entrypoint cannot reach into another entrypoint's cache, and no zone-level purge (via the dashboard, [API](https://developers.cloudflare.com/cache/how-to/purge-cache/), or Terraform) affects Workers Caching content. ## Two ways to call purge There are two equivalent ways to trigger a purge from inside your Worker: * **`ctx.cache.purge(...)`** — available on the execution context passed to every handler. Use this when you already have `ctx` in scope. * **`cache.purge(...)`** — imported from `cloudflare:workers`. Use this when you want to call purge from code that does not receive `ctx` — for example, a utility module shared across multiple handlers, or a framework adapter that does not thread the execution context through its internals. Both forms call into the same API and behave identically. Pick whichever reads more cleanly for your code. ```js import { cache } from "cloudflare:workers"; export default { async fetch(request, env, ctx) { // Using the module import — no need to thread ctx through helper functions. await cache.purge({ tags: ["blog-posts"] }); // Equivalent, using ctx directly: // await ctx.cache.purge({ tags: ["blog-posts"] }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts import { cache } from "cloudflare:workers"; export default { async fetch(request, env, ctx): Promise { // Using the module import — no need to thread ctx through helper functions. await cache.purge({ tags: ["blog-posts"] }); // Equivalent, using ctx directly: // await ctx.cache.purge({ tags: ["blog-posts"] }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` The rest of this page uses `ctx.cache.purge(...)` in most examples because those examples already have `ctx` in scope. If you prefer the import form, substitute `cache.purge(...)` — nothing else changes. ## Purge modes `purge()` accepts either `purgeEverything: true` on its own, or one or both of `tags` and `pathPrefixes`: | Field | Purges | Scope | | --------------- | ------------------------------------------------------------------------------- | -------------- | | tags | Every cached response tagged with one of the given values via Cache-Tag. | Per entrypoint | | pathPrefixes | Every cached response whose request path starts with one of the given prefixes. | Per entrypoint | | purgeEverything | Every cached response for the entrypoint that called purge(). | Per entrypoint | `purgeEverything` is exclusive — combine `tags` and `pathPrefixes` in a single call if you want, but do not pass either alongside `purgeEverything`. All three modes are scoped to the [entrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#named-entrypoints) that called `purge()`. A purge from `PublicAPI` does not affect cached responses stored by `AdminAPI`, even if they share tag names or path prefixes. To invalidate across every entrypoint of a Worker, call `purge()` from each entrypoint. The returned promise resolves to a result object you can inspect to confirm success or handle failures — see [Return value](#return-value). Purge after a write by calling `ctx.cache.purge()` at the end of any handler that mutates data: ```js export default { async fetch(request, env, ctx) { if (request.method === "POST") { const body = await request.json(); // Mutate your data source (D1, KV, an origin, and so on), then invalidate // every cached response tagged for this post. await ctx.cache.purge({ tags: [`post-${body.postId}`, "post-list"], }); return new Response("Updated", { status: 200 }); } // Handle cacheable reads here. return new Response("Hello", { headers: { "Cache-Control": "public, max-age=3600" }, }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { if (request.method === "POST") { const body = await request.json<{ postId: string }>(); // Mutate your data source (D1, KV, an origin, and so on), then invalidate // every cached response tagged for this post. await ctx.cache.purge({ tags: [`post-${body.postId}`, "post-list"], }); return new Response("Updated", { status: 200 }); } // Handle cacheable reads here. return new Response("Hello", { headers: { "Cache-Control": "public, max-age=3600" }, }); }, } satisfies ExportedHandler; ``` You can combine fields in a single call. For example, `purge({ tags: ["blog-posts"], pathPrefixes: ["/blog/"] })` purges everything that matches **either** tag or path-prefix — the fields are unioned, not intersected. Use this when one logical invalidation affects responses tagged by multiple schemes. ```js export default { async fetch(request, env, ctx) { // Combined call: invalidates everything tagged "blog-posts" AND // everything under /blog/ in a single round-trip. await ctx.cache.purge({ tags: ["blog-posts"], pathPrefixes: ["/blog/"], }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { // Combined call: invalidates everything tagged "blog-posts" AND // everything under /blog/ in a single round-trip. await ctx.cache.purge({ tags: ["blog-posts"], pathPrefixes: ["/blog/"], }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` ## Purge by tag Tags are attached to responses via the `Cache-Tag` response header, and purged later by name. This is the most flexible and commonly used purge method. ### Attach tags on write ```js export default { async fetch(request) { const url = new URL(request.url); const postId = url.pathname.split("/").pop() ?? "unknown"; const body = { id: postId, title: `Post ${postId}` }; return new Response(JSON.stringify(body), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600", "Cache-Tag": `post,post-${postId},blog`, }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const url = new URL(request.url); const postId = url.pathname.split("/").pop() ?? "unknown"; const body = { id: postId, title: `Post ${postId}` }; return new Response(JSON.stringify(body), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600", "Cache-Tag": `post,post-${postId},blog`, }, }); }, } satisfies ExportedHandler; ``` The `Cache-Tag` header value is a comma-separated list of tags. Cloudflare strips this header before returning the response to clients. Tag values must be **printable ASCII** (no spaces, no Unicode), each tag is at most **1024 characters** long, and a response can carry up to **1000 tags**. Tag matching at purge time is **case-insensitive**, so `Foo` and `foo` purge the same set of responses. Invalid tags are silently dropped at storage time — the response is still cached with the remaining valid tags. Refer to [Cache tag limits](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#a-few-things-to-remember) for the full list. ### Trigger the purge ```js export default { async fetch(request, env, ctx) { const postId = new URL(request.url).searchParams.get("id"); if (!postId) return new Response("Missing id", { status: 400 }); await ctx.cache.purge({ tags: [`post-${postId}`] }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { const postId = new URL(request.url).searchParams.get("id"); if (!postId) return new Response("Missing id", { status: 400 }); await ctx.cache.purge({ tags: [`post-${postId}`] }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` ### Tag scope across entrypoints Tags are scoped to the entrypoint that called `purge()`. A tag named `user-42` applied to responses in two different entrypoints is **not** invalidated by a single `purge({ tags: ["user-42"] })` call — it only affects the entrypoint the call originated from. If you need to invalidate the same tag across several entrypoints, call `purge()` from each entrypoint, or centralize purge calls in a shared entrypoint that caches every response you later need to invalidate. ### Use hierarchical tags To invalidate groups of related responses in one call, tag each response with multiple tags representing every level of hierarchy it belongs to — sometimes called "soft tags": ```js export default { async fetch(request) { const path = new URL(request.url).pathname; // Build a list of hierarchical tags for the current path. // A response at /blog/2025/02/hello gets tags for: // _path:/blog/, _path:/blog/2025/, _path:/blog/2025/02/, _path:/blog/2025/02/hello const segments = path.split("/").filter(Boolean); const tags = segments.map( (_, i) => `_path:/${segments.slice(0, i + 1).join("/")}/`, ); const body = `${path}`; return new Response(body, { headers: { "Content-Type": "text/html", "Cache-Control": "public, max-age=3600", "Cache-Tag": tags.join(","), }, }); }, }; ``` ```ts export default { async fetch(request): Promise { const path = new URL(request.url).pathname; // Build a list of hierarchical tags for the current path. // A response at /blog/2025/02/hello gets tags for: // _path:/blog/, _path:/blog/2025/, _path:/blog/2025/02/, _path:/blog/2025/02/hello const segments = path.split("/").filter(Boolean); const tags = segments.map( (_, i) => `_path:/${segments.slice(0, i + 1).join("/")}/`, ); const body = `${path}`; return new Response(body, { headers: { "Content-Type": "text/html", "Cache-Control": "public, max-age=3600", "Cache-Tag": tags.join(","), }, }); }, } satisfies ExportedHandler; ``` Purging the tag `_path:/blog/2025/` then invalidates every cached response whose URL starts with `/blog/2025/`. For limits on the number, length, and character set of tags, refer to [Cache tag limits](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#a-few-things-to-remember). ### Version-specific purging By default, Workers Caching [partitions the cache by Worker version](https://developers.cloudflare.com/workers/cache/cache-keys/#invalidating-cache-across-deployments), so each deployment already starts from a cold cache and version-specific purging is unnecessary. This section applies only when you have enabled [cache.cross\_version\_cache](https://developers.cloudflare.com/workers/cache/configuration/#cross-version-caching) to share cached responses across versions. In that case, a response written by version A may still be served after version B is deployed, and you may want to purge the entries a specific version wrote — for example, after a rollback. To do that, tag each response with the version that produced it and purge that tag later. Add the [version metadata binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) to your Wrangler configuration: ```jsonc { "name": "my-worker", "main": "src/index.ts", // Set this to today's date "compatibility_date": "2026-08-14", "cache": { "enabled": true, "cross_version_cache": true }, "version_metadata": { "binding": "CF_VERSION_METADATA" }, } ``` ```toml name = "my-worker" main = "src/index.ts" # Set this to today's date compatibility_date = "2026-08-14" [cache] enabled = true cross_version_cache = true [version_metadata] binding = "CF_VERSION_METADATA" ``` Then prepend the version ID to your tags: ```js export default { async fetch(request, env, ctx) { const { id: versionId } = env.CF_VERSION_METADATA; const postId = new URL(request.url).pathname.split("/").pop() ?? "unknown"; return new Response(JSON.stringify({ id: postId }), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600", // Include the version ID as a tag so you can purge by version later. "Cache-Tag": `post,post-${postId},v:${versionId}`, }, }); }, }; ``` ```ts interface Env { CF_VERSION_METADATA: WorkerVersionMetadata; } export default { async fetch(request, env, ctx): Promise { const { id: versionId } = env.CF_VERSION_METADATA; const postId = new URL(request.url).pathname.split("/").pop() ?? "unknown"; return new Response(JSON.stringify({ id: postId }), { headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=3600", // Include the version ID as a tag so you can purge by version later. "Cache-Tag": `post,post-${postId},v:${versionId}`, }, }); }, } satisfies ExportedHandler; ``` When you want to invalidate everything a specific version wrote — for example, after a rollback — purge the version tag: ```js export default { async fetch(request, env, ctx) { const versionId = new URL(request.url).searchParams.get("version"); if (!versionId) return new Response("Missing version", { status: 400 }); await ctx.cache.purge({ tags: [`v:${versionId}`] }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { const versionId = new URL(request.url).searchParams.get("version"); if (!versionId) return new Response("Missing version", { status: 400 }); await ctx.cache.purge({ tags: [`v:${versionId}`] }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` ## Purge by path prefix `pathPrefixes` invalidates every cached response whose **request path** begins with one of the given prefixes: ```js export default { async fetch(request, env, ctx) { // Invalidate everything under /blog/2025/ for the current entrypoint. await ctx.cache.purge({ pathPrefixes: ["/blog/2025/"], }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { // Invalidate everything under /blog/2025/ for the current entrypoint. await ctx.cache.purge({ pathPrefixes: ["/blog/2025/"], }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` Entries in `pathPrefixes` are **paths**, not full URLs. A prefix must not include a scheme, host, query string, or fragment — passing something like `https://example.com/blog/` is invalid input, not a prefix that simply fails to match. A leading slash is optional (`/images` and `images` are treated the same) but recommended for clarity. `pathPrefixes` is scoped to the entrypoint that makes the purge call. A `purge({ pathPrefixes: ["/blog/"] })` from `PublicAPI` will not affect cached responses stored by `AdminAPI`, even when their paths also start with `/blog/`. ### Purge a single URL There is no dedicated "purge by URL" mode. To invalidate a single cached URL, pass its path as a single-element `pathPrefixes` array: ```js export default { async fetch(request, env, ctx) { // Invalidate the cached response for exactly /blog/2026/hello-world. await ctx.cache.purge({ pathPrefixes: ["/blog/2026/hello-world"], }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { // Invalidate the cached response for exactly /blog/2026/hello-world. await ctx.cache.purge({ pathPrefixes: ["/blog/2026/hello-world"], }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` Because `pathPrefixes` matches on the start of the request path, passing the full path matches only that path — plus any paths that happen to extend it (for example, `/blog/2026/hello-world-2`). If you need exact-match semantics with no risk of over-purge, use a [tag](#purge-by-tag) instead. ## Purge everything Invalidate every cached response stored by the calling entrypoint: ```js export default { async fetch(request, env, ctx) { await ctx.cache.purge({ purgeEverything: true }); return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { await ctx.cache.purge({ purgeEverything: true }); return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` Use this sparingly. Purging everything causes all subsequent requests to miss the cache until they can be re-filled, which temporarily increases load on your Worker and any upstream services it calls. ## Purge propagation Purges triggered by `ctx.cache.purge()` use Cloudflare's [Instant Purge](https://developers.cloudflare.com/cache/how-to/purge-cache/) infrastructure and propagate globally with the same guarantees as zone-level purges. ## Return value `purge()` resolves to a result object. Check `success` to confirm the purge was accepted, and inspect `errors` if it was not: ```js export default { async fetch(request, env, ctx) { const result = await ctx.cache.purge({ tags: ["blog-posts"] }); if (!result.success) { console.error("Cache purge failed", result.errors); return new Response("Purge failed", { status: 500 }); } return new Response("Purged", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env, ctx): Promise { const result = await ctx.cache.purge({ tags: ["blog-posts"] }); if (!result.success) { console.error("Cache purge failed", result.errors); return new Response("Purge failed", { status: 500 }); } return new Response("Purged", { status: 200 }); }, } satisfies ExportedHandler; ``` On failure, each error in `errors` carries a numeric `code` and a human-readable `message` you can log or surface to the caller. ## Rate limits `purge()` uses the same rate-limiting system as Cloudflare's zone purge API. For the rate limits that apply to your account's plan, refer to [Availability and limits](https://developers.cloudflare.com/cache/how-to/purge-cache/#availability-and-limits). When the purge is rate-limited, `success` is `false` and `errors` contains an entry describing the rejection. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/cache/purge/#page","headline":"Purging the cache · Cloudflare Workers docs","description":"Invalidate cached responses using ctx.cache.purge() — purge by tag, by path prefix, or purge everything.","url":"https://developers.cloudflare.com/workers/cache/purge/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Set up continuous integration and continuous deployment for your Workers. title: CI/CD image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # CI/CD Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can set up continuous integration and continuous deployment (CI/CD) for your Workers by using either the integrated build system, [Workers Builds](#workers-builds), or using [external providers](#external-cicd) to optimize your development workflow. ## Why use CI/CD? Using a CI/CD pipeline to deploy your Workers is a best practice because it: * Automates the build and deployment process, removing the need for manual `wrangler deploy` commands. * Ensures consistent builds and deployments across your team by using the same source control management (SCM) system. * Reduces variability and errors by deploying in a uniform environment. * Simplifies managing access to production credentials. ## Which CI/CD should I use? Choose [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds) if you want a fully integrated solution within Cloudflare's ecosystem that requires minimal setup and configuration for GitHub or GitLab users. We recommend using [external CI/CD providers](https://developers.cloudflare.com/workers/ci-cd/external-cicd) if: * You have a self-hosted instance of GitHub or GitLabs, which is currently not supported in Workers Builds' [Git integration](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/) * You are using a Git provider that is not GitHub or GitLab ## Workers Builds [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds) is Cloudflare's native CI/CD system that allows you to integrate with GitHub or GitLab to automatically deploy changes with each new push to a selected branch (e.g. `main`). ![Workers Builds Workflow Diagram](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=923,height=203,format=webp/_astro/workers-builds-workflow.Bmy3qIVc.png) Ready to streamline your Workers deployments? Get started with [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/#get-started). ## External CI/CD You can also choose to set up your CI/CD pipeline with an external provider. * [GitHub Actions](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/) * [GitLab CI/CD](https://developers.cloudflare.com/workers/ci-cd/external-cicd/gitlab-cicd/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/#page","headline":"CI/CD · Cloudflare Workers docs","description":"Set up continuous integration and continuous deployment for your Workers.","url":"https://developers.cloudflare.com/workers/ci-cd/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use Workers Builds to integrate with Git and automatically build and deploy your Worker when pushing a change title: Builds image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Builds Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The Cloudflare [Git integration](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/) lets you connect a new or existing Worker to a GitHub or GitLab repository, enabling automated builds and deployments for your Worker on push. ## Get started ### Connect a new Worker To create a new Worker and connect it to a GitHub or GitLab repository: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select **Create application**. 3. Select **Get started** next to **Import a repository**. 4. Under **Import a repository**, select a **Git account**. 5. Select the repository you want to import from the list. You can also use the search bar to narrow the results. 6. Configure your project and select **Save and Deploy**. 7. Preview your Worker at its provided [workers.dev](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/) subdomain. ### Connect an existing Worker To connect an existing Worker to a GitHub or GitLab repository: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select the Worker you want to connect to a repository. 3. Select **Settings** and then **Builds**. 4. Select **Connect** and follow the prompts to connect the repository to your Worker and configure your [build settings](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/). 5. Push a commit to your Git repository to trigger a build and deploy to your Worker. Caution When connecting a repository to a Workers project, the Worker name in the Cloudflare dashboard must match the `name` in the Wrangler configuration file in the specified root directory, or the build will fail. This ensures that the Worker deployed from the repository is consistent with the Worker registered in the Cloudflare dashboard. For details, see [Workers name requirement](https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/#workers-name-requirement). ## Automatic project configuration When you connect a repository that does not have a Wrangler configuration file, [autoconfig](https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/) runs to detect your framework and create a [pull request](https://developers.cloudflare.com/workers/ci-cd/builds/automatic-prs/) to configure your project for Cloudflare Workers. 1. Autoconfig detects your framework and generates the necessary configuration 2. A pull request is created in your repository with the necessary configuration changes 3. A preview deployment is generated so you can test before merging 4. Once you merge the PR, your project is ready for deployment For details about supported frameworks and what files are created, refer to [Deploy an existing project](https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/). For details about the PRs created, refer to [Automatic pull requests](https://developers.cloudflare.com/workers/ci-cd/builds/automatic-prs/). ## View build and preview URL You can monitor a build's status and its build logs by navigating to **View build history** at the bottom of the **Deployments** tab of your Worker. If the build is successful, you can view the build details by selecting **View build** in the associated new [version](https://developers.cloudflare.com/workers/versions-and-deployments/) created under Version History. There you will also find the [preview URL](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) generated by the version under Version ID. Builds, versions, deployments If a build succeeds, it is uploaded as a version. If the build is configured to deploy (for example, with `wrangler deploy` set as the deploy command), the uploaded version will be automatically promoted to the Active Deployment. ## Workers with Containers For Workers that use [Containers](https://developers.cloudflare.com/containers/), use `wrangler deploy` on the production branch so container images and container instances can update. Preview builds still default to `wrangler versions upload`, which does not update container images. Preview URLs are not generated for these Workers because each container is managed from a Durable Object. Refer to [Deploy Containers](https://developers.cloudflare.com/containers/deploy/#before-production). ## Disconnecting builds To disconnect a Worker from a GitHub or GitLab repository: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select the Worker you want to disconnect from a repository. 3. Select **Settings** and then **Builds**. 4. Select **Disconnect**. If you want to switch to a different repository for your Worker, you must first disable builds, then reconnect to select the new repository. To disable automatic deployments while still allowing builds to run automatically and save as [versions](https://developers.cloudflare.com/workers/versions-and-deployments/) (without promoting them to an active deployment), update your deploy command to: `npx wrangler versions upload`. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/#page","headline":"Builds · Cloudflare Workers docs","description":"Use Workers Builds to integrate with Git and automatically build and deploy your Worker when pushing a change","url":"https://developers.cloudflare.com/workers/ci-cd/builds/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to use Workers Builds with more advanced setups title: Advanced setups image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Advanced setups Last updated May 29, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Monorepos A monorepo is a single repository that contains multiple applications. This setup can be useful for a few reasons: * **Simplified dependency management**: Manage dependencies across all your workers and shared packages from a single place using tools like [pnpm workspaces ↗](https://pnpm.io/workspaces) and [syncpack ↗](https://syncpack.dev/). * **Code sharing and reuse**: Easily create and share common logic, types, and utilities between workers by creating shared packages. * **Atomic commits**: Changes affecting multiple workers or shared libraries can be committed together, making the history easier to understand and reducing the risk of inconsistencies. * **Consistent tooling**: Apply the same build, test, linting, and formatting configurations (e.g., via [Turborepo ↗](https://turborepo.com) for task orchestration and shared configs in `packages/`) across all projects, ensuring consistent tooling and code quality across Workers. * **Easier refactoring**: Refactoring code that spans multiple Workers or shared packages is significantly easier within a single repository. #### Example Workers monorepos: * [cloudflare/mcp-server-cloudflare ↗](https://github.com/cloudflare/mcp-server-cloudflare) * [jahands/workers-monorepo-template ↗](https://github.com/jahands/workers-monorepo-template) * [cloudflare/templates ↗](https://github.com/cloudflare/templates) * [cloudflare/workers-sdk ↗](https://github.com/cloudflare/workers-sdk) ### Getting Started To set up a monorepo workflow: 1. Find the Workers associated with your project in the [Workers & Pages Dashboard ↗](https://dash.cloudflare.com). 2. Connect your monorepo to each Worker in the repository. 3. Set the root directory for each Worker to specify the location of its `wrangler.jsonc` and where build and deploy commands should run. 4. Optionally, configure unique build and deploy commands for each Worker. 5. Optionally, configure [build watch paths](https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/) for each Worker to monitor specific paths for changes. When a new commit is made to the monorepo, a new build and deploy will trigger for each Worker if the change is within each of its included watch paths. You can also check on the status of each build associated with your repository within GitHub with [check runs](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#check-run) or within GitLab with [commit statuses](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/#commit-status). ### Example In the example `ecommerce-monorepo`, a Workers project should be created for `product-service`, `order-service`, and `notification-service`. A Git connection to `ecommerce-monorepo` should be added in all of the Workers projects. If you are using a monorepo tool, such as [Turborepo ↗](https://turbo.build/), you can configure a different deploy command for each Worker, for example, `turbo deploy -F product-service`. Set the root directory of each Worker to where its Wrangler configuration file is located. For example, for `product-service`, the root directory should be `/workers/product-service/`. Optionally, you can add [build watch paths](https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/) to optimize your builds. When a new commit is made to `ecommerce-monorepo`, a build and deploy will be triggered for each of the Workers if the change is within its included watch paths using the configured commands for that Worker. * ecommerce-monorepo/ * workers/ * product-service/ * src/ * wrangler.jsonc * order-service/ * src/ * wrangler.jsonc * notification-service/ * src/ * wrangler.jsonc * packages/ * schema/ * README.md ## Wrangler Environments You can use [Wrangler Environments](https://developers.cloudflare.com/workers/wrangler/environments/) with Workers Builds by completing the following steps: 1. [Deploy via Wrangler](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) to create the Workers for your environments on the Dashboard, if you do not already have them. 2. Find the Workers for your environments. They are typically named `[name of Worker] - [environment name]`. 3. Connect your repository to each of the Workers for your environment. 4. In each of the Workers, edit your Wrangler commands to include the flag `--env: ` in the build configurations for both the deploy command, and the non-production branch deploy command ([if applicable](https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/#configure-non-production-branch-builds)). When a new commit is detected in the repository, a new build/deploy will trigger for each associated Worker. ### Example Imagine you have a Worker named `my-worker`, and you want to set up two environments `staging` and `production` set in the `wrangler.jsonc`. If you have not already, you can deploy `my-worker` for each environment using the commands `wrangler deploy --env staging` and `wrangler deploy --env production`. In your Cloudflare Dashboard, you should find the two Workers `my-worker-staging` and `my-worker-production`. Then, connect the Git repository for the Worker, `my-worker`, to both of the environment Workers. In the build configurations of each environment Worker, edit the deploy commands to be `npx wrangler deploy --env staging` and `npx wrangler deploy --env production` and the non-production branch deploy commands to be `npx wrangler versions upload --env staging` and `npx wrangler versions upload --env production` respectively. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#page","headline":"Advanced setups · Cloudflare Workers docs","description":"Learn how to use Workers Builds with more advanced setups","url":"https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-29","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to programmatically trigger builds, manage triggers, and monitor your Workers Builds using the API. title: Builds API reference image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Builds API reference Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/api-reference/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This guide shows you how to use the [Workers Builds REST API](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/) to programmatically trigger builds, manage triggers, and monitor build status. The examples use `curl` commands that you can run directly in your terminal or adapt to your preferred programming language. Some examples pipe output through [jq ↗](https://jqlang.org/) to filter JSON responses — install it if you do not have it already. ## Before you start ### 1\. Create an API token with the correct permissions To use the Builds API, you need an API token to authenticate your requests. The Builds API requires a **user-scoped** API token. Account-scoped tokens are not supported and will return "Invalid token" errors. Create your token at [dash.cloudflare.com/profile/api-tokens ↗](https://dash.cloudflare.com/profile/api-tokens) with the following permissions: | Permission | Access level | Why you need it | | ---------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Workers Builds Configuration | Edit | Trigger builds, manage triggers, configure environment variables | | Workers Scripts | Read | Only needed for [one endpoint](#step-1-get-your-worker-tag) to retrieve your Worker's tag (documented as [external\_script\_id](#2-worker-tags-documented-as-external%5Fscript%5Fid)) | Note This API token is different from a **build token**. Build tokens are used by the build system to deploy your Worker — you will need a build token UUID when [creating triggers](#step-4-get-your-build-token-uuid). The API token described above is what you use to call the Builds API itself. ### 2\. Worker tags (documented as external\_script\_id) The Builds API identifies Workers by their **tag**, an immutable UUID assigned by Cloudflare. In API responses and parameters, this value appears as `external_script_id`. | Identifier | Example | Where it comes from | | --------------------------------- | -------------------------------- | ------------------------------------- | | Worker name (id) | my-worker | The name you gave your Worker | | Worker tag (external\_script\_id) | 1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d | Immutable UUID assigned by Cloudflare | Every Builds API endpoint that references a Worker requires the **tag**, not the name. ### 3\. What is a trigger? A **trigger** is a configuration that defines how your Worker gets built and deployed. It specifies the build command, deploy command, environment variables, and which branches should trigger builds. Each Worker has up to **two triggers**: one for production (runs on your [production branch](https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/#change-production-branch)) and one for preview (runs on all other branches). To set up triggers, refer to [Set up Workers Builds from scratch](#set-up-workers-builds-from-scratch). **Trigger fields:** | Field | Type | Description | | ----------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | trigger\_name | string | Display name for the trigger | | build\_token\_uuid | string | UUID of the build token used to deploy your Worker. Find this in your Worker's **Settings** \> **Builds** \> **API token** section, or via the [GET /builds/tokens](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/build%5Ftokens/methods/list/) endpoint. | | build\_command | string | Command to build your project (for example, npm run build) | | deploy\_command | string | Command to deploy your Worker (for example, npx wrangler deploy) | | root\_directory | string | Path to your project root | | branch\_includes | array | Branch patterns that trigger builds (for example, \["main"\] or \["\*"\]) | | branch\_excludes | array | Branch patterns to exclude | | path\_includes | array | File path patterns that trigger builds | | path\_excludes | array | File path patterns to ignore | | build\_caching\_enabled | boolean | Enable or disable build caching | | environment\_variables | object | Build-time variables specific to this trigger | ## Workflow overview Most Builds API operations follow this pattern: first get your Worker's tag, then get the trigger UUID, then perform build operations. ![Workflow overview: get Worker tag, then get trigger UUID, then perform build operations.](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=760,height=100,format=svg/_astro/workflow-overview.DL8TB_t1.svg) | Step | Action | Endpoint | | ---- | ---------------- | ------------------------------------------- | | 1 | Get Worker tag | GET /workers/scripts | | 2 | Get trigger UUID | GET /builds/workers/:worker\_tag/triggers | | 3a | Trigger a build | POST /builds/triggers/:trigger\_uuid/builds | | 3b | List builds | GET /builds/workers/:worker\_tag/builds | | 3c | Get build logs | GET /builds/builds/:build\_uuid/logs | | 3d | Cancel a build | PUT /builds/builds/:build\_uuid/cancel | ## Step 1: Get your Worker tag Call the [Workers Scripts API](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/list/) to list all your Workers and find the `tag` for the Worker you want to work with: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts" \ --header "Authorization: Bearer " \ | jq '.result[] | {name: .id, tag: .tag}' ``` Example output: ```json { "name": "my-worker", "tag": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d" } { "name": "another-worker", "tag": "8a1b2c3d4e5f67890abcdef123456789" } ``` Save the `tag` value for your Worker. You will use it in all subsequent API calls. ## Step 2: Get your trigger UUID Use the [GET /builds/workers/{tag}/triggers](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/triggers/methods/list/) endpoint to list triggers for your Worker: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/workers/{worker_tag}/triggers" \ --header "Authorization: Bearer " \ | jq '.result[] | {trigger_uuid, trigger_name, branch_includes, branch_excludes}' ``` Example output: ```json { "trigger_uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "trigger_name": "Deploy production", "branch_includes": ["main"], "branch_excludes": [] } { "trigger_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "trigger_name": "Deploy non-production branches", "branch_includes": ["*"], "branch_excludes": ["main"] } ``` Save the `trigger_uuid` for the trigger you want to work with. Remember, you will have at most two triggers: one for your production branch (for example, `main`) that deploys to your live Worker, and optionally one for all other branches that creates preview deployments. ## Step 3: Work with builds Now that you have the Worker tag and trigger UUID, you can trigger builds, list build history, and get logs. ### Trigger a manual build Use the [POST /builds/triggers/{uuid}/builds](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/create/) endpoint with the `trigger_uuid` from [Step 2](#step-2-get-your-trigger-uuid). ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{trigger_uuid}/builds" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request POST \ --data '{"branch": "main"}' ``` You must specify `branch`, `commit_hash`, or both: | Field | Description | | ------------ | -------------------------------------------------------------------------------------------------- | | branch | Git branch name to build (for example, main) | | commit\_hash | Specific commit SHA to build. If provided without branch, builds the commit on its current branch. | The response includes the `build_uuid` which you can use to monitor the build. ### List builds for a Worker Use the [GET /builds/workers/{tag}/builds](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/list/) endpoint with the `worker_tag` from [Step 1](#step-1-get-your-worker-tag). ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/workers/{worker_tag}/builds" \ --header "Authorization: Bearer " \ | jq '.result[] | {build_uuid, status, branch, created_at}' ``` The response includes `build_uuid` for each build, which you need for getting logs or canceling builds. ### Get build logs Use the [GET /builds/builds/{uuid}/logs](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/get%5Flogs/) endpoint. Get the `build_uuid` from: * [List builds](#list-builds-for-a-worker) * The response when [triggering a build](#trigger-a-manual-build) * [Get latest builds by script IDs](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/get%5Flatest%5Fby%5Fscript%5Fids/) * The last segment of the URL on your build details page in the dashboard ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/builds/{build_uuid}/logs" \ --header "Authorization: Bearer " ``` ### Cancel a running build Use the [PUT /builds/builds/{uuid}/cancel](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/cancel/) endpoint. Get the `build_uuid` from: * [List builds](#list-builds-for-a-worker) * The response when [triggering a build](#trigger-a-manual-build) * [Get latest builds by script IDs](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/get%5Flatest%5Fby%5Fscript%5Fids/) * The last segment of the URL on your build details page in the dashboard ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/builds/{build_uuid}/cancel" \ --header "Authorization: Bearer " \ --request PUT ``` ## Update trigger configuration Use the [PATCH /builds/triggers/{uuid}](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/triggers/methods/update/) endpoint with the `trigger_uuid` from [Step 2](#step-2-get-your-trigger-uuid). You can update any of the trigger fields described in [What is a trigger?](#3-what-is-a-trigger). ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{trigger_uuid}" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request PATCH \ --data '{ "build_command": "npm run build:prod", "deploy_command": "npx wrangler deploy" }' ``` ## Manage build environment variables Environment variables are set per trigger, meaning you can have different values for production and preview builds. For example, you might set `NODE_ENV=production` on your production trigger and `NODE_ENV=development` on your preview trigger. Refer to the [environment variables API reference](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/triggers/subresources/environment%5Fvariables/) for full endpoint details. Note These are **build-time** environment variables, available only during the build process. For runtime environment variables, refer to [Environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). ### List environment variables Use the `trigger_uuid` from [Step 2](#step-2-get-your-trigger-uuid). ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{trigger_uuid}/environment_variables" \ --header "Authorization: Bearer " ``` ### Set environment variables You can set different variables for each trigger. For example, to set production environment variables: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{production_trigger_uuid}/environment_variables" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request PATCH \ --data '{ "NODE_ENV": {"value": "production", "is_secret": false}, "API_KEY": {"value": "prod-secret-key", "is_secret": true} }' ``` And different values for preview builds: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{preview_trigger_uuid}/environment_variables" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request PATCH \ --data '{ "NODE_ENV": {"value": "development", "is_secret": false}, "API_KEY": {"value": "dev-secret-key", "is_secret": true} }' ``` Set `is_secret` to `false` for plain values and `true` for sensitive values that should be masked in logs. ### Delete an environment variable Use the `trigger_uuid` from [Step 2](#step-2-get-your-trigger-uuid). The `variable_key` is the key name you set (for example, `NODE_ENV`). ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{trigger_uuid}/environment_variables/{variable_key}" \ --header "Authorization: Bearer " \ --request DELETE ``` ## Purge build cache Use the [POST /builds/triggers/{uuid}/purge\_build\_cache](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/triggers/methods/purge%5Fbuild%5Fcache/) endpoint with the `trigger_uuid` from [Step 2](#step-2-get-your-trigger-uuid). This clears cached dependencies and build artifacts for that trigger. ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{trigger_uuid}/purge_build_cache" \ --header "Authorization: Bearer " \ --request POST ``` ## Examples The following examples show common use cases for the Builds API. ### Set up Workers Builds from scratch This example walks through the complete process of connecting a GitHub repository to a Worker and setting up automated builds using only the API. ![Setup flow: get GitHub IDs, create repo connection, get Worker tag, create triggers, set env variables, trigger first build.](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=820,height=310,format=svg/_astro/setup-from-scratch.BV6HM_4R.svg) | Step | Action | Endpoint | | ---- | --------------------------- | ------------------------------------------------------------- | | 1 | Get GitHub account/repo IDs | GET api.github.com/users/... and GET api.github.com/repos/... | | 2 | Create repo connection | PUT /builds/repos/connections | | 3 | Get Worker tag | GET /workers/scripts | | 4 | Get build token UUID | GET /builds/tokens | | 5a | Create production trigger | POST /builds/triggers | | 5b | Create preview trigger | POST /builds/triggers | | 6 | Set environment variables | PATCH /builds/triggers/:trigger\_uuid/environment\_variables | | 7 | Trigger first build | POST /builds/triggers/:trigger\_uuid/builds | #### Prerequisites Before using the API, you must first install the Cloudflare GitHub App through the dashboard: 1. Go to **Workers & Pages** in the [Cloudflare dashboard ↗](https://dash.cloudflare.com). 2. Select any Worker and go to **Settings** \> **Builds** \> **Connect**. 3. Select **GitHub** and authorize the Cloudflare GitHub App for your account or organization. This one-time setup creates the connection between your GitHub account and Cloudflare. Once complete, you can use the API for everything else. #### Step 1: Get your GitHub account information After installing the GitHub App, you need your GitHub account ID and repository ID. You can find these from an existing trigger or from the GitHub API. From GitHub's API: ```bash # Get your GitHub user/org ID curl -s "https://api.github.com/users/" | jq '.id' # Get a repository ID curl -s "https://api.github.com/repos//" | jq '.id' ``` #### Step 2: Create a repository connection Create a connection between your GitHub repository and Cloudflare: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/repos/connections" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request PUT \ --data '{ "provider_type": "github", "provider_account_id": "", "provider_account_name": "", "repo_id": "", "repo_name": "" }' ``` Save the `repo_connection_uuid` from the response. #### Step 3: Get your Worker tag ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts" \ --header "Authorization: Bearer " \ | jq '.result[] | {name: .id, tag: .tag}' ``` #### Step 4: Get your build token UUID A build token authorizes the build system to deploy your Worker. To get your build token UUID: 1. Go to your Worker in the [Cloudflare dashboard ↗](https://dash.cloudflare.com). 2. Navigate to **Settings** \> **Builds** \> **API token**. 3. Select an existing build token or create a new one. You can also list your build tokens via the API: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/tokens" \ --header "Authorization: Bearer " \ | jq '.result[] | {build_token_uuid, build_token_name}' ``` Save the `build_token_uuid` for the next step. #### Step 5: Create a production trigger Create a trigger that deploys when you push to `main`: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request POST \ --data '{ "external_script_id": "", "repo_connection_uuid": "", "build_token_uuid": "", "trigger_name": "Deploy production", "build_command": "npm run build", "deploy_command": "npx wrangler deploy", "root_directory": "/", "branch_includes": ["main"], "branch_excludes": [], "path_includes": ["*"], "path_excludes": [] }' ``` #### Step 6: Create a preview trigger (optional) Create a second trigger for preview deployments on all other branches: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request POST \ --data '{ "external_script_id": "", "repo_connection_uuid": "", "build_token_uuid": "", "trigger_name": "Deploy preview branches", "build_command": "npm run build", "deploy_command": "npx wrangler versions upload", "root_directory": "/", "branch_includes": ["*"], "branch_excludes": ["main"], "path_includes": ["*"], "path_excludes": [] }' ``` Note the different `deploy_command`: production uses `wrangler deploy` while preview uses `wrangler versions upload` to create preview URLs without affecting the live deployment. Note Preview URLs are not generated for Durable Object Workers, including [Containers](https://developers.cloudflare.com/containers/). Refer to [Preview URL limitations](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/#limitations). #### Step 7: Set environment variables for each trigger Set production environment variables: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{production_trigger_uuid}/environment_variables" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request PATCH \ --data '{ "NODE_ENV": {"value": "production", "is_secret": false} }' ``` Set preview environment variables: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{preview_trigger_uuid}/environment_variables" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request PATCH \ --data '{ "NODE_ENV": {"value": "development", "is_secret": false} }' ``` #### Step 8: Trigger your first build ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{production_trigger_uuid}/builds" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request POST \ --data '{"branch": "main"}' ``` Your Worker is now connected to GitHub. Future pushes to `main` will automatically trigger production deployments, and pushes to other branches will create preview deployments. ### Redeploy current deployment Redeploy your current active deployment to refresh build-time data. This is useful when you need to rebuild without code changes. ![Redeploy flow: get active deployment, find the build for that version, retrigger with same branch and commit.](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=820,height=100,format=svg/_astro/redeploy-flow.CJhryiKc.svg) | Step | Action | Endpoint | | ---- | --------------------------------- | ---------------------------------------------- | | 1 | Get active deployment | GET /workers/scripts/:worker\_name/deployments | | 2 | Find the build for that version | GET /builds/builds?version\_ids=:version\_id | | 3 | Retrigger with same branch/commit | POST /builds/triggers/:trigger\_uuid/builds | **Step 1: Get the active deployment's version ID** Use the [GET /workers/scripts/{script\_name}/deployments](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/deployments/methods/list/) endpoint with the `worker_name` from [Step 1](#step-1-get-your-worker-tag): ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{worker_name}/deployments" \ --header "Authorization: Bearer " \ | jq '.result.deployments[0].versions[0].version_id' ``` Save the `version_id` from the output. **Step 2: Find the build for that version** Use the [GET /builds/builds](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/get%5Fby%5Fversion%5Fids/) endpoint with the `version_id` from the previous step: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/builds?version_ids={version_id}" \ --header "Authorization: Bearer " \ | jq '.result.builds' ``` From the response, note the `trigger.trigger_uuid`, `build_trigger_metadata.branch`, and `build_trigger_metadata.commit_hash`. **Step 3: Retrigger with the same branch and commit** Use the [POST /builds/triggers/{uuid}/builds](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/builds/methods/create/) endpoint with the values from the previous step: ```bash curl -s "https://api.cloudflare.com/client/v4/accounts/{account_id}/builds/triggers/{trigger_uuid}/builds" \ --header "Authorization: Bearer " \ --header "Content-Type: application/json" \ --request POST \ --data '{ "branch": "{branch}", "commit_hash": "{commit_hash}" }' Passing both `branch` and `commit_hash` pins the build to that exact commit on that branch. ``` ## Troubleshooting ### "Resource not found" error You are likely using the Worker name instead of the Worker tag. The Builds API requires the `tag` (a UUID like `1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d`), not the Worker name. Refer to [Step 1](#step-1-get-your-worker-tag) to get your Worker tag. For other build errors, refer to [Troubleshooting builds](https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/). ## Related resources * [Workers Builds REST API reference](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/) \- Complete endpoint documentation * [Workers Scripts REST API reference](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/) \- For retrieving Worker tags * [Workers Builds overview](https://developers.cloudflare.com/workers/ci-cd/builds/) \- Dashboard setup and configuration * [Build configuration](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/) \- Build settings and options * [Create API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) \- How to create tokens with the correct permissions Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/api-reference/#page","headline":"Builds API reference · Cloudflare Workers docs","description":"Learn how to programmatically trigger builds, manage triggers, and monitor your Workers Builds using the API.","url":"https://developers.cloudflare.com/workers/ci-cd/builds/api-reference/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn about the pull requests Workers Builds creates to configure your project or resolve issues. title: Automatic pull requests image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Automatic pull requests Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/automatic-prs/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Workers Builds can automatically create pull requests in your repository to configure your project or resolve deployment issues. ## Configuration PR When you connect a repository that does not have a Wrangler configuration file, Workers Builds runs `wrangler deploy` which triggers [automatic project configuration](https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/). Instead of failing, it creates a pull request with the necessary configuration for your detected framework. Note A configuration PR is only created when your deploy command is `npx wrangler deploy`. If you have a custom deploy command, autoconfig will still run and configure your project, but no PR will be created. ### Why you should merge the PR Without the configuration in your repository, every build has to run autoconfig first, which means your project gets built twice - once during autoconfig to generate the configuration, and again for the actual deployment. Merging the PR commits the configuration to your repository, so future builds skip autoconfig and go straight to building and deploying. This results in faster deployments and version-controlled settings. ### What the PR includes ![Example of an automatic configuration pull request created by Workers Builds](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1918,height=1348,format=webp/_astro/automatic-pr.CwJG6Bec.png) The configuration PR may contain changes to the following files, depending on your framework: * **`wrangler.jsonc`** \- Wrangler configuration file with your Worker settings * **Framework adapter** \- Any required Cloudflare adapter for your framework (for example, `@astrojs/cloudflare` for Astro) * **Framework configuration** \- Updates to framework config files (for example, `astro.config.mjs` for Astro or `svelte.config.js` for SvelteKit) * **`package.json`** \- New scripts like `deploy`, `preview`, and `cf-typegen`, plus required dependencies * **`package-lock.json`** / **`yarn.lock`** / **`pnpm-lock.yaml`** \- Updated lock file with new dependencies * **`.gitignore`** \- Entries for `.wrangler` and `.dev.vars*` files * **`.assetsignore`** \- For frameworks that generate worker files in the output directory ### PR description The PR description includes: * **Detected settings** \- Framework, build command, deploy command, and version command * **Preview link** \- A working preview generated using the detected settings * **Next steps** \- Links to documentation for adding bindings, custom domains, and more Note When you merge the PR, Workers Builds will update your build and deploy commands if they do not match the detected settings, ensuring successful deployments. ## Name conflict PR If Workers Builds detects a mismatch between your Worker name in the Cloudflare dashboard and the `name` field in your Wrangler configuration file, it will create a pull request to fix the conflict. This can happen when: * You rename your Worker in the dashboard but not in your config file * You connect a repository that was previously used with a different Worker * The `name` field in your config does not match the connected Worker The PR will update the `name` field in your Wrangler configuration to match the Worker name in the dashboard. For more details, refer to the [name conflict changelog](https://developers.cloudflare.com/changelog/2025-02-20-builds-name-conflict/). ## Reviewing PRs When you receive a PR from Workers Builds: 1. **Review the changes** \- Check that the configuration matches your project requirements 2. **Test the preview** \- Use the preview link in the PR description to verify everything works 3. **Merge when ready** \- Once satisfied, merge the PR to enable faster deployments Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/automatic-prs/#page","headline":"Automatic pull requests · Cloudflare Workers docs","description":"Learn about the pull requests Workers Builds creates to configure your project or resolve issues.","url":"https://developers.cloudflare.com/workers/ci-cd/builds/automatic-prs/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Configure which git branches should trigger a Workers Build title: Build branches image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build branches Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) When you connect a git repository to Workers, commits made on the production git branch will produce a Workers Build. If you want to take advantage of [preview URLs](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) and [pull request comments](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#pull-request-comment), you can additionally enable "non-production branch builds" in order to trigger a build on all branches of your repository. Note [Preview URLs](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) are not generated for Workers that implement a [Durable Object](https://developers.cloudflare.com/durable-objects/), including [Containers](https://developers.cloudflare.com/containers/) and [Sandbox](https://developers.cloudflare.com/sandbox/) Workers. Refer to [Deploy Containers](https://developers.cloudflare.com/containers/deploy/#before-production) if your Worker uses containers. ## Change production branch To change the production branch of your project: 1. In **Overview**, select your Workers project. 2. Go to **Settings** \> **Build** \> **Branch control**. Workers will default to the default branch of your git repository, but this can be changed in the dropdown. Every push event made to this branch will trigger a build and execute the [build command](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#deploy-command), followed by the [deploy command](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#deploy-command). ## Configure non-production branch builds To enable or disable non-production branch builds: 1. In **Overview**, select your Workers project. 2. Go to **Settings** \> **Build** \> **Branch control**. The checkbox **Builds for non-production branches** allows you to enable or disable builds for non-production branches. When enabled, every push event made to a non-production branch will trigger a build and execute the [build command](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#deploy-command), followed by the [non-production branch deploy command](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#non-production-branch-deploy-command). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/#page","headline":"Build branches · Cloudflare Workers docs","description":"Configure which git branches should trigger a Workers Build","url":"https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Improve build times by caching build outputs and dependencies title: Build caching image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build caching Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/build-caching/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Improve Workers build times by caching dependencies and build output between builds with a project-wide shared cache. The first build to occur after enabling build caching on your Workers project will save relevant artifacts to cache. Every subsequent build will restore from cache unless configured otherwise. ## About build cache When enabled, build caching will automatically detect which package manager and framework the project is using from its `package.json` and cache data accordingly for the build. The following shows which package managers and frameworks are supported for dependency and build output caching respectively. ### Package managers Workers build cache will cache the global cache directories of the following package managers: | Package Manager | Directories cached | | ------------------------------- | ------------------ | | [npm ↗](https://www.npmjs.com/) | .npm | | [yarn ↗](https://yarnpkg.com/) | .cache/yarn | | [pnpm ↗](https://pnpm.io/) | .pnpm-store | | [bun ↗](https://bun.sh/) | .bun/install/cache | ### Frameworks Some frameworks provide a cache directory that is typically populated by the framework with intermediate build outputs or dependencies during build time. Workers Builds will automatically detect the framework you are using and cache this directory for reuse in subsequent builds. The following frameworks support build output caching: | Framework | Directories cached | | ---------- | ---------------------------------------- | | Astro | node\_modules/.astro | | Docusaurus | node\_modules/.cache, .docusaurus, build | | Eleventy | .cache | | Gatsby | .cache, public | | Next.js | .next/cache | | Nuxt | node\_modules/.cache/nuxt | | SvelteKit | node\_modules/.cache/imagetools | Note [Static assets](https://developers.cloudflare.com/workers/static-assets/) and [frameworks](https://developers.cloudflare.com/workers/framework-guides/) are now supported in Cloudflare Workers. ### Limits The following limits are imposed for build caching: * **Retention**: Cache is purged 7 days after its last read date. Unread cache artifacts are purged 7 days after creation. * **Storage**: Every project is allocated 10 GB. If the project cache exceeds this limit, the project will automatically start deleting artifacts that were read least recently. ## Enable build cache To enable build caching: 1. Navigate to [Workers & Pages Overview ↗](https://dash.cloudflare.com) on the Dashboard. 2. Find your Workers project. 3. Go to **Settings** \> **Build** \> **Build cache**. 4. Select **Enable** to turn on build caching. ## Clear build cache The build cache can be cleared for a project when needed, such as when debugging build issues. To clear the build cache: 1. Navigate to [Workers & Pages Overview ↗](https://dash.cloudflare.com) on the Dashboard. 2. Find your Workers project. 3. Go to **Settings** \> **Build** \> **Build cache**. 4. Select **Clear Cache** to clear the build cache. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/build-caching/#page","headline":"Build caching · Cloudflare Workers docs","description":"Improve build times by caching build outputs and dependencies","url":"https://developers.cloudflare.com/workers/ci-cd/builds/build-caching/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Understand the build image used in Workers Builds. title: Build image image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build image Last updated Jul 30, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Workers Builds uses a build image with support for a variety of languages and tools such as Node.js, Python, PHP, Ruby, and Go. ## Supported Tooling Workers Builds supports a variety of runtimes, languages, and tools. Builds will use the default versions listed below unless a custom version is detected or specified. You can [override the default versions](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#overriding-default-versions) using environment variables or version files. All versions are available for override. Default version updates The default versions will be updated regularly to the latest minor version. No major version updates will be made without notice. If you need a specific minor version, please specify it by [overriding the default version](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#overriding-default-versions). ### Runtime | Tool | Default version | Environment variable | File | | ----------- | --------------- | -------------------- | ---------------------------- | | **Go** | 1.24.3 | GO\_VERSION | | | **Node.js** | 24.18.0 | NODE\_VERSION | .nvmrc, .node-version | | **Python** | 3.13.3 | PYTHON\_VERSION | .python-version, runtime.txt | | **Ruby** | 3.4.4 | RUBY\_VERSION | .ruby-version | The build image preinstalls Node.js 22.23.2 and 24.18.0. ### Tools and languages | Tool | Default version | Environment variable | | ----------- | ----------------- | -------------------- | | **Bun** | 1.2.15 | BUN\_VERSION | | **Hugo** | extended\_0.147.7 | HUGO\_VERSION | | **npm** | 10.9.2 | | | **yarn** | 4.9.1 | YARN\_VERSION | | **pnpm** | 10.11.1 | PNPM\_VERSION | | **pip** | 25.1.1 | | | **gem** | 3.6.9 | | | **poetry** | 2.1.3 | | | **pipx** | 1.7.1 | | | **bundler** | 2.6.9 | | ## Advanced Settings ### Overriding Default Versions If you need to override a [specific version](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#overriding-default-versions) of a language or tool within the image, you can specify it as a [build environment variable](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#build-settings), or set the relevant file in your source code as shown above. To set the version using a build environment variables, you can: 1. Find the environment variable name for the language or tool and desired version (e.g. `NODE_VERSION = 22`) 2. Add and save the environment variable on the dashboard by going to **Settings** \> **Build** \> **Build Variables and Secrets** in your Workers project Or, to set the version by adding a file to your project, you can: 1. Find the filename for the language or tool (e.g. `.nvmrc`) 2. Add the specified file name to the root directory and set the desired version number as the file's content. For example, if the version number is 22, the file should contain '22'. ### Skip dependency install You can add the following build variable to disable automatic dependency installation and run a custom install command instead. | Build variable | Value | | ------------------------- | --------- | | SKIP\_DEPENDENCY\_INSTALL | 1 or true | ## Pre-installed Packages In the following table, review the pre-installed packages in the build image. The packages are installed with `apt`, a package manager for Linux distributions. | curl | libbz2-dev | libreadline-dev | | --------------- | --------------- | --------------- | | git | libc++1 | libssl-dev | | git-lfs | libdb-dev | libvips-dev | | unzip | libgdbm-dev | libyaml-dev | | autoconf | libgdbm6 | tzdata | | build-essential | libgbm1 | wget | | bzip2 | libgmp-dev | zlib1g-dev | | gnupg | liblzma-dev | zstd | | libffi-dev | libncurses5-dev | | ## Build Environment Workers Builds are run in the following environment: | **Build Environment** | Ubuntu 24.04 | | --------------------- | ------------ | | **Architecture** | x86\_64 | ## Build Image Policy ### Preinstalled Software Updates Preinstalled software (languages and tools) will be updated before reaching end-of-life (EOL). These updates apply only if you have not [overridden the default version](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#overriding-default-versions). * **Minor version updates**: May be updated to the latest available minor version without notice. For tools that do not follow semantic versioning (e.g., Bun or Hugo), updates that may contain breaking changes will receive 3 months’ notice. * **Major version updates**: Updated to the next stable long-term support (LTS) version with 3 months’ notice. **How you'll be notified (for changes requiring notice):** * [Cloudflare Changelog ↗](https://developers.cloudflare.com/changelog/) * Dashboard notifications for projects that will receive the update * Email notifications to project owners To maintain a specific version and avoid automatic updates, [override the default version](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#overriding-default-versions). ### Best Practices To avoid unexpected build failures: * **Monitor announcements** via the [Cloudflare Changelog ↗](https://developers.cloudflare.com/changelog/), dashboard notifications, and email * **Pin specific versions** of critical preinstalled software by [overriding default versions](https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#overriding-default-versions) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/build-image/#page","headline":"Build image · Cloudflare Workers docs","description":"Understand the build image used in Workers Builds.","url":"https://developers.cloudflare.com/workers/ci-cd/builds/build-image/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-30","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Reduce compute for your monorepo by specifying paths for Workers Builds to skip title: Build watch paths image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Build watch paths Last updated May 29, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) When you connect a git repository to Workers, by default a change to any file in the repository will trigger a build. You can configure Workers to include or exclude specific paths to specify if Workers should skip a build for a given path. This can be especially helpful if you are using a monorepo project structure and want to limit the number of builds being kicked off. ## Configure Paths To configure which paths are included and excluded: 1. In **Overview**, select your Workers project. 2. Go to **Settings** \> **Build** \> **Build watch paths**. Workers will default to setting your project’s includes paths to everything (\[\*\]) and excludes paths to nothing (`[]`). The configuration fields can be filled in two ways: * **Static filepaths**: Enter the precise name of the file you are looking to include or exclude (for example, `docs/README.md`). * **Wildcard syntax:** Use wildcards to match multiple path directories. You can specify wildcards at the start or end of your rule. Wildcard syntax A wildcard (`*`) is a character that is used within rules. It can be placed alone to match anything or placed at the start or end of a rule to allow for better control over branch configuration. A wildcard will match zero or more characters. For example, if you wanted to match all branches that started with `fix/` then you would create the rule `fix/*` to match strings like `fix/1`, `fix/bugs`, or `fix/`. For each path in a push event, build watch paths will be evaluated as follows: * Paths satisfying excludes conditions are ignored first * Any remaining paths are checked against includes conditions * If any matching path is found, a build is triggered. Otherwise the build is skipped Workers will bypass the path matching for a push event and default to building the project if: * A push event contains 0 file changes, in case a user pushes an empty push event to trigger a build * A push event contains 3000+ file changes or 20+ commits ## Examples ### Example 1 If you want to trigger a build from all changes within a set of directories, such as all changes in the folders `project-a/` and `packages/` * Include paths: `project-a/*, packages/*` * Exclude paths: \`\` ### Example 2 If you want to trigger a build for any changes, but want to exclude changes to a certain directory, such as all changes in a docs/ directory * Include paths: `*` * Exclude paths: `docs/*` ### Example 3 If you want to trigger a build for a specific file or specific filetype, for example all files ending in `.md`. * Include paths: `*.md` * Exclude paths: \`\` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/#page","headline":"Build watch paths · Cloudflare Workers docs","description":"Reduce compute for your monorepo by specifying paths for Workers Builds to skip","url":"https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-29","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Understand the different settings associated with your build. title: Configuration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Configuration Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) When connecting your Git repository to your Worker, you can customize the configurations needed to build and deploy your Worker. ## How Workers Builds works When a commit is pushed to your connected repository, Workers Builds runs a two-step process: 1. **Build command** _(optional)_ \- Compiles your project (for example, `npm run build` for frameworks like Next.js or Astro) 2. **Deploy command** \- Deploys your Worker to Cloudflare (defaults to `npx wrangler deploy`) For preview builds (commits to branches other than your production branch), the deploy command is replaced with a **preview deploy command** (defaults to `npx wrangler versions upload`), which creates a preview version without promoting it to production. Workers that use Containers `wrangler deploy` can build and publish container images and roll out container instances. `wrangler versions upload` does not update container images. Preview URLs are not generated for Workers that implement Durable Objects, including Containers. Refer to [Deploy Containers](https://developers.cloudflare.com/containers/deploy/#before-production). ## Build settings Build settings can be found by navigating to **Settings** \> **Build** within your Worker. Note that when you update and save build settings, the updated settings will be applied to your _next_ build. When you _retry_ a build, the build configurations that exist when the build is retried will be applied. ### Overview | Setting | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Git account** | Select the Git account you would like to use. After the initial connection, you can continue to use this Git account for future projects. | | **Git repository** | Choose the Git repository you would like to connect your Worker to. | | **Git branch** | Select the branch you would like Cloudflare to listen to for new commits. This will be defaulted to main. | | **Build command** _(Optional)_ | Set a build command if your project requires a build step (e.g. npm run build). This is necessary, for example, when using a [front-end framework](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#framework-support) such as Next.js or Remix. | | **[Deploy command](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#deploy-command)** | The deploy command lets you set the [specific Wrangler command](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) used to deploy your Worker. Your deploy command will default to npx wrangler deploy but you may customize this command. Workers Builds will use the Wrangler version set in your package.json. | | **[Non-production branch deploy command](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#non-production-branch-deploy-command)** | Set a command to run when executing [a build for commit on a non-production branch](https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/#configure-non-production-branch-builds). This will default to npx wrangler versions upload but you may customize this command. Workers Builds will use the Wrangler version set in your package.json. | | **Root directory** _(Optional)_ | Specify the path to your project. The root directory defines where the build command will be run and can be helpful in [monorepos](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#monorepos) to isolate a specific project within the repository for builds. | | **[API token](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#api-token)** _(Optional)_ | The API token is used to authenticate your build request and authorize the upload and deployment of your Worker to Cloudflare. By default, Cloudflare will automatically generate an API token for your account when using Workers Builds, and continue to use this API token for all subsequent builds. Alternatively, you can [create your own API token](https://developers.cloudflare.com/workers/wrangler/migration/v1-to-v2/wrangler-legacy/authentication/#generate-tokens), or select one that you already own. | | **Build variables and secrets** _(Optional)_ | Add environment variables and secrets accessible only to your build. Build variables will not be accessible at runtime. If you would like to configure runtime variables you can do so in **Settings** \> **Variables & Secrets** | Note Currently, Workers Builds does not honor the configurations set in [Custom Builds](https://developers.cloudflare.com/workers/wrangler/custom-builds/) within your Wrangler configuration file. ### Deploy command You can run your deploy command using the package manager of your choice. If you have added a Wrangler deploy command as a script in your `package.json`, then you can run it by setting it as your deploy command. For example, `npm run deploy`. Examples of other deploy commands you can set include: | Example Command | Description | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | npx wrangler deploy --assets ./public/ | Deploy your Worker along with static assets from the specified directory. Alternatively, you can use the [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/). | | npx wrangler deploy --env staging | If you have a [Wrangler environment](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#wrangler-environments) Worker, you should set your deploy command with the environment flag. For more details, see [Advanced Setups](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#wrangler-environments). | | npx wrangler deploy --containers-rollout=immediate | [Containers](https://developers.cloudflare.com/containers/) rollout mode for this deploy. Refer to [Rollouts](https://developers.cloudflare.com/containers/platform-details/rollouts/). | | npx wrangler deploy --containers-rollout=none | Deploy the Worker only. Skip container image build/push and instance rollout. | ### Non-production branch deploy command The non-production branch deploy command is only applicable when you have enabled [non-production branch builds](https://developers.cloudflare.com/workers/ci-cd/builds/build-branches/#configure-non-production-branch-builds). It defaults to `npx wrangler versions upload`, producing a [preview URL](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/). Like the build and deploy commands, it can be customized to instead run anything. Note Preview URLs are not generated for Durable Object Workers, including [Containers](https://developers.cloudflare.com/containers/). For Containers, `versions upload` also does not update the container image. Refer to [Deploy Containers](https://developers.cloudflare.com/containers/deploy/#before-production). Examples of other non-production branch deploy commands you can set include: | Example Command | Description | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | yarn exec wrangler versions upload | You can customize the package manager used to run Wrangler. | | npx wrangler versions upload --env staging | If you have a [Wrangler environment](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#wrangler-environments) Worker, you should set your non-production branch deploy command with the environment flag. For more details, see [Advanced Setups](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#wrangler-environments). | ### Automatic configuration for new projects If your repository does not have a Wrangler configuration file, the deploy command (`wrangler deploy`) will trigger [automatic project configuration](https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/). This detects your framework, creates the necessary configuration, and opens a [pull request](https://developers.cloudflare.com/workers/ci-cd/builds/automatic-prs/) for you to review. Once you merge the PR, your project is configured and future builds will deploy normally. ### API token The API token in Workers Builds defines the access granted to Workers Builds for interacting with your account's resources. Currently, only user tokens are supported, with account-owned token support coming soon. When you select **Create new token**, a new API token will be created automatically with the following permissions: * **Account:** Account Settings (read), Workers Scripts (edit), Workers KV Storage (edit), Workers R2 Storage (edit) * **Zone:** Workers Routes (edit) for all zones on the account * **User:** User Details (read), Memberships (read) You can configure the permissions of this API token by navigating to **My Profile** \> **API Tokens** for user tokens. It is recommended to consistently use the same API token across all uploads and deployments of your Worker to maintain consistent access permissions. ## Framework support [Static assets](https://developers.cloudflare.com/workers/static-assets/) and [frameworks](https://developers.cloudflare.com/workers/framework-guides/) are now supported in Cloudflare Workers. Learn to set up Workers projects and the commands for each framework in the framework guides: * [Deploy an existing project](https://developers.cloudflare.com/workers/framework-guides/automatic-configuration/) * [Web applications](https://developers.cloudflare.com/workers/framework-guides/web-apps/) * [React + Vite](https://developers.cloudflare.com/workers/framework-guides/web-apps/react/) * [Astro](https://developers.cloudflare.com/workers/framework-guides/web-apps/astro/) * [React Router (formerly Remix)](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/) * [Next.js](https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/) * [Vue](https://developers.cloudflare.com/workers/framework-guides/web-apps/vue/) * [RedwoodSDK](https://developers.cloudflare.com/workers/framework-guides/web-apps/redwoodsdk/) * [TanStack Start](https://developers.cloudflare.com/workers/framework-guides/web-apps/tanstack-start/) * [Microfrontends](https://developers.cloudflare.com/workers/framework-guides/web-apps/microfrontends/) * [SvelteKit](https://developers.cloudflare.com/workers/framework-guides/web-apps/sveltekit/) * [Vike](https://developers.cloudflare.com/workers/framework-guides/web-apps/vike/) * [More guides...](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/) * [Analog](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/analog/) * [Angular](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/angular/) * [Docusaurus](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/docusaurus/) * [Gatsby](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/gatsby/) * [Hono](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/hono/) * [Nuxt](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/nuxt/) * [Qwik](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/qwik/) * [Solid](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/solid/) * [Waku](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/waku/) * [Mobile applications](https://developers.cloudflare.com/workers/framework-guides/mobile-apps/) * [Expo](https://docs.expo.dev/eas/hosting/reference/worker-runtime/) * [APIs](https://developers.cloudflare.com/workers/framework-guides/apis/) * [FastAPI](https://developers.cloudflare.com/workers/languages/python/packages/fastapi/) * [Hono](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/hono/) * [AI & agents](https://developers.cloudflare.com/workers/framework-guides/ai-and-agents/) * [Agents SDK](https://developers.cloudflare.com/agents/) * [LangChain](https://developers.cloudflare.com/workers/languages/python/packages/langchain/) ## Environment variables You can provide custom environment variables to your build. To add environment variables via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 1. In **Overview**, select your Worker. 2. Select **Settings** \> **Environment variables**. To add env variables using Wrangler, define text and JSON via the `[vars]` configuration in your Wrangler file. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker-dev", "vars": { "API_HOST": "example.com", "API_ACCOUNT_ID": "example_user", "SERVICE_X_DATA": { "URL": "service-x-api.dev.example", "MY_ID": 123 } } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker-dev" [vars] API_HOST = "example.com" API_ACCOUNT_ID = "example_user" [vars.SERVICE_X_DATA] URL = "service-x-api.dev.example" MY_ID = 123 ``` ### Default variables The following system environment variables are injected by default (but can be overridden): | Environment Variable | Injected value | Example use-case | | ------------------------ | ----------------------------- | ------------------------------------------------------------------------------------- | | CI | true | Changing build behaviour when run on CI versus locally | | WORKERS\_CI | 1 | Changing build behaviour when run on Workers Builds versus locally | | WORKERS\_CI\_BUILD\_UUID | | Passing the Build UUID along to custom workflows | | WORKERS\_CI\_COMMIT\_SHA | | Passing current commit ID to error reporting, for example, Sentry | | WORKERS\_CI\_BRANCH | Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Deploy Hooks Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) By default, Workers Builds triggers a build when you push a commit to your [connected Git repository](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/). Deploy Hooks provide another way to trigger a build. Each hook is a unique URL that triggers a manual build for one branch when it receives an HTTP POST request. Use Deploy Hooks to connect Workers Builds with workflows such as: * Rebuild automatically when content changes in a headless CMS * Build on a schedule using an external cron service * Trigger deployments from custom CI/CD pipelines based on specific conditions ## Create a Deploy Hook Before creating a Deploy Hook, ensure your Worker is [connected to a Git repository](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/). 1. Go to **Workers & Pages** and select your Worker. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Go to **Settings** \> **Builds** \> **Deploy Hooks**. 3. Enter a **name** and select the **branch** to build. 4. Select **Create** and copy the generated URL. Note Give each Deploy Hook a descriptive name so you can tell them apart. If you have multiple content sources that each need to trigger builds independently, create a separate hook for each one. ## Trigger a Deploy Hook Send an HTTP POST request to your Deploy Hook URL to start a build: ```bash curl -X POST "https://api.cloudflare.com/client/v4/workers/builds/deploy_hooks/" ``` No `Authorization` header is needed. The unique identifier embedded in the URL acts as the authentication credential. Example response: ```json { "success": true, "errors": [], "messages": [], "result": { "build_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "branch": "main", "worker": "my-worker" } } ``` The `build_uuid` in the response can be used to [monitor build status and retrieve logs](https://developers.cloudflare.com/workers/ci-cd/builds/api-reference/#get-build-logs). ### Verify the build After you trigger a Deploy Hook, you can verify it from the dashboard: * In the **Deploy Hooks** list, the hook shows when it was last triggered. * In your Worker's build history, the **Triggered by** column identifies builds started by a Deploy Hook using the hook name and a `deploy hook` label. If you need to inspect these builds programmatically, use [List builds for a Worker](https://developers.cloudflare.com/workers/ci-cd/builds/api-reference/#list-builds-for-a-worker) in the Builds API reference. Hook-triggered builds are recorded with `build_trigger_source: "deploy_hook"`. ## CMS integration Most headless CMS platforms support webhooks that call your Deploy Hook URL when content changes. The general setup is the same across platforms: 1. Find the webhooks or integrations settings in your CMS. 2. Create a new webhook and paste your Deploy Hook URL as the target URL. 3. Select which events should trigger the webhook (for example, publish, unpublish, or update). Refer to your CMS documentation for platform-specific instructions. Popular platforms with webhook support include Contentful, Sanity, Strapi, Storyblok, DatoCMS, and Prismic. ## Idempotency If the same Deploy Hook is triggered again before the previous build has fully started, Workers Builds does not create a duplicate build. Instead, it returns the build that is already in progress. If an external system sends the same Deploy Hook twice in quick succession: 1. The first request creates a build. 2. If a second request arrives while that build is still `queued` or `initializing`, no second build is created. 3. Instead, the response returns the existing `build_uuid` and sets `already_exists` to `true`. Example response when an existing pending build is returned: ```json { "success": true, "errors": [], "messages": [], "result": { "build_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "queued", "created_on": "2026-01-21T18:50:00Z", "already_exists": true } } ``` Once the earlier build moves past `initializing`, a later POST creates a new build as normal. This makes Deploy Hooks safe to use with systems that retry webhooks or emit bursts of content-update events. ## Examples ### Deploy from a Slack slash command A Worker that receives a `/deploy` command from Slack and triggers a build: ```js export default { async fetch(request, env) { const body = await request.formData(); const command = body.get("command"); const token = body.get("token"); if (token !== env.SLACK_VERIFICATION_TOKEN) { return new Response("Unauthorized", { status: 401 }); } if (command === "/deploy") { const res = await fetch(env.DEPLOY_HOOK_URL, { method: "POST" }); const { result } = await res.json(); return new Response(`Build started: ${result.build_uuid}`); } return new Response("Unknown command", { status: 400 }); }, }; ``` ```ts export default { async fetch(request: Request, env: Env): Promise { const body = await request.formData(); const command = body.get("command"); const token = body.get("token"); if (token !== env.SLACK_VERIFICATION_TOKEN) { return new Response("Unauthorized", { status: 401 }); } if (command === "/deploy") { const res = await fetch(env.DEPLOY_HOOK_URL, { method: "POST" }); const { result } = await res.json<{ result: { build_uuid: string } }>(); return new Response(`Build started: ${result.build_uuid}`); } return new Response("Unknown command", { status: 400 }); }, }; ``` ### Rebuild on a schedule A Worker with a [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) that rebuilds every hour: ```js export default { async scheduled(event, env) { await fetch(env.DEPLOY_HOOK_URL, { method: "POST" }); }, }; ``` ```ts export default { async scheduled(event: ScheduledEvent, env: Env): Promise { await fetch(env.DEPLOY_HOOK_URL, { method: "POST" }); }, }; ``` ## Security considerations Caution Deploy Hook URLs do not require a separate authorization header. Anyone with access to the URL can trigger builds for your Worker, so store them like other sensitive credentials. * Store Deploy Hook URLs in environment variables or a secrets manager, never in source code or public configuration files. * Restrict access to the URL to only the systems that need it. * If a URL is compromised or you suspect unauthorized use, delete the Deploy Hook immediately and create a new one. The old URL stops working as soon as it is deleted. ### Using the Builds API for authenticated triggers If your external system supports custom headers, you can call the [manual build endpoint](https://developers.cloudflare.com/api/resources/workers%5Fbuilds/subresources/triggers/methods/create%5Fbuild) with an API token in the `Authorization` header instead. This gives you token-based authentication and the ability to choose the branch per request. For a step-by-step walkthrough, see [Trigger a manual build](https://developers.cloudflare.com/workers/ci-cd/builds/api-reference/#trigger-a-manual-build). ## Limits Deploy Hooks are rate limited to 10 builds per minute per Worker and 100 builds per minute per account. For all Workers Builds limits, see [Limits & pricing](https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/#page","headline":"Deploy Hooks · Cloudflare Workers docs","description":"Generate unique URLs that trigger new builds when they receive an HTTP POST request.","url":"https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Subscribe to Workers Builds events and send notifications to Slack, Discord, or webhook endpoints. title: Event subscriptions image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Event subscriptions Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/event-subscriptions/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) [Event subscriptions](https://developers.cloudflare.com/queues/event-subscriptions/) allow you to receive messages when events occur across your Cloudflare account. Cloudflare products (e.g., [KV](https://developers.cloudflare.com/kv/), [Workers AI](https://developers.cloudflare.com/workers-ai/), [Workers](https://developers.cloudflare.com/workers/)) can publish structured events to a [queue](https://developers.cloudflare.com/queues/), which you can then consume with Workers or [HTTP pull consumers](https://developers.cloudflare.com/queues/configuration/pull-consumers/) to build custom workflows, integrations, or logic. For more information on [Event Subscriptions](https://developers.cloudflare.com/queues/event-subscriptions/), refer to the [management guide](https://developers.cloudflare.com/queues/event-subscriptions/manage-event-subscriptions/). ## Send build notifications You can deploy a Worker that consumes build events and sends notifications to Slack, Discord, or any webhook endpoint: [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/templates/tree/main/workers-builds-notifications-template) The template sends notifications for: * Successful builds with preview or live deployment URLs * Failed builds with error messages * Cancelled builds ![Example Slack notifications for Workers Builds events](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1700,height=1088,format=webp/_astro/builds-notifications-slack.rcRiU95L.png) You can customize the Worker to format messages for your webhook provider. For setup instructions, refer to the [template README ↗](https://github.com/cloudflare/templates/tree/main/workers-builds-notifications-template#readme). ## Available Workers Builds events #### `build.started` Triggered when a build starts. **Example:** ```json { "type": "cf.workersBuilds.worker.build.started", "source": { "type": "workersBuilds.worker", "workerName": "my-worker" }, "payload": { "buildUuid": "build-12345678-90ab-cdef-1234-567890abcdef", "status": "running", "buildOutcome": null, "createdAt": "2025-05-01T02:48:57.132Z", "initializingAt": "2025-05-01T02:48:58.132Z", "runningAt": "2025-05-01T02:48:59.132Z", "stoppedAt": null, "buildTriggerMetadata": { "buildTriggerSource": "push_event", "branch": "main", "commitHash": "abc123def456", "commitMessage": "Fix bug in authentication", "author": "developer@example.com", "buildCommand": "npm run build", "deployCommand": "wrangler deploy", "rootDirectory": "/", "repoName": "my-worker-repo", "providerAccountName": "github-user", "providerType": "github" } }, "metadata": { "accountId": "f9f79265f388666de8122cfb508d7776", "eventSubscriptionId": "1830c4bb612e43c3af7f4cada31fbf3f", "eventSchemaVersion": 1, "eventTimestamp": "2025-05-01T02:48:57.132Z" } } ``` #### `build.failed` Triggered when a build fails. **Example:** ```json { "type": "cf.workersBuilds.worker.build.failed", "source": { "type": "workersBuilds.worker", "workerName": "my-worker" }, "payload": { "buildUuid": "build-12345678-90ab-cdef-1234-567890abcdef", "status": "failed", "buildOutcome": "failure", "createdAt": "2025-05-01T02:48:57.132Z", "initializingAt": "2025-05-01T02:48:58.132Z", "runningAt": "2025-05-01T02:48:59.132Z", "stoppedAt": "2025-05-01T02:50:00.132Z", "buildTriggerMetadata": { "buildTriggerSource": "push_event", "branch": "main", "commitHash": "abc123def456", "commitMessage": "Fix bug in authentication", "author": "developer@example.com", "buildCommand": "npm run build", "deployCommand": "wrangler deploy", "rootDirectory": "/", "repoName": "my-worker-repo", "providerAccountName": "github-user", "providerType": "github" } }, "metadata": { "accountId": "f9f79265f388666de8122cfb508d7776", "eventSubscriptionId": "1830c4bb612e43c3af7f4cada31fbf3f", "eventSchemaVersion": 1, "eventTimestamp": "2025-05-01T02:48:57.132Z" } } ``` #### `build.canceled` Triggered when a build is canceled. **Example:** ```json { "type": "cf.workersBuilds.worker.build.canceled", "source": { "type": "workersBuilds.worker", "workerName": "my-worker" }, "payload": { "buildUuid": "build-12345678-90ab-cdef-1234-567890abcdef", "status": "canceled", "buildOutcome": "canceled", "createdAt": "2025-05-01T02:48:57.132Z", "initializingAt": "2025-05-01T02:48:58.132Z", "runningAt": "2025-05-01T02:48:59.132Z", "stoppedAt": "2025-05-01T02:49:30.132Z", "buildTriggerMetadata": { "buildTriggerSource": "push_event", "branch": "main", "commitHash": "abc123def456", "commitMessage": "Fix bug in authentication", "author": "developer@example.com", "buildCommand": "npm run build", "deployCommand": "wrangler deploy", "rootDirectory": "/", "repoName": "my-worker-repo", "providerAccountName": "github-user", "providerType": "github" } }, "metadata": { "accountId": "f9f79265f388666de8122cfb508d7776", "eventSubscriptionId": "1830c4bb612e43c3af7f4cada31fbf3f", "eventSchemaVersion": 1, "eventTimestamp": "2025-05-01T02:48:57.132Z" } } ``` #### `build.succeeded` Triggered when a build succeeds. **Example:** ```json { "type": "cf.workersBuilds.worker.build.succeeded", "source": { "type": "workersBuilds.worker", "workerName": "my-worker" }, "payload": { "buildUuid": "build-12345678-90ab-cdef-1234-567890abcdef", "status": "success", "buildOutcome": "success", "createdAt": "2025-05-01T02:48:57.132Z", "initializingAt": "2025-05-01T02:48:58.132Z", "runningAt": "2025-05-01T02:48:59.132Z", "stoppedAt": "2025-05-01T02:50:15.132Z", "buildTriggerMetadata": { "buildTriggerSource": "push_event", "branch": "main", "commitHash": "abc123def456", "commitMessage": "Fix bug in authentication", "author": "developer@example.com", "buildCommand": "npm run build", "deployCommand": "wrangler deploy", "rootDirectory": "/", "repoName": "my-worker-repo", "providerAccountName": "github-user", "providerType": "github" } }, "metadata": { "accountId": "f9f79265f388666de8122cfb508d7776", "eventSubscriptionId": "1830c4bb612e43c3af7f4cada31fbf3f", "eventSchemaVersion": 1, "eventTimestamp": "2025-05-01T02:48:57.132Z" } } ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/event-subscriptions/#page","headline":"Event subscriptions · Cloudflare Workers docs","description":"Subscribe to Workers Builds events and send notifications to Slack, Discord, or webhook endpoints.","url":"https://developers.cloudflare.com/workers/ci-cd/builds/event-subscriptions/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to add and manage your Git integration for Workers Builds title: Git integration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Git integration Last updated May 29, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Cloudflare supports connecting your [GitHub](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/) and [GitLab](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/) repository to your Cloudflare Worker, and will automatically deploy your code every time you push a change. Adding a Git integration also lets you monitor build statuses directly in your Git provider using [pull request comments](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#pull-request-comment), [check runs](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#check-run), or [commit statuses](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/#commit-status), so you can manage deployments without leaving your workflow. ## Supported Git Providers Cloudflare supports connecting Cloudflare Workers to your GitHub and GitLab repositories. Workers Builds does not currently support connecting self-hosted instances of GitHub or GitLab. If you are using a different Git provider (e.g. Bitbucket), you can use an [external CI/CD provider (e.g. GitHub Actions)](https://developers.cloudflare.com/workers/ci-cd/external-cicd/) and deploy using [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy). ## Add a Git Integration Workers Builds provides direct integration with GitHub and GitLab accounts, including both individual and organization accounts, that are _not_ self-hosted. If you do not have a Git account linked to your Cloudflare account, you will be prompted to set up an installation to GitHub or GitLab when [connecting a repository](https://developers.cloudflare.com/workers/ci-cd/builds/#get-started) for the first time, or when adding a new Git account. Follow the prompts and authorize the Cloudflare Git integration. ![Git providers](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1522,height=282,format=webp/_astro/workers-git-provider.aIMoWcJE.png) You can check the following pages to see if your Git integration has been installed: * [GitHub Applications page ↗](https://github.com/settings/installations) (if you are in an organization, select **Switch settings context** to access your GitHub organization settings) * [GitLab Authorized Applications page ↗](https://gitlab.com/-/profile/applications) For details on providing access to organization accounts, see [GitHub organizational access](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#organizational-access) and [GitLab organizational access](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/#organizational-access). ## Manage a Git Integration To manage your Git installation: 1. Go to the **Workers & Pages** page in the Cloudflare dashboard. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select your Worker. 3. Go to **Settings** \> **Builds**. 4. Under **Git Repository**, select **Manage**. This can be useful for managing repository access or troubleshooting installation issues by reinstalling. For more details, see the [GitHub](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration) and [GitLab](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration) guides for how to manage your installation. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/#page","headline":"Git integration · Cloudflare Workers docs","description":"Learn how to add and manage your Git integration for Workers Builds","url":"https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-29","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to manage your GitHub integration for Workers Builds title: GitHub integration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # GitHub integration Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Cloudflare supports connecting your GitHub repository to your Cloudflare Worker, and will automatically deploy your code every time you push a change. ## Features Beyond automatic builds and deployments, the Cloudflare GitHub integration lets you monitor builds directly in GitHub, keeping you informed without leaving your workflow. ### Pull request comment If a commit is on a pull request, Cloudflare will automatically post a comment on the pull request with the status of the build. ![GitHub pull request comment](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1426,height=504,format=webp/_astro/github-pull-request-comment.DIkAC8Yh.png) A [preview URL](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) will be provided for any builds which perform `wrangler versions upload`. This is particularly useful when reviewing your pull request, as it allows you to compare the code changes alongside an updated version of your Worker. Note Preview URLs are not generated for Workers that implement a [Durable Object](https://developers.cloudflare.com/durable-objects/), including [Containers](https://developers.cloudflare.com/containers/) and [Sandbox](https://developers.cloudflare.com/sandbox/) Workers. Refer to [Preview URL limitations](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/#limitations). Comment history reveals any builds completed earlier while the PR was open. ![GitHub pull request comment history](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1634,height=604,format=webp/_astro/github-pull-request-comment-history.pAxP7K1u.png) ### Check run If you have one or multiple Workers connected to a repository (i.e. a [monorepo](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#monorepos)), you can check on the status of each build within GitHub via [GitHub check runs ↗](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks#checks). You can see the checks by selecting on the status icon next to a commit within your GitHub repository. In the example below, you can select the green check mark to see the results of the check run. ![GitHub status](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1804,height=122,format=webp/_astro/gh-status-check-runs.DkY_pO9C.png) Check runs will appear like the following in your repository. You can select **Details** to view the build (Build ID) and project (Script) associated with each check. ![GitHub check runs](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1294,height=400,format=webp/_astro/workers-builds-gh-check-runs.CuqL6Htu.png) Note that when using [build watch paths](https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/), only projects that trigger a build will generate a check run. ## Manage access You can deploy projects to Cloudflare Workers from your company or side project on GitHub using the [Cloudflare Workers & Pages GitHub App ↗](https://github.com/apps/cloudflare-workers-and-pages). ### Organizational access When authorizing Cloudflare Workers to access a GitHub account, you can specify access to your individual account or an organization that you belong to on GitHub. To add Cloudflare Workers installation to an organization, your user account must be an owner or have the appropriate role within the organization (i.e. the GitHub Apps Manager role). More information on these roles can be seen on [GitHub's documentation ↗](https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/roles-in-an-organization#github-app-managers). GitHub security consideration A GitHub account should only point to one Cloudflare account. If you are setting up Cloudflare with GitHub for your organization, Cloudflare recommends that you limit the scope of the application to only the repositories you intend to build with Pages. To modify these permissions, go to the [Applications page ↗](https://github.com/settings/installations) on GitHub and select **Switch settings context** to access your GitHub organization settings. Then, select **Cloudflare Workers & Pages** \> For **Repository access**, select **Only select repositories** \> select your repositories. ### Remove access You can remove Cloudflare Workers' access to your GitHub repository or account by going to the [Applications page ↗](https://github.com/settings/installations) on GitHub (if you are in an organization, select Switch settings context to access your GitHub organization settings). The GitHub App is named Cloudflare Workers and Pages, and it is shared between Workers and Pages projects. #### Remove Cloudflare access to a GitHub repository To remove access to an individual GitHub repository, you can navigate to **Repository access**. Select the **Only select repositories** option, and configure which repositories you would like Cloudflare to have access to. ![GitHub Repository Access](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1900,height=810,format=webp/_astro/github-repository-access.DGHekBft.png) #### Remove Cloudflare access to the entire GitHub account To remove Cloudflare Workers and Pages access to your entire Git account, you can navigate to **Uninstall "Cloudflare Workers and Pages"**, then select **Uninstall**. Removing access to the Cloudflare Workers and Pages app will revoke Cloudflare's access to _all repositories_ from that GitHub account. If you want to only disable automatic builds and deployments, follow the [Disable Build](https://developers.cloudflare.com/workers/ci-cd/builds/#disconnecting-builds) instructions. Note that removing access to GitHub will disable new builds for Workers and Pages projects that were connected to those repositories, though your previous deployments will continue to be hosted by Cloudflare Workers. ### Reinstall the Cloudflare GitHub App When encountering Git integration related issues, one potential troubleshooting step is attempting to uninstall and reinstall the GitHub or GitLab application associated with the Cloudflare Pages installation. The process for each Git provider is provided below. 1. Go to the installation settings page on GitHub: * Navigate to **Settings > Builds** for the Workers or Pages project and select **Manage** under Git Repository. * Alternatively, visit these links to find the Cloudflare Workers and Pages installation and select **Configure**: | **Individual** | https://github.com/settings/installations | | ---------------- | ---------------------------------------------------------------------------------- | | **Organization** | https://github.com/organizations//settings/installations | 1. In the Cloudflare Workers and Pages GitHub App settings page, navigate to **Uninstall "Cloudflare Workers and Pages"** and select **Uninstall**. 2. Go back to the [**Workers & Pages** overview ↗](https://dash.cloudflare.com) page. Select **Create application** \> **Pages** \> **Connect to Git**. 3. Select the **\+ Add account** button, select the GitHub account you want to add, and then select **Install & Authorize**. 4. You should be redirected to the create project page with your GitHub account or organization in the account list. 5. Attempt to make a new deployment with your project which was previously broken. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#page","headline":"GitHub integration · Cloudflare Workers docs","description":"Learn how to manage your GitHub integration for Workers Builds","url":"https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to manage your GitLab integration for Workers Builds title: GitLab integration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # GitLab integration Last updated Aug 13, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Cloudflare supports connecting your GitLab repository to your Cloudflare Worker, and will automatically deploy your code every time you push a change. ## Features Beyond automatic builds and deployments, the Cloudflare GitLab integration lets you monitor builds directly in GitLab, keeping you informed without leaving your workflow. ### Merge request comment If a commit is on a merge request, Cloudflare will automatically post a comment on the merge request with the status of the build. ![GitLab merge request comment](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1890,height=586,format=webp/_astro/gitlab-pull-request-comment.CQVsQ21r.png) A [preview URL](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/) will be provided for any builds which perform `wrangler versions upload`. This is particularly useful when reviewing your pull request, as it allows you to compare the code changes alongside an updated version of your Worker. Note Preview URLs are not generated for Workers that implement a [Durable Object](https://developers.cloudflare.com/durable-objects/), including [Containers](https://developers.cloudflare.com/containers/) and [Sandbox](https://developers.cloudflare.com/sandbox/) Workers. Refer to [Preview URL limitations](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/#limitations). Enabling GitLab Merge Request events for existing connections New GitLab connections are automatically configured to receive merge request events, which enable commenting functionality. For existing connections, you'll need to manually enable `Merge request events` in the Webhooks tab of your project's settings. You can follow GitLab's documentation for guidance on [managing webhooks ↗](https://docs.gitlab.com/user/project/integrations/webhooks/#manage-webhooks). ### Commit Status If you have one or multiple Workers connected to a repository (i.e. a [monorepo](https://developers.cloudflare.com/workers/ci-cd/builds/advanced-setups/#monorepos)), you can check on the status of each build within GitLab via [GitLab commit status ↗](https://docs.gitlab.com/ee/user/project/merge%5Frequests/status%5Fchecks.html). You can see the statuses by selecting the status icon next to a commit or by going to **Build** \> **Pipelines** within your GitLab repository. In the example below, you can select on the green check mark to see the results of the check run. ![GitLab Status](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=2510,height=152,format=webp/_astro/gl-status-checks.B9jgSbf7.png) Check runs will appear like the following in your repository. You can select one of the statuses to view the build on the Cloudflare Dashboard. ![GitLab Commit Status](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=610,height=270,format=webp/_astro/gl-commit-status.BghMWpYX.png) Note that when using [build watch paths](https://developers.cloudflare.com/workers/ci-cd/builds/build-watch-paths/), only projects that trigger a build will generate a commit status. ## Manage access You can deploy projects to Cloudflare Workers from your company or side project on GitLab using the Cloudflare Pages app. ### Organizational access When you authorize Cloudflare Workers to access your GitLab account, you automatically give Cloudflare Workers access to organizations, groups, and namespaces accessed by your GitLab account. Managing access to these organizations and groups is handled by GitLab. ### Remove access You can remove Cloudflare Workers' access to your GitLab account by navigating to [Authorized Applications page ↗](https://gitlab.com/-/profile/applications) on GitLab. Find the applications called Cloudflare Pages and select the **Revoke** button to revoke access. Note that the GitLab application Cloudflare Workers is shared between Workers and Pages projects, and removing access to GitLab will disable new builds for Workers and Pages, though your previous deployments will continue to be hosted by Cloudflare Workers. ### Reinstall the Cloudflare GitLab App 1. Go to your application settings page on GitLab: [https://gitlab.com/-/profile/applications ↗](https://gitlab.com/-/profile/applications) 2. Click the "Revoke" button on your Cloudflare Workers installation if it exists. 3. Go back to the [**Workers & Pages** overview ↗](https://dash.cloudflare.com) page. Select **Create application** \> **Pages** \> **Connect to Git**. 4. Select the **\+ Add account** button, select the GitLab account you want to add, and then select **Install & Authorize**. 5. You should be redirected to the create project page with your GitLab account or organization in the account list. 6. Attempt to make a new deployment with your project which was previously broken. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/#page","headline":"GitLab integration · Cloudflare Workers docs","description":"Learn how to manage your GitLab integration for Workers Builds","url":"https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-13","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Limits & pricing for Workers Builds title: Limits & pricing image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Limits & pricing Last updated May 29, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Workers Builds has the following limits. | Metric | Free plan | Paid plans | | --------------------------------- | -------------------------------------- | ------------------------------------------ | | **Build minutes** | 3,000 per month | 6,000 per month (then, +$0.005 per minute) | | **Concurrent builds** | 1 | 6 | | **Build timeout** | 20 minutes | 20 minutes | | **Deploy Hooks** | 10/min per Worker, 100/min per account | 10/min per Worker, 100/min per account | | **CPU** | 2 vCPU | 4 vCPU | | **Memory** | 8 GB | 8 GB | | **Disk space** | 20 GB | 20 GB | | **Environment variables** | 64 | 64 | | **Size per environment variable** | 5 KB | 5 KB | ## Definitions * **Build minutes**: The number of minutes that it takes to build a project. * **Concurrent builds**: The number of builds that can run in parallel across an account. * **Build timeout**: The amount of time that a build can be run before it is terminated. * **Deploy Hooks**: The rate limit for builds triggered by [Deploy Hooks](https://developers.cloudflare.com/workers/ci-cd/builds/deploy-hooks/). * **vCPU**: The number of CPU cores available to your build. * **Memory**: The amount of memory available to your build. * **Disk space**: The amount of disk space available to your build. * **Environment variables**: The number of custom environment variables you can configure per Worker. * **Size per environment variable**: The maximum size for each individual environment variable. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/#page","headline":"Limits & pricing · Cloudflare Workers docs","description":"Limits & pricing for Workers Builds","url":"https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-29","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn how to troubleshoot common and known issues in Workers Builds. title: Troubleshooting builds image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Troubleshooting builds Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This guide explains how to identify and resolve build errors, as well as troubleshoot common issues in the Workers Builds deployment process. To view your build history, go to your Worker project in the Cloudflare dashboard, select **Deployment**, select **View Build History** at the bottom of the page, and select the build you want to view. To retry a build, select the ellipses next to the build and select **Retry build**. Alternatively, you can select **Retry build** on the Build Details page. ## Known issues or limitations Here are some common build errors that may surface in the build logs or general issues and how you can resolve them. ### Workers name requirement `✘ [ERROR] The name in your Wrangler configuration file () must match the name of your Worker. Please update the name field in your Wrangler configuration file.` When connecting a Git repository to your Workers project, the specified name for the Worker on the Cloudflare dashboard must match the `name` argument in the Wrangler configuration file located in the specified root directory. If it does not match, update the name field in your Wrangler configuration file to match the name of the Worker on the dashboard. The build system uses the `name` argument in the Wrangler configuration file to determine which Worker to deploy to Cloudflare's global network. This requirement ensures consistency between the Worker's name on the dashboard and the deployed Worker. Note This does not apply to [Wrangler Environments](https://developers.cloudflare.com/workers/wrangler/environments/) if the Worker name before the `-` suffix matches the name in the Wrangler configuration file. For example, a Worker named `my-worker-staging` on the dashboard can be deployed from a repository that contains a Wrangler configuration file with the arguments `name = my-worker` and `[env.staging]` using the deploy command `npx wrangler deploy --env staging`. On Wrangler v3 and up, Workers Builds automatically matches the name of the connected Worker by overriding it with the `WRANGLER_CI_OVERRIDE_NAME` environment variable. ### Missing Wrangler configuration file `✘ [ERROR] Missing entry-point: The entry-point should be specified via the command line (e.g. wrangler deploy path/to/script) or the main config field.` If you see this error, a Wrangler configuration file is likely missing from the root directory. Navigate to **Settings** \> **Build** \> **Build Configuration** to update the root directory, or add a [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to the specified directory. ### Incorrect account\_id `Could not route to /client/v4/accounts//workers/services/, perhaps your object identifier is invalid? [code: 7003]` If you see this error, the Wrangler configuration file likely has an `account_id` for a different account. Remove the `account_id` argument or update it with your account's `account_id`, available in **Workers & Pages Overview** under **Account Details**. ### Stale API token ` Failed: The build token selected for this build has been deleted or rolled and cannot be used for this build. Please update your build token in the Worker Builds settings and retry the build.` The API Token dropdown in Build Configuration settings may show stale tokens that were edited, deleted, or rolled. If you encounter an error due to a stale token, create a new API Token and select it for the build. ### Build timed out `Build was timed out` There is a maximum build duration of 20 minutes. If a build exceeds this time, then the build will be terminated and the above error log is shown. For more details, see [Workers Builds limits](https://developers.cloudflare.com/workers/ci-cd/builds/limits-and-pricing/). ### Git integration issues If you are running into errors associated with your Git integration, you can try removing access to your [GitHub](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#removing-access) or [GitLab](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/#removing-access) integration from Cloudflare, then reinstalling the [GitHub](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/github-integration/#reinstall-a-git-integration) or [GitLab](https://developers.cloudflare.com/workers/ci-cd/builds/git-integration/gitlab-integration/#reinstall-a-git-integration) integration. ## For additional support If you discover additional issues or would like to provide feedback, reach out to us in the [Cloudflare Developers Discord ↗](https://discord.com/channels/595317990191398933/1052656806058528849). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/#page","headline":"Troubleshooting builds · Cloudflare Workers docs","description":"Learn how to troubleshoot common and known issues in Workers Builds.","url":"https://developers.cloudflare.com/workers/ci-cd/builds/troubleshoot/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Integrate Workers development into your existing continuous integration and continuous development workflows, such as GitHub Actions or GitLab Pipelines. title: External CI/CD image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # External CI/CD Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/external-cicd/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Deploying Cloudflare Workers with CI/CD ensures reliable, automated deployments for every code change. If you prefer to use your existing CI/CD provider instead of [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/builds/), this section offers guides for popular providers: * [**GitHub Actions**](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/) * [**GitLab CI/CD**](https://developers.cloudflare.com/workers/ci-cd/external-cicd/gitlab-cicd/) Other CI/CD options including but not limited to Terraform, CircleCI, Jenkins, and more, can also be used to deploy Workers following a similar set up process. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/external-cicd/#page","headline":"External CI/CD · Cloudflare Workers docs","description":"Integrate Workers development into your existing continuous integration and continuous development workflows, such as GitHub Actions or GitLab Pipelines.","url":"https://developers.cloudflare.com/workers/ci-cd/external-cicd/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Integrate Workers development into your existing GitHub Actions workflows. title: GitHub Actions image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # GitHub Actions Last updated Apr 30, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can deploy Workers with [GitHub Actions ↗](https://github.com/marketplace/actions/deploy-to-cloudflare-workers-with-wrangler). Here is how you can set up your GitHub Actions workflow. ## 1\. Authentication When running Wrangler locally, authentication to the Cloudflare API happens via the [wrangler login](https://developers.cloudflare.com/workers/wrangler/commands/general/#login) command, which initiates an interactive authentication flow. Since CI/CD environments are non-interactive, Wrangler requires a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and [account ID](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/) to authenticate with the Cloudflare API. ### Cloudflare account ID To find your Cloudflare account ID, refer to [Find account and zone IDs](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/). ### API token To create an API token to authenticate Wrangler in your CI job: 1. In the Cloudflare dashboard, go to the **Account API tokens** page. [Go to **Account API tokens** ↗](https://dash.cloudflare.com/?to=/:account/api-tokens) 2. Select **Create Token**. 3. Under **Permission policies**, open the **Custom** dropdown and select **Edit Cloudflare Workers**. 4. Customize your token name. 5. Scope your token. You will need to choose the account and zone resources that the generated API token will have access to. We recommend scoping these down as much as possible to limit the access of your token. For example, if you have access to three different Cloudflare accounts, you should restrict the generated API token to only the account on which you will be deploying a Worker. ## 2\. Set up CI/CD The method for running Wrangler in your CI/CD environment will depend on the specific setup for your project (whether you use GitHub Actions/Jenkins/GitLab or something else entirely). To set up your CI/CD: 1. Go to your CI/CD platform and add the following as secrets: * `CLOUDFLARE_ACCOUNT_ID`: Set to the [Cloudflare account ID](#cloudflare-account-id) for the account on which you want to deploy your Worker. * `CLOUDFLARE_API_TOKEN`: Set to the [Cloudflare API token you generated](#api-token). Caution Don't store the value of `CLOUDFLARE_API_TOKEN` in your repository, as it gives access to deploy Workers on your account. Instead, you should utilize your CI/CD provider's support for storing secrets. 1. Create a workflow that will be responsible for deploying the Worker. This workflow should run `wrangler deploy`. Review an example [GitHub Actions ↗](https://docs.github.com/en/actions/using-workflows/about-workflows) workflow in the follow section. ### GitHub Actions Cloudflare provides [an official action ↗](https://github.com/cloudflare/wrangler-action) for deploying Workers. Refer to the following example workflow which deploys your Worker on push to the `main` branch. ```yaml name: Deploy Worker on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 60 steps: - uses: actions/checkout@v6 - name: Build & Deploy Worker uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/#page","headline":"GitHub Actions · Cloudflare Workers docs","description":"Integrate Workers development into your existing GitHub Actions workflows.","url":"https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-30","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Integrate Workers development into your existing GitLab Pipelines workflows. title: GitLab CI/CD image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # GitLab CI/CD Last updated Apr 30, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/ci-cd/external-cicd/gitlab-cicd/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can deploy Workers with [GitLab CI/CD ↗](https://docs.gitlab.com/ee/ci/pipelines/index.html). Here is how you can set up your Gitlab CI/CD pipeline. ## 1\. Authentication When running Wrangler locally, authentication to the Cloudflare API happens via the [wrangler login](https://developers.cloudflare.com/workers/wrangler/commands/general/#login) command, which initiates an interactive authentication flow. Since CI/CD environments are non-interactive, Wrangler requires a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and [account ID](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/) to authenticate with the Cloudflare API. ### Cloudflare account ID To find your Cloudflare account ID, refer to [Find account and zone IDs](https://developers.cloudflare.com/fundamentals/account/find-account-and-zone-ids/). ### API token To create an API token to authenticate Wrangler in your CI job: 1. In the Cloudflare dashboard, go to the **Account API tokens** page. [Go to **Account API tokens** ↗](https://dash.cloudflare.com/?to=/:account/api-tokens) 2. Select **Create Token**. 3. Under **Permission policies**, open the **Custom** dropdown and select **Edit Cloudflare Workers**. 4. Customize your token name. 5. Scope your token. You will need to choose the account and zone resources that the generated API token will have access to. We recommend scoping these down as much as possible to limit the access of your token. For example, if you have access to three different Cloudflare accounts, you should restrict the generated API token to only the account on which you will be deploying a Worker. ## 2\. Set up CI The method for running Wrangler in your CI/CD environment will depend on the specific setup for your project (whether you use GitHub Actions/Jenkins/GitLab or something else entirely). To set up your CI: 1. Go to your CI platform and add the following as secrets: * `CLOUDFLARE_ACCOUNT_ID`: Set to the [Cloudflare account ID](#cloudflare-account-id) for the account on which you want to deploy your Worker. * `CLOUDFLARE_API_TOKEN`: Set to the [Cloudflare API token you generated](#api-token). Caution Don't store the value of `CLOUDFLARE_API_TOKEN` in your repository, as it gives access to deploy Workers on your account. Instead, you should utilize your CI/CD provider's support for storing secrets. 1. Create a workflow that will be responsible for deploying the Worker. This workflow should run `wrangler deploy`. Review an example [GitHub Actions ↗](https://docs.github.com/en/actions/using-workflows/about-workflows) workflow in the following section. ### GitLab Pipelines Refer to [GitLab's blog ↗](https://about.gitlab.com/blog/2022/11/21/deploy-remix-with-gitlab-and-cloudflare/) for an example pipeline. Under the `script` key, replace `npm run deploy` with [npx wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/ci-cd/external-cicd/gitlab-cicd/#page","headline":"GitLab CI/CD · Cloudflare Workers docs","description":"Integrate Workers development into your existing GitLab Pipelines workflows.","url":"https://developers.cloudflare.com/workers/ci-cd/external-cicd/gitlab-cicd/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-30","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Explore the JavaScript and web platform APIs available in the Cloudflare Workers runtime. title: Runtime APIs image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Runtime APIs Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The [Workers runtime ↗](https://blog.cloudflare.com/workerd-open-source-workers-runtime/) is designed to be [JavaScript standards compliant ↗](https://ecma-international.org/publications-and-standards/standards/ecma-262/) and web-interoperable. Wherever possible, it uses web platform APIs, so that code can be reused across client and server, as well as across [WinterCG ↗](https://wintercg.org/) JavaScript runtimes. [Workers runtime features](https://developers.cloudflare.com/workers/runtime-apis/) are [compatible with a subset of Node.js APIs](https://developers.cloudflare.com/workers/runtime-apis/nodejs) and the ability to set a [compatibility date or compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-dates/). * [Bindings (env)](https://developers.cloudflare.com/workers/runtime-apis/bindings/) * [Cache](https://developers.cloudflare.com/workers/runtime-apis/cache/) * [Console](https://developers.cloudflare.com/workers/runtime-apis/console/) * [Context (ctx)](https://developers.cloudflare.com/workers/runtime-apis/context/) * [Encoding](https://developers.cloudflare.com/workers/runtime-apis/encoding/) * [EventSource](https://developers.cloudflare.com/workers/runtime-apis/eventsource/) * [Fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/) * [Handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/) * [Headers](https://developers.cloudflare.com/workers/runtime-apis/headers/) * [HTMLRewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/) * [MessageChannel](https://developers.cloudflare.com/workers/runtime-apis/messagechannel/) * [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) * [Performance and timers](https://developers.cloudflare.com/workers/runtime-apis/performance/) * [Remote-procedure call (RPC)](https://developers.cloudflare.com/workers/runtime-apis/rpc/) * [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) * [Response](https://developers.cloudflare.com/workers/runtime-apis/response/) * [Scheduler](https://developers.cloudflare.com/workers/runtime-apis/scheduler/) * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [TCP sockets](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) * [Web Crypto](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) * [Web standards](https://developers.cloudflare.com/workers/runtime-apis/web-standards/) * [WebAssembly (Wasm)](https://developers.cloudflare.com/workers/runtime-apis/webassembly/) * [WebSockets](https://developers.cloudflare.com/workers/runtime-apis/websockets/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/runtime-apis/#page","headline":"Runtime APIs · Cloudflare Workers docs","description":"Explore the JavaScript and web platform APIs available in the Cloudflare Workers runtime.","url":"https://developers.cloudflare.com/workers/runtime-apis/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Worker Bindings that allow for interaction with other Cloudflare Resources. title: Bindings (env) image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Bindings (env) Last updated Jul 22, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform. Bindings provide better performance and less restrictions when accessing resources from Workers than the [REST APIs](https://developers.cloudflare.com/api/) which are intended for non-Workers applications. During local development, bindings connect to locally simulated resources by default. You can also configure them to connect to real, production resources using [remote bindings](https://developers.cloudflare.com/workers/local-development/#remote-bindings). The following bindings are available today: * [AI](https://developers.cloudflare.com/workers-ai/get-started/workers-wrangler/#2-connect-your-worker-to-workers-ai) * [Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine) * [Assets](https://developers.cloudflare.com/workers/static-assets/binding/) * [Browser Run](https://developers.cloudflare.com/browser-run/) * [D1](https://developers.cloudflare.com/d1/worker-api/) * [Dispatcher (Workers for Platforms)](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/) * [Durable Objects](https://developers.cloudflare.com/durable-objects/api/) * [Dynamic Worker Loaders](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/) * [Environment Variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) * [Hyperdrive](https://developers.cloudflare.com/hyperdrive) * [Images](https://developers.cloudflare.com/images/optimization/binding/) * [KV](https://developers.cloudflare.com/kv/api/) * [Media Transformations](https://developers.cloudflare.com/stream/transform-videos/bindings/) * [mTLS](https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls/) * [Queues](https://developers.cloudflare.com/queues/configuration/javascript-apis/) * [R2](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/) * [Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) * [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) * [Secrets Store](https://developers.cloudflare.com/secrets-store/integrations/workers/) * [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) * [Stream](https://developers.cloudflare.com/stream/manage-video-library/bindings/) * [Vectorize](https://developers.cloudflare.com/vectorize/reference/client-api/) * [Version metadata](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) * [Workflows](https://developers.cloudflare.com/workflows/) ## What is a binding? When you declare a binding on your Worker, you grant it a specific capability, such as being able to read and write files to an [R2](https://developers.cloudflare.com/r2/) bucket. For example: ```jsonc { "main": "./src/index.js", "r2_buckets": [ { "binding": "MY_BUCKET", "bucket_name": "" } ] } ``` ```toml main = "./src/index.js" [[r2_buckets]] binding = "MY_BUCKET" bucket_name = "" ``` ```js export default { async fetch(request, env) { const url = new URL(request.url); const key = url.pathname.slice(1); await env.MY_BUCKET.put(key, request.body); return new Response(`Put ${key} successfully!`); }, }; ``` ```python from workers import WorkerEntrypoint, Response from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): url = urlparse(request.url) key = url.path.slice(1) await self.env.MY_BUCKET.put(key, request.body) return Response(f"Put {key} successfully!") ``` You can think of a binding as a permission and an API in one piece. With bindings, you never have to add secret keys or tokens to your Worker in order to access resources on your Cloudflare account — the permission is embedded within the API itself. The underlying secret is never exposed to your Worker's code, and therefore can't be accidentally leaked. ## Making changes to bindings When you deploy a change to your Worker, and only change its bindings (i.e. you don't change the Worker's code), Cloudflare may reuse existing isolates that are already running your Worker. This improves performance — you can change an environment variable or other binding without unnecessarily reloading your code. As a result, you must be careful when "polluting" global scope with derivatives of your bindings. Anything you create there might continue to exist despite making changes to any underlying bindings. Consider an external client instance which uses a secret API key accessed from `env`: if you put this client instance in global scope and then make changes to the secret, a client instance using the original value might continue to exist. The correct approach would be to create a new client instance for each request. The following is a good approach: ```ts export default { fetch(request, env) { let client = new Client(env.MY_SECRET); // `client` is guaranteed to be up-to-date with the latest value of `env.MY_SECRET` since a new instance is constructed with every incoming request // ... do things with `client` }, }; ``` Compared to this alternative, which might have surprising and unwanted behavior: ```ts let client = undefined; export default { fetch(request, env) { client ??= new Client(env.MY_SECRET); // `client` here might not be updated when `env.MY_SECRET` changes, since it may already exist in global scope // ... do things with `client` }, }; ``` If you have more advanced needs, explore the [AsyncLocalStorage API](https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/), which provides a mechanism for exposing values down to child execution handlers. ## How to access `env` Bindings are located on the `env` object, which can be accessed in several ways: * It is an argument to entrypoint handlers such as [fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/): ```js export default { async fetch(request, env) { return new Response(`Hi, ${env.NAME}`); }, }; ``` * It is as class property on [WorkerEntrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env), [DurableObject](https://developers.cloudflare.com/durable-objects/), and [Workflow](https://developers.cloudflare.com/workflows/): ```js export class MyDurableObject extends DurableObject { async sayHello() { return `Hi, ${this.env.NAME}!`; } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response(f"Hi {self.env.NAME}") ``` * It can be imported from `cloudflare:workers`: ```js import { env } from "cloudflare:workers"; console.log(`Hi, ${env.Name}`); ``` ```python from workers import env print(f"Hi, {env.NAME}") ``` ### Importing `env` as a global Importing `env` from `cloudflare:workers` is useful when you need to access a binding such as [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) or [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/)in top-level global scope. For example, to initialize an API client: ```js import { env } from "cloudflare:workers"; import ApiClient from "example-api-client"; // API_KEY and LOG_LEVEL now usable in top-level scope let apiClient = ApiClient.new({ apiKey: env.API_KEY }); const LOG_LEVEL = env.LOG_LEVEL || "info"; export default { fetch(req) { // you can use apiClient or LOG_LEVEL, configured before any request is handled }, }; ``` ```python from workers import WorkerEntrypoint, env from example_api_client import ApiClient api_client = ApiClient(api_key=env.API_KEY) LOG_LEVEL = getattr(env, "LOG_LEVEL", "info") class Default(WorkerEntrypoint): async def fetch(self, request): # ... ``` Workers do not allow I/O from outside a request context. This means that even though `env` is accessible from the top-level scope, you will not be able to access every binding's methods. For instance, environment variables and secrets are accessible, and you are able to call `env.NAMESPACE.get` to get a [Durable Object stub](https://developers.cloudflare.com/durable-objects/api/stub/) in the top-level context. However, calling methods on the Durable Object stub, making [calls to a KV store](https://developers.cloudflare.com/kv/api/), and [calling to other Workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings) will not work. ```js import { env } from "cloudflare:workers"; // This would error! // env.KV.get('my-key') export default { async fetch(req) { // This works let myVal = await env.KV.get("my-key"); Response.new(myVal); }, }; ``` ```python from workers import Response, WorkerEntrypoint, env # This would fail! # env.KV.get('my-key') class Default(WorkerEntrypoint): async def fetch(self, request): # This works mv_val = await env.KV.get("my-key") return Response(my_val) ``` Additionally, importing `env` from `cloudflare:workers` lets you avoid passing `env`as an argument through many function calls if you need to access a binding from a deeply-nested function. This can be helpful in a complex codebase. ```js import { env } from "cloudflare:workers"; export default { fetch(req) { Response.new(sayHello()); }, }; // env is not an argument to sayHello... function sayHello() { let myName = getName(); return `Hello, ${myName}`; } // ...nor is it an argument to getName function getName() { return env.MY_NAME; } ``` ```python from workers import Response, WorkerEntrypoint, env class Default(WorkerEntrypoint): def fetch(req): return Response(say_hello()) # env is not an argument to say_hello... def say_hello(): my_name = get_name() return f"Hello, {myName}" # ...nor is it an argument to getName def get_name(): return env.MY_NAME ``` Note While using `env` from `cloudflare:workers` may be simpler to write than passing it through a series of function calls, passing `env` as an argument is a helpful pattern for dependency injection and testing. ### Overriding `env` values The `withEnv` function provides a mechanism for overriding values of `env`. Imagine a user has defined the [environment variable](https://developers.cloudflare.com/workers/configuration/environment-variables/)"NAME" to be "Alice" in their Wrangler configuration file and deployed a Worker. By default, logging `env.NAME` would print "Alice". Using the `withEnv` function, you can override the value of "NAME". ```js import { env, withEnv } from "cloudflare:workers"; function logName() { console.log(env.NAME); } export default { fetch(req) { // this will log "Alice" logName(); withEnv({ NAME: "Bob" }, () => { // this will log "Bob" logName(); }); // ...etc... }, }; ``` ```python from workers import Response, WorkerEntrypoint, env, patch_env def log_name(): print(env.NAME) class Default(WorkerEntrypoint): async def fetch(req): # this will log "Alice" log_name() with patch_env(NAME="Bob"): # this will log "Bob" log_name() # ...etc... ``` This can be useful when testing code that relies on an imported `env` object. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/#page","headline":"Bindings (env) · Cloudflare Workers docs","description":"Worker Bindings that allow for interaction with other Cloudflare Resources.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-22","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Bindings"]} ``` --- --- description: Details on how to configure Workers static assets and its binding. title: Configuration and Bindings image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Configuration and Bindings Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/binding/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Configuring a Worker with assets requires specifying a [directory](https://developers.cloudflare.com/workers/static-assets/binding/#directory) and, optionally, an [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/), in your Worker's Wrangler file. The [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/) allows you to dynamically fetch assets from within your Worker script (e.g. `env.ASSETS.fetch()`), similarly to how you might with a make a `fetch()` call with a [Service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/). Only one collection of static assets can be configured in each Worker. ## `directory` The folder of static assets to be served. For many frameworks, this is the `./public/`, `./dist/`, or `./build/` folder. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", // Set this to today's date "compatibility_date": "2026-08-14", "assets": { "directory": "./public/", }, } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" # Set this to today's date compatibility_date = "2026-08-14" [assets] directory = "./public/" ``` ### Ignoring assets Sometime there are files in the asset directory that should not be uploaded. In this case, create a `.assetsignore` file in the root of the assets directory. This file takes the same format as `.gitignore`. Wrangler will not upload asset files that match lines in this file. **Example** You are migrating from a Pages project where the assets directory is `dist`. You do not want to upload the server-side Worker code nor Pages configuration files as public client-side assets. Add the following `.assetsignore` file: ```txt _worker.js _redirects _headers ``` Now Wrangler will not upload these files as client-side assets when deploying the Worker. ## `run_worker_first` Controls whether to invoke the Worker script regardless of a request which would have otherwise matched an asset. `run_worker_first = false` (default) will serve any static asset matching a request, while `run_worker_first = true` will unconditionally [invoke your Worker script](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/#run-your-worker-script-first). ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", // Set this to today's date "compatibility_date": "2026-08-14", "main": "src/index.ts", // The following configuration unconditionally invokes the Worker script at // `src/index.ts`, which can programmatically fetch assets via the ASSETS binding "assets": { "directory": "./public/", "binding": "ASSETS", "run_worker_first": true, }, } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" # Set this to today's date compatibility_date = "2026-08-14" main = "src/index.ts" [assets] directory = "./public/" binding = "ASSETS" run_worker_first = true ``` You can also specify `run_worker_first` as an array of route patterns to selectively run the Worker script first only for specific routes. The array supports glob patterns with `*` for deep matching and negative patterns with `!` prefix. Negative patterns have precedence over non-negative patterns. The Worker will run first when a non-negative pattern matches and none of the negative pattern matches. The order in which the patterns are listed is not significant. `run_worker_first` is often paired with the [not\_found\_handling = "single-page-application" setting](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/#advanced-routing-control): ```jsonc { "name": "my-spa-worker", // Set this to today's date "compatibility_date": "2026-08-14", "main": "./src/index.ts", "assets": { "directory": "./dist/", "not_found_handling": "single-page-application", "binding": "ASSETS", "run_worker_first": ["/api/*", "!/api/docs/*"] } } ``` ```toml name = "my-spa-worker" # Set this to today's date compatibility_date = "2026-08-14" main = "./src/index.ts" [assets] directory = "./dist/" not_found_handling = "single-page-application" binding = "ASSETS" run_worker_first = [ "/api/*", "!/api/docs/*" ] ``` In this configuration, requests to `/api/*` routes will invoke the Worker script first, except for `/api/docs/*` which will follow the default asset-first routing behavior. Common uses for `run_worker_first` include authentication checks, A/B testing, and [injecting bootstrap data into your SPA shell](https://developers.cloudflare.com/workers/examples/spa-shell/). ## `binding` Configuring the optional [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) gives you access to the collection of assets from within your Worker script. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "main": "./src/index.js", // Set this to today's date "compatibility_date": "2026-08-14", "assets": { "directory": "./public/", "binding": "ASSETS", }, } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" main = "./src/index.js" # Set this to today's date compatibility_date = "2026-08-14" [assets] directory = "./public/" binding = "ASSETS" ``` In the example above, assets would be available through `env.ASSETS`. ### Runtime API Reference #### `fetch()` **Parameters** * `request: Request | URL | string` Pass a [Request object](https://developers.cloudflare.com/workers/runtime-apis/request/), URL object, or URL string. Requests made through this method have `html_handling` and `not_found_handling` configuration applied to them. **Response** * `Promise` Returns a static asset response for the given request. **Example** Your dynamic code can make new, or forward incoming requests to your project's static assets using the assets binding. For example, `env.ASSETS.fetch(request)`, `env.ASSETS.fetch(new URL('https://assets.local/my-file'))` or `env.ASSETS.fetch('https://assets.local/my-file')`. The hostname used in the URL (for example, `assets.local`) is not meaningful — any valid hostname will work. Only the URL pathname is used to match assets. Note If you need to fetch assets from within an [RPC method](https://developers.cloudflare.com/workers/runtime-apis/rpc/#fetching-static-assets) (where there is no incoming `request`), construct a URL using any hostname — for example, `this.env.ASSETS.fetch(new Request('https://assets.local/path/to/asset'))`. Take the following example that configures a Worker script to return a response under all requests headed for `/api/`. Otherwise, the Worker script will pass the incoming request through to the asset binding. In this case, because a Worker script is only invoked when the requested route has not matched any static assets, this will always evaluate [not\_found\_handling](https://developers.cloudflare.com/workers/static-assets/#routing-behavior) behavior. ```js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { // TODO: Add your custom /api/* logic here. return new Response("Ok"); } // Passes the incoming request through to the assets binding. // No asset matched this request, so this will evaluate `not_found_handling` behavior. return env.ASSETS.fetch(request); }, }; ``` ```ts interface Env { ASSETS: Fetcher; } export default { async fetch(request, env): Promise { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { // TODO: Add your custom /api/* logic here. return new Response("Ok"); } // Passes the incoming request through to the assets binding. // No asset matched this request, so this will evaluate `not_found_handling` behavior. return env.ASSETS.fetch(request); }, } satisfies ExportedHandler; ``` ## Routing configuration For the various static asset routing configuration options, refer to [Routing](https://developers.cloudflare.com/workers/static-assets/routing/). ## Smart Placement [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) can be used to place a Worker's code close to your back-end infrastructure. Smart Placement will only have an effect if you specified a `main`, pointing to your Worker code. ### Smart Placement with Worker Code First If you desire to run your [Worker code ahead of assets](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/#run-your-worker-script-first) by setting `run_worker_first=true`, all requests must first travel to your Smart-Placed Worker. As a result, you may experience increased latency for asset requests. Use Smart Placement with `run_worker_first=true` when you need to integrate with other backend services, authenticate requests before serving any assets, or if you want to make modifications to your assets before serving them. If you want some assets served as quickly as possible to the user, but others to be served behind a smart-placed Worker, considering splitting your app into multiple Workers and [using service bindings to connect them](https://developers.cloudflare.com/workers/configuration/placement/#multiple-workers). ### Smart Placement with Assets First Enabling Smart Placement with `run_worker_first=false` (or not specifying it) lets you serve assets from as close as possible to your users, but moves your Worker logic to run most efficiently (such as near a database). Use Smart Placement with `run_worker_first=false` (or not specifying it) when prioritizing fast asset delivery. This will not impact the [default routing behavior](https://developers.cloudflare.com/workers/static-assets/#routing-behavior). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/binding/#page","headline":"Configuration and Bindings · Cloudflare Workers docs","description":"Details on how to configure Workers static assets and its binding.","url":"https://developers.cloudflare.com/workers/static-assets/binding/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Bindings"]} ``` --- --- description: Control headless browsers with Cloudflare's Workers Browser Run API. Automate tasks, take screenshots, convert pages to PDFs, and test web apps. title: Browser Run image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/browser-run/llms.txt > Use this file to discover all available pages before exploring further. # Browser Run Last updated Aug 11, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/browser-run/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Run headless Chrome on [Cloudflare's global network](https://developers.cloudflare.com/workers/) for browser automation, web scraping, testing, and content generation. Available on Free and Paid plans Browser Run, formerly known as Browser Rendering, enables developers to programmatically control and interact with headless browser instances running on Cloudflare’s global network. ## Use cases Programmatically load and fully render dynamic webpages or raw HTML and capture specific outputs such as: * [Markdown](https://developers.cloudflare.com/browser-run/quick-actions/markdown-endpoint/) * [Screenshots](https://developers.cloudflare.com/browser-run/quick-actions/screenshot-endpoint/) * [PDFs](https://developers.cloudflare.com/browser-run/quick-actions/pdf-endpoint/) * [Snapshots](https://developers.cloudflare.com/browser-run/quick-actions/snapshot/) * [Links](https://developers.cloudflare.com/browser-run/quick-actions/links-endpoint/) * [HTML elements](https://developers.cloudflare.com/browser-run/quick-actions/scrape-endpoint/) * [Structured data](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) * [Crawled web content](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/) ## Integration methods Browser Run offers two categories of integration methods: * **[Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/)**: Simple, stateless browser tasks like screenshots, PDFs, and scraping. No code deployment needed. * **Browser Sessions**: Direct browser control via [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [CDP](https://developers.cloudflare.com/browser-run/cdp/), or [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/). Deploy within Cloudflare Workers or connect from any environment via CDP. | Use case | Recommended | Why | | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | Simple screenshot, PDF, or scrape | [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) | No code deployment; single HTTP request | | Browser automation | [Playwright](https://developers.cloudflare.com/browser-run/playwright/), [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) | Full browser control with scripting | | Porting existing scripts | [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/), [Playwright](https://developers.cloudflare.com/browser-run/playwright/), or [CDP](https://developers.cloudflare.com/browser-run/cdp/) | Minimal code changes from standard libraries | | AI-powered data extraction | [JSON endpoint](https://developers.cloudflare.com/browser-run/quick-actions/json-endpoint/) | Structured data via natural language prompts | | Site-wide crawling | [Crawl endpoint](https://developers.cloudflare.com/browser-run/quick-actions/crawl-endpoint/) | Multi-page content extraction with async results | | AI agent browsing | [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) or [CDP with MCP clients](https://developers.cloudflare.com/browser-run/cdp/mcp-clients/) | LLMs control browsers via MCP | | Resilient scraping | [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/) | AI finds elements by intent, not selectors | | Direct browser control from any environment | [CDP](https://developers.cloudflare.com/browser-run/cdp/) | WebSocket access from local machines, CI/CD, or external servers | ## Key features * **Scale to thousands of browsers**: Instant access to a global pool of browsers with low cold-start time, ideal for high-volume screenshot generation, data extraction, or automation at scale * **Global by default**: Browser sessions run on Cloudflare's edge network, opening close to your users for better speed and availability worldwide * **Easy to integrate**: [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) for common tasks, [Puppeteer](https://developers.cloudflare.com/browser-run/puppeteer/) and [Playwright](https://developers.cloudflare.com/browser-run/playwright/) for complex workflows, and [CDP](https://developers.cloudflare.com/browser-run/cdp/) for direct browser control from any environment * **Session management**: [Reuse browser sessions](https://developers.cloudflare.com/browser-run/features/reuse-sessions/) across requests to improve performance and reduce cold-start overhead * **Flexible pricing**: Pay only for browser time used with generous free tier ([view pricing](https://developers.cloudflare.com/browser-run/pricing/)) ## Related products [Workers](https://developers.cloudflare.com/workers/) Build serverless applications and deploy instantly across the globe for exceptional performance, reliability, and scale. [Durable Objects](https://developers.cloudflare.com/durable-objects/) A globally distributed coordination API with strongly consistent storage. Using Durable Objects to [persist browser sessions](https://developers.cloudflare.com/browser-run/how-to/browser-run-with-do/) improves performance by eliminating the time that it takes to spin up a new browser session. [Agents](https://developers.cloudflare.com/agents/) Build AI-powered agents that autonomously navigate websites and perform tasks using [Playwright MCP](https://developers.cloudflare.com/browser-run/playwright/playwright-mcp/) or [Stagehand](https://developers.cloudflare.com/browser-run/stagehand/). ## More resources ### [Get started](https://developers.cloudflare.com/browser-run/get-started/) Choose an integration method and deploy your first project. ### [Limits](https://developers.cloudflare.com/browser-run/limits/) Learn about Browser Run limits. ### [Pricing](https://developers.cloudflare.com/browser-run/pricing/) Learn about Browser Run pricing. ### [Playwright API](https://developers.cloudflare.com/browser-run/playwright/) Use Cloudflare's fork of Playwright for testing and automation. ### [Developer Discord](https://discord.cloudflare.com) Connect with the Workers community on Discord to ask questions, show what you are building, and discuss the platform with other developers. ### [@CloudflareDev](https://x.com/cloudflaredev) Follow @CloudflareDev on Twitter to learn about product announcements, and what is new in Cloudflare Workers. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/browser-run/#page","headline":"Browser Run · Cloudflare Browser Run docs","description":"Control headless browsers with Cloudflare's Workers Browser Run API. Automate tasks, take screenshots, convert pages to PDFs, and test web apps.","url":"https://developers.cloudflare.com/browser-run/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-11","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Query D1 databases from a Cloudflare Worker using the D1 Binding API for prepared statements, batching, and type-safe results. title: Workers Binding API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/d1/llms.txt > Use this file to discover all available pages before exploring further. # Workers Binding API Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/d1/worker-api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can execute SQL queries on your D1 database from a Worker using the Worker Binding API. To do this, you can perform the following steps: 1. [Bind the D1 Database](https://developers.cloudflare.com/d1/get-started/#3-bind-your-worker-to-your-d1-database). 2. [Prepare a statement](https://developers.cloudflare.com/d1/worker-api/d1-database/#prepare). 3. [Run the prepared statement](https://developers.cloudflare.com/d1/worker-api/prepared-statements). 4. Analyze the [return object](https://developers.cloudflare.com/d1/worker-api/return-object) (if necessary). Refer to the relevant sections for the API documentation. ## TypeScript support D1 Worker Bindings API is fully-typed via the runtime types generated by running [wrangler types](https://developers.cloudflare.com/workers/languages/typescript/) package, and also supports [generic types ↗](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-types) as part of its TypeScript API. A generic type allows you to provide an optional `type parameter` so that a function understands the type of the data it is handling. When using the query statement methods [D1PreparedStatement::run](https://developers.cloudflare.com/d1/worker-api/prepared-statements/#run), [D1PreparedStatement::raw](https://developers.cloudflare.com/d1/worker-api/prepared-statements/#raw) and [D1PreparedStatement::first](https://developers.cloudflare.com/d1/worker-api/prepared-statements/#first), you can provide a type representing each database row. D1's API will [return the result object](https://developers.cloudflare.com/d1/worker-api/return-object/#d1result) with the correct type. For example, providing an `OrderRow` type as a type parameter to [D1PreparedStatement::run](https://developers.cloudflare.com/d1/worker-api/prepared-statements/#run) will return a typed `Array` object instead of the default `Record` type: ```ts // Row definition type OrderRow = { Id: string; CustomerName: string; OrderDate: number; }; // Elsewhere in your application // env.MY_DB is the D1 database binding from your Wrangler configuration file const result = await env.MY_DB.prepare( "SELECT Id, CustomerName, OrderDate FROM [Order] ORDER BY ShippedDate DESC LIMIT 100", ).run(); ``` ## Type conversion D1 automatically converts supported JavaScript (including TypeScript) types passed as parameters via the Workers Binding API to their associated D1 types 1. This conversion is permanent and one-way only. This means that when reading the written values back in your code, you will get the converted values rather than the originally inserted values. Note We recommend using [STRICT tables ↗](https://www.sqlite.org/stricttables.html) in your SQL schema to avoid issues with mismatched types between values that are actually stored in your database compared to values defined by your schema. The type conversion during writes is as follows: | JavaScript (write) | D1 | JavaScript (read) | | ------------------ | ---------------- | ----------------- | | null | NULL | null | | Number | REAL | Number | | Number 2 | INTEGER | Number | | String | TEXT | String | | Boolean 3 | INTEGER | Number (0,1) | | ArrayBuffer | BLOB | Array 4 | | ArrayBuffer View | BLOB | Array 4 | | undefined | Not supported. 5 | \- | 1 D1 types correspond to the underlying [SQLite types ↗](https://www.sqlite.org/datatype3.html). 2 D1 supports 64-bit signed `INTEGER` values internally, however [BigInts ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/BigInt)are not currently supported in the API yet. JavaScript integers are safe up to [Number.MAX\_SAFE\_INTEGER ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Number/MAX%5FSAFE%5FINTEGER). 3 Booleans will be cast to an `INTEGER` type where `1` is `TRUE` and `0` is `FALSE`. 4 `ArrayBuffer` and [ArrayBufferviews ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/ArrayBuffer/isView)are converted using [Array.from ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Array/from). 5 Queries with `undefined` values will return a `D1_TYPE_ERROR`. ## API playground The D1 Worker Binding API playground is an `index.js` file where you can test each of the documented Worker Binding APIs for D1\. The file builds from the end-state of the [Get started](https://developers.cloudflare.com/d1/get-started/#write-queries-within-your-worker) code. You can use this alongside the API documentation to better understand how each API works. Follow the steps to setup your API playground. ### 1\. Complete the Get started tutorial Complete the [Get started](https://developers.cloudflare.com/d1/get-started/#write-queries-within-your-worker) tutorial. Ensure you use JavaScript instead of TypeScript. ### 2\. Modify the content of `index.js` Replace the contents of your `index.js` file with the code below to view the effect of each API. index.js ```js // D1 API Playground - Test each D1 Worker Binding API method // Change the URL pathname to test different methods (e.g., /RUN, /RAW, /FIRST) export default { async fetch(request, env) { const { pathname } = new URL(request.url); // Sample data for testing const companyName1 = `Bs Beverages`; const companyName2 = `Around the Horn`; // Prepare reusable statements const stmt = env.DB.prepare(`SELECT * FROM Customers WHERE CompanyName = ?`); const stmtMulti = env.DB.prepare(`SELECT * FROM Customers; SELECT * FROM Customers WHERE CompanyName = ?`); const session = env.DB.withSession("first-primary") const sessionStmt = session.prepare(`SELECT * FROM Customers WHERE CompanyName = ?`); // Test D1PreparedStatement::run - returns full D1Result object if (pathname === `/RUN`){ const returnValue = await stmt.bind(companyName1).run(); return Response.json(returnValue); // Test D1PreparedStatement::raw - returns array of arrays } else if (pathname === `/RAW`){ const returnValue = await stmt.bind(companyName1).raw(); return Response.json(returnValue); // Test D1PreparedStatement::first - returns first row only } else if (pathname === `/FIRST`){ const returnValue = await stmt.bind(companyName1).first(); return Response.json(returnValue); // Test D1Database::batch - execute multiple statements } else if (pathname === `/BATCH`) { const batchResult = await env.DB.batch([ stmt.bind(companyName1), stmt.bind(companyName2) ]); return Response.json(batchResult); // Test D1Database::exec - execute raw SQL without parameters } else if (pathname === `/EXEC`){ const returnValue = await env.DB.exec(`SELECT * FROM Customers WHERE CompanyName = "Bs Beverages"`); return Response.json(returnValue); // Test D1 Sessions API with read replication } else if (pathname === `/WITHSESSION`){ const returnValue = await sessionStmt.bind(companyName1).run(); console.log("You're now using D1 Sessions!") return Response.json(returnValue); } // Default response with instructions return new Response( `Welcome to the D1 API Playground! \nChange the URL to test the various methods inside your index.js file.`, ); }, }; ``` ### 3\. Deploy the Worker 1. Navigate to your tutorial directory you created by following step 1. 2. Run `npx wrangler deploy`. ```sh npx wrangler deploy ``` ```sh ⛅️ wrangler 3.112.0 -------------------- Total Upload: 1.90 KiB / gzip: 0.59 KiB Your worker has access to the following bindings: - D1 Databases: - DB: DATABASE_NAME () Uploaded WORKER_NAME (7.01 sec) Deployed WORKER_NAME triggers (1.25 sec) https://jun-d1-rr.d1-sandbox.workers.dev Current Version ID: VERSION_ID ``` 3. Open a browser at the specified address. ### 4\. Test the APIs Change the URL to test the various D1 Worker Binding APIs. ```plaintext ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/d1/worker-api/#page","headline":"Workers Binding API · Cloudflare D1 docs","description":"Query D1 databases from a Cloudflare Worker using the D1 Binding API for prepared statements, batching, and type-safe results.","url":"https://developers.cloudflare.com/d1/worker-api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Create a dynamic dispatch Worker to route incoming requests to user Workers in your dispatch namespace. title: Dynamic dispatch Worker image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/cloudflare-for-platforms/llms.txt > Use this file to discover all available pages before exploring further. # Dynamic dispatch Worker Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) A [dynamic dispatch Worker](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/how-workers-for-platforms-works/#dynamic-dispatch-worker) is a specialized routing Worker that directs incoming requests to the appropriate user Workers in your dispatch namespace. Instead of using [Workers Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/), dispatch Workers let you programmatically control request routing through code. ![Figure 1: Workers for Platforms: Main Flow](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1200,height=466,format=svg/_astro/programmable-platforms-1.BCCEhzLr.svg) Note You can also create a dispatch Worker from the Cloudflare dashboard. Go to **Workers for Platforms**, select your namespace, and click **Create** \> **Dispatch Worker**. The dashboard provides templates for path-based and subdomain-based routing. #### Why use a dynamic dispatch Worker? * **Scale**: Route requests to millions of hostnames to different Workers, without defining [Workers Routes](https://developers.cloudflare.com/workers/configuration/routing/routes/) configuration for each one * **Custom routing logic**: Write code to determine exactly how requests should be routed. For example: * Store hostname-to-Worker mappings in [Workers KV](https://developers.cloudflare.com/kv/) and look them up dynamically * Route requests based on subdomain, path, headers, or other request properties * Use [custom metadata](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata/) attached to [custom hostnames](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/) for routing decisions * **Add platform functionality**: Build additional features at the routing layer: * Run authentication checks before requests reach user Workers * Remove or add headers or metadata from incoming requests * Attach useful context like user IDs or account information * Transform requests or responses as needed ### Configure the dispatch namespace binding To allow your dynamic dispatch Worker to dynamically route requests to Workers in a namespace, you need to configure a dispatch namespace [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/). This binding enables your dynamic dispatch Worker to call any user Worker within that namespace using `env.dispatcher.get()`. ```jsonc { "dispatch_namespaces": [ { "binding": "DISPATCHER", "namespace": "my-dispatch-namespace" } ] } ``` ```toml [[dispatch_namespaces]] binding = "DISPATCHER" namespace = "my-dispatch-namespace" ``` Once the binding is configured, your dynamic dispatch Worker can route requests to any Worker in the namespace. Below are common routing patterns you can implement in your dispatcher. ### Routing examples ![Figure 2: Workers for Platforms: Main Flow](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1200,height=594,format=svg/_astro/programmable-platforms-2.DGAT6ZDR.svg) #### KV-Based Routing Store the routing mappings in [Workers KV](https://developers.cloudflare.com/kv/). This allows you to modify your routing logic without requiring you to change or redeploy the dynamic dispatch Worker. ```js export default { async fetch(request, env) { try { const url = new URL(request.url); // Use hostname, path, or any combination as the routing key const routingKey = url.hostname; // Lookup user Worker name from KV store const userWorkerName = await env.USER_ROUTING.get(routingKey); if (!userWorkerName) { return new Response("Route not configured", { status: 404 }); } // Optional: Cache the KV lookup result const userWorker = env.DISPATCHER.get(userWorkerName); return await userWorker.fetch(request); } catch (e) { if (e.message.startsWith("Worker not found")) { return new Response("", { status: 404 }); } return new Response(e.message, { status: 500 }); } }, }; ``` #### Subdomain-Based Routing Route subdomains to the corresponding Worker. For example, `my-customer.example.com` will route to the Worker named `my-customer` in the dispatch namespace. ```js export default { async fetch(request, env) { try { // Extract user Worker name from subdomain // Example: customer1.example.com -> customer1 const url = new URL(request.url); const userWorkerName = url.hostname.split(".")[0]; // Get user Worker from dispatch namespace const userWorker = env.DISPATCHER.get(userWorkerName); return await userWorker.fetch(request); } catch (e) { if (e.message.startsWith("Worker not found")) { // User Worker doesn't exist in dispatch namespace return new Response("", { status: 404 }); } // Could be any other exception from fetch() or from the dispatched Worker return new Response(e.message, { status: 500 }); } }, }; ``` #### Path-Based routing Route URL paths to the corresponding Worker. For example, `example.com/customer-1` will route to the Worker named `customer-1` in the dispatch namespace. ```js export default { async fetch(request, env) { try { const url = new URL(request.url); const pathParts = url.pathname.split("/").filter(Boolean); if (pathParts.length === 0) { return new Response("Invalid path", { status: 400 }); } // example.com/customer-1 -> routes to 'customer-1' worker const userWorkerName = pathParts[0]; const userWorker = env.DISPATCHER.get(userWorkerName); return await userWorker.fetch(request); } catch (e) { if (e.message.startsWith("Worker not found")) { return new Response("", { status: 404 }); } return new Response(e.message, { status: 500 }); } }, }; ``` ### Enforce custom limits Use [custom limits](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/custom-limits/) to control how much CPU time a given user Worker can use, or how many subrequests it can make. You can set different limits based on customer plan type or other criteria. ```js export default { async fetch(request, env) { try { const url = new URL(request.url); const userWorkerName = url.hostname.split(".")[0]; // Look up customer plan from your database or KV const customerPlan = await env.CUSTOMERS.get(userWorkerName); // Set limits based on plan type const plans = { enterprise: { cpuMs: 50, subRequests: 50 }, pro: { cpuMs: 20, subRequests: 20 }, free: { cpuMs: 10, subRequests: 5 }, }; const limits = plans[customerPlan] || plans.free; const userWorker = env.DISPATCHER.get(userWorkerName, {}, { limits }); return await userWorker.fetch(request); } catch (e) { if (e.message.startsWith("Worker not found")) { return new Response("", { status: 404 }); } if (e.message.includes("CPU time limit")) { // Track limit violations with Analytics Engine env.ANALYTICS.writeDataPoint({ indexes: [userWorkerName], blobs: ["cpu_limit_exceeded"], }); return new Response("CPU limit exceeded", { status: 429 }); } return new Response(e.message, { status: 500 }); } }, }; ``` For more details on available limits, refer to [Custom limits](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/custom-limits/). To track limit violations and other metrics across user Workers, use [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/). For detailed logging and debugging, configure a [Tail Worker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) to capture events from your dispatch Worker. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/#page","headline":"Dynamic dispatch Worker · Cloudflare for Platforms docs","description":"Create a dynamic dispatch Worker to route incoming requests to user Workers in your dispatch namespace.","url":"https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/configuration/dynamic-dispatch/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Reference documentation for Durable Objects binding APIs, including namespaces, stubs, state, and storage. title: Workers Binding API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/durable-objects/llms.txt > Use this file to discover all available pages before exploring further. # Workers Binding API Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/durable-objects/api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) * [Durable Object Base Class](https://developers.cloudflare.com/durable-objects/api/base/) * [Durable Object Container](https://developers.cloudflare.com/durable-objects/api/container/) * [Durable Object Namespace](https://developers.cloudflare.com/durable-objects/api/namespace/) * [Durable Object ID](https://developers.cloudflare.com/durable-objects/api/id/) * [Durable Object Stub](https://developers.cloudflare.com/durable-objects/api/stub/) * [Durable Object State](https://developers.cloudflare.com/durable-objects/api/state/) * [SQLite-backed Durable Object Storage](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) * [KV-backed Durable Object Storage (Legacy)](https://developers.cloudflare.com/durable-objects/api/legacy-kv-storage-api/) * [Alarms](https://developers.cloudflare.com/durable-objects/api/alarms/) * [WebGPU](https://developers.cloudflare.com/durable-objects/api/webgpu/) * [Rust API](https://github.com/cloudflare/workers-rs?tab=readme-ov-file#durable-objects) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/durable-objects/api/#page","headline":"Workers Binding API · Cloudflare Durable Objects docs","description":"Reference documentation for Durable Objects binding APIs, including namespaces, stubs, state, and storage.","url":"https://developers.cloudflare.com/durable-objects/api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: You can add environment variables, which are a type of binding, to attach text strings or JSON values to your Worker. title: Environment variables image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Environment variables Last updated Jun 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/environment-variables/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background You can add environment variables, which are a type of binding, to attach text strings or JSON values to your Worker. Environment variables are available on the [env parameter](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#parameters) passed to your Worker's [fetch event handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). Text strings and JSON values are not encrypted and are useful for storing application configuration. ## Add environment variables via Wrangler To add env variables using Wrangler, define text and JSON via the `[vars]` configuration in your Wrangler file. In the following example, `API_HOST` and `API_ACCOUNT_ID` are text values and `SERVICE_X_DATA` is a JSON value. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker-dev", "vars": { "API_HOST": "example.com", "API_ACCOUNT_ID": "example_user", "SERVICE_X_DATA": { "URL": "service-x-api.dev.example", "MY_ID": 123 } } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker-dev" [vars] API_HOST = "example.com" API_ACCOUNT_ID = "example_user" [vars.SERVICE_X_DATA] URL = "service-x-api.dev.example" MY_ID = 123 ``` Refer to the following example on how to access the `API_HOST` environment variable in your Worker code: ```js export default { async fetch(request, env, ctx) { return new Response(`API host: ${env.API_HOST}`); }, }; ``` ```ts export interface Env { API_HOST: string; } export default { async fetch(request, env, ctx): Promise { return new Response(`API host: ${env.API_HOST}`); }, } satisfies ExportedHandler; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): # Environment variables are accessed via attribute access on self.env return Response(f"API host: {self.env.API_HOST}") ``` ### Import `env` for global access You can also import `env` from [cloudflare:workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) to access environment variables from anywhere in your code, including outside of request handlers: ```js import { env } from "cloudflare:workers"; // Access environment variables at the top level const apiHost = env.API_HOST; export default { async fetch(request) { return new Response(`API host: ${apiHost}`); }, }; ``` ```ts import { env } from "cloudflare:workers"; // Access environment variables at the top level const apiHost = env.API_HOST; export default { async fetch(request: Request): Promise { return new Response(`API host: ${apiHost}`); }, }; ``` This approach is useful when you need to: * Initialize configuration or API clients at the top level of your Worker. * Access environment variables from deeply nested functions without passing `env` through every function call. For more details, refer to [Importing env as a global](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global). ### Configuring different environments in Wrangler [Environments in Wrangler](https://developers.cloudflare.com/workers/wrangler/environments) let you specify different configurations for the same Worker, including different values for `vars` in each environment. As `vars` is a [non-inheritable key](https://developers.cloudflare.com/workers/wrangler/configuration/#non-inheritable-keys), they are not inherited by environments and must be specified for each environment. The example below sets up two environments, `staging` and `production`, with different values for `API_HOST`. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker-dev", // top level environment "vars": { "API_HOST": "api.example.com" }, "env": { "staging": { "vars": { "API_HOST": "staging.example.com" } }, "production": { "vars": { "API_HOST": "production.example.com" } } } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker-dev" [vars] API_HOST = "api.example.com" [env.staging.vars] API_HOST = "staging.example.com" [env.production.vars] API_HOST = "production.example.com" ``` To run Wrangler commands in specific environments, you can pass in the `--env` or `-e` flag. For example, you can develop the Worker in an environment called `staging` by running `npx wrangler dev --env staging`, and deploy it with `npx wrangler deploy --env staging`. Learn about [environments in Wrangler](https://developers.cloudflare.com/workers/wrangler/environments). ## Add environment variables via the dashboard To add environment variables via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker. 3. Select **Settings**. 4. Under **Variables and Secrets**, select **Add**. 5. Select a **Type**, input a **Variable name**, and input its **Value**. This variable will be made available to your Worker. 6. (Optional) To add multiple environment variables, select **Add variable**. 7. Select **Deploy** to implement your changes. Plaintext strings and secrets Select the **Secret** type if your environment variable is a [secret](https://developers.cloudflare.com/workers/configuration/secrets/). Alternatively, consider [Cloudflare Secrets Store](https://developers.cloudflare.com/secrets-store/), for account-level secrets. ## Compare secrets and environment variables Use secrets for sensitive information Do not use plaintext environment variables to store sensitive information. Use [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) or [Secrets Store bindings](https://developers.cloudflare.com/secrets-store/integrations/workers/) instead. [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) are [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). The difference is secret values are not visible within Wrangler or Cloudflare dashboard after you define them. This means that sensitive data, including passwords or API tokens, should always be encrypted to prevent data leaks. To your Worker, there is no difference between an environment variable and a secret. The secret's value is passed through as defined. ### Local development with secrets Caution Do not use `vars` to store sensitive information in your Worker's Wrangler configuration file. Use secrets instead. Put secrets for use in local development in either a `.dev.vars` file or a `.env` file, in the same directory as the Wrangler configuration file. Note You can use the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) to declare which secret names your Worker requires. When defined, only the keys listed in `secrets.required` are loaded from `.dev.vars` or `.env`. Additional keys are excluded and missing keys produce a warning. Note Choose to use either `.dev.vars` or `.env` but not both. If you define a `.dev.vars` file, then values in `.env` files will not be included in the `env` object during local development. These files should be formatted using the [dotenv ↗](https://hexdocs.pm/dotenvy/dotenv-file-format.html) syntax. For example: ```bash SECRET_KEY="value" API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ``` Do not commit secrets to git The `.dev.vars` and `.env` files should not be committed to git. Add `.dev.vars*` and `.env*` to your project's `.gitignore` file. To set different secrets for each Cloudflare environment, create files named `.dev.vars.` or `.env.`. When you select a Cloudflare environment in your local development, the corresponding environment-specific file will be loaded ahead of the generic `.dev.vars` (or `.env`) file. * When using `.dev.vars.` files, all secrets must be defined per environment. If `.dev.vars.` exists then only this will be loaded; the `.dev.vars` file will not be loaded. * In contrast, all matching `.env` files are loaded and the values are merged. For each variable, the value from the most specific file is used, with the following precedence: * `.env..local` (most specific) * `.env.local` * `.env.` * `.env` (least specific) Controlling \`.env\` handling It is possible to control how `.env` files are loaded in local development by setting environment variables on the process running the tools. * To disable loading local dev vars from `.env` files without providing a `.dev.vars` file, set the `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV` environment variable to `"false"`. * To include every environment variable defined in your system's process environment as a local development variable, ensure there is no `.dev.vars` and then set the `CLOUDFLARE_INCLUDE_PROCESS_ENV` environment variable to `"true"`. This is not needed when using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property), which loads from `process.env` automatically. ## Environment variables and Node.js compatibility When you enable [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) and the [nodejs\_compat\_populate\_process\_env](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs%5Fcompat%5Fpopulate%5Fprocess%5Fenv) compatibility flag (enabled by default for compatibility dates on or after 2025-04-01), environment variables are available via the global `process.env`. The `process.env` will be populated lazily the first time that `process` is accessed in the worker. Text variable values are exposed directly. JSON variable values that evaluate to string values are exposed as the parsed value. JSON variable values that do not evaluate to string values are exposed as the raw JSON string. For example, imagine a Worker with three environment variables, two text values, and one JSON value: ```plaintext [vars] FOO = "abc" BAR = "abc" BAZ = { "a": 123 } ``` Environment variables can be added using either the `wrangler.{json|jsonc|toml}` file or via the Cloudflare dashboard UI. The values of `process.env.FOO` and `process.env.BAR` will each be the JavaScript string `"abc"`. The value of `process.env.BAZ` will be the JSON-encoded string `"{ \"a\": 123 }"`. :::note Note also that because secrets are a form of environment variable within the runtime, secrets are also exposed via `process.env`. ## Related resources * Migrating environment variables from [Service Worker format to ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/#environment-variables). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/environment-variables/#page","headline":"Environment variables · Cloudflare Workers docs","description":"You can add environment variables, which are a type of binding, to attach text strings or JSON values to your Worker.","url":"https://developers.cloudflare.com/workers/configuration/environment-variables/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Images binding to optimize, resize, and manipulate images directly in a Worker from any source. title: Optimize with Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/images/llms.txt > Use this file to discover all available pages before exploring further. # Optimize with Workers Last updated Jul 8, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/images/optimization/binding/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) A [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) connects your [Worker](https://developers.cloudflare.com/workers/) to external resources on the Developer Platform, like [Images](https://developers.cloudflare.com/images/), [R2 buckets](https://developers.cloudflare.com/r2/buckets/), or [KV namespaces](https://developers.cloudflare.com/kv/concepts/kv-namespaces/). The Images binding lets you optimize and manipulate images directly in a Worker. Unlike the [URL interface](https://developers.cloudflare.com/images/optimization/transformations/overview/), which requires images to be accessible through a URL, the binding works with raw image bytes. You can pass images from any source, including [Images](https://developers.cloudflare.com/images/storage/upload-images/methods/), [R2](https://developers.cloudflare.com/r2/), a `fetch()` response, or a request body. With the Images binding, you can: * Optimize an image stored in Images or R2 by passing the bytes directly, instead of fetching through a public URL. * Resize an image, overlay a watermark, then resize the combined output into a final result — all in a single chain of operations. * Control the order of operations for optimization parameters. When you use the URL interface, the optimization parameters follow a fixed order of operations. Bindings can be configured in the Cloudflare dashboard for your Worker or in the Wrangler configuration file in your project's directory. Billing Calls to the Images binding are billed as [unique transformations](https://developers.cloudflare.com/images/pricing/#images-transformed): each unique combination of source image and parameters is billed only once per calendar month, and repeat requests within the same month do not incur additional usage. Calls to [.info()](#infostream) are free. Refer to [Images pricing](https://developers.cloudflare.com/images/pricing/) for more information about billing. ## Setup The Images binding is enabled on a per-Worker basis. You can define variables in the Wrangler configuration file of your Worker project's directory. These variables are bound to external resources at runtime, and you can then interact with them through this variable. To bind Images to your Worker, add the following to the end of your Wrangler configuration file: ```jsonc { "images": { "binding": "IMAGES", // i.e. available in your Worker on env.IMAGES }, } ``` ```toml [images] binding = "IMAGES" ``` Within your Worker code, use `env.IMAGES.input()` to build an object that can manipulate the image (passed as a `ReadableStream`). ## Methods Enable caching Responses from the Images binding are not automatically cached. Every uncached call performs a full decode and re-encode of the source image, which adds unnecessary latency to every request. We strongly recommend enabling [Workers Cache](https://developers.cloudflare.com/workers/cache/) so that repeat requests are served from cache without re-running your Worker or re-transforming the image. Add the following to your Wrangler configuration file: ```jsonc { "cache": { "enabled": true, }, } ``` ```toml [cache] enabled = true ``` Then set `Cache-Control` headers on your response to control how long transformed images are cached: ```js const response = ( await env.IMAGES.input(stream) .transform({ width: 800 }) .output({ format: "image/webp" }) ).response(); return new Response(response.body, { headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400", }, }); ``` ```ts const response = ( await env.IMAGES.input(stream) .transform({ width: 800 }) .output({ format: "image/webp" }) ).response(); return new Response(response.body, { headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=3600, stale-while-revalidate=86400", }, }); ``` ### `.input(stream)` Creates an optimization handle for an image. All operations begin with this method. Accepts image bytes up to 20 MB from any source, including [Images](https://developers.cloudflare.com/images/storage/upload-images/methods/), [R2](https://developers.cloudflare.com/r2/), a `fetch()` response, or a request body. Returns a handle that you can use to chain `.transform()`, `.draw()`, and `.output()` calls. ```js export default { async fetch(request, env) { const imageURL = "https://example.com/photo.jpg"; const response = await fetch(imageURL); return ( await env.IMAGES.input(response.body) .transform({ width: 800 }) .output({ format: "image/webp" }) ).response(); }, }; ``` ```ts export default { async fetch(request, env) { const imageURL = "https://example.com/photo.jpg"; const response = await fetch(imageURL); return ( await env.IMAGES .input(response.body) .transform({ width: 800 }) .output({ format: "image/webp" }) ).response(); }, }; ``` ### `.transform(options)` Applies optimization parameters to the image, such as `width`, `height`, or `blur`. You can chain multiple `.transform()` calls to control the order that parameters will be applied. For the full list of parameters, refer to [Features](https://developers.cloudflare.com/images/optimization/features/). The example below shows how you can resize an image that is [stored in Images](https://developers.cloudflare.com/images/storage/binding/) by getting the image bytes: ```js // Get the raw bytes of a hosted image const bytes = await env.IMAGES.hosted.image("IMAGE_ID").bytes(); // Resize and transcode the image const response = ( await env.IMAGES.input(bytes) .transform({ width: 400 }) .output({ format: "image/webp" }) ).response(); return response; ``` ```ts // Get the raw bytes of a hosted image const bytes = await env.IMAGES.hosted.image("IMAGE_ID").bytes(); // Resize and transcode the image const response = ( await env.IMAGES.input(bytes) .transform({ width: 400 }) .output({ format: "image/webp" }) ).response(); return response; ``` ### `.draw(image, options)` Draws an overlay image over another image. The overlay can be a stream of image bytes or another `.input()` chain. You can pass a child `.transform()` function inside this method to resize or manipulate the overlay before drawing it. Accepts `opacity`, `repeat`, a side (`left`, `right`, `top`, `bottom`), and `composite`. For the full list of draw options and examples, refer to [Draw overlays and watermarks](https://developers.cloudflare.com/images/optimization/draw-overlays/#options). ### `.output(options)` Generates the final image with the specified output options. Accepts the following options: * `format` — Encodes the image in [a supported format](https://developers.cloudflare.com/images/get-started/limits/#output-formats), such as AVIF, WebP, or JPEG. This method is required — there is no default output format. * `quality` — Specifies the output [quality](https://developers.cloudflare.com/images/optimization/features/#quality--q) of an image for JPEG, WebP, and AVIF formats, expressed as a fixed value or perceptual quality level. * `anim` — Specifies whether to [preserve animation frames](https://developers.cloudflare.com/images/optimization/features/#anim) from input files. Set `anim:false` to convert animations to still images. ```js const response = ( await env.IMAGES.input(stream) .transform({ rotate: 90 }) .transform({ width: 128 }) .transform({ blur: 20 }) .output({ format: "image/avif" }) ).response(); return response; ``` ```ts const response = ( await env.IMAGES.input(stream) .transform({ rotate: 90 }) .transform({ width: 128 }) .transform({ blur: 20 }) .output({ format: "image/avif" }) ).response(); return response; ``` ### `.info(stream)` Outputs information about the image, such as `format`, `fileSize`, `width`, and `height`. ## Interact with your Images binding locally The Images API can be used in local development through [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), the command-line interface for Workers. Using the Images binding in local development will not incur usage charges. Wrangler supports two different versions of the Images API: * A high-fidelity version that supports all features that are available through the Images API. This is the same version that Cloudflare runs globally in production. * A low-fidelity offline version that supports only a subset of features, such as resizing and rotation. To test the low-fidelity version of Images, you can run `wrangler dev`: ```txt npx wrangler dev ``` Currently, this version supports only `width`, `height`, `rotate`, and `format`. To test the high-fidelity remote version of Images, you can use the `--remote` flag: ```txt npx wrangler dev --remote ``` When testing with the [Workers Vitest integration](https://developers.cloudflare.com/workers/testing/vitest-integration/), the low-fidelity offline version is used by default, to avoid hitting the Cloudflare API in tests. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/images/optimization/binding/#page","headline":"Optimize with Workers · Cloudflare Images docs","description":"Use the Images binding to optimize, resize, and manipulate images directly in a Worker from any source.","url":"https://developers.cloudflare.com/images/optimization/binding/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-08","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Workers KV binding API to read, write, delete, and list key-value pairs from your Worker code. title: Workers Binding API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/kv/llms.txt > Use this file to discover all available pages before exploring further. # Workers Binding API Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/kv/api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) * [Read key-value pairs](https://developers.cloudflare.com/kv/api/read-key-value-pairs/) * [Write key-value pairs](https://developers.cloudflare.com/kv/api/write-key-value-pairs/) * [Delete key-value pairs](https://developers.cloudflare.com/kv/api/delete-key-value-pairs/) * [List keys](https://developers.cloudflare.com/kv/api/list-keys/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/kv/api/#page","headline":"Workers Binding API · Cloudflare Workers KV docs","description":"Use the Workers KV binding API to read, write, delete, and list key-value pairs from your Worker code.","url":"https://developers.cloudflare.com/kv/api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Bind the Media Transformations API to a Cloudflare Worker to transform videos programmatically. title: Bind to Workers API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/stream/llms.txt > Use this file to discover all available pages before exploring further. # Bind to Workers API Last updated Jun 10, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/stream/transform-videos/bindings/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) A [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) connects your [Worker](https://developers.cloudflare.com/workers/) to external resources on the Developer Platform, like [Media Transformations](https://developers.cloudflare.com/stream/transform-videos/), [R2 buckets](https://developers.cloudflare.com/r2/buckets/), or [KV namespaces](https://developers.cloudflare.com/kv/concepts/kv-namespaces/). You can bind the Media Transformations API to your Worker to transform, resize, and extract content from videos without requiring them to be accessible through a URL. For example, when you use Media Transformations within Workers, you can: * Transform a video stored in a private R2 bucket or other protected source * Optimize videos and store the output directly back in R2 without serving it to the browser * Extract still frames and spritesheets for videos and use them for classification or description with [Workers AI](https://developers.cloudflare.com/workers-ai/) * Extract audio tracks from video files for dynamic transcription using Workers AI Beta restrictions The binding for Media Transformations is currently in public open beta. During this period: * Transformation operations via the binding will not be billed. * Certain operations may incur higher latency. ## Setup The Media binding is enabled on a per-Worker basis. [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) can be configured in the Cloudflare dashboard for your Worker or in the Wrangler configuration file in your project directory. To bind Media Transformations to your Worker, add the following to the end of your Wrangler configuration file: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "media": { "binding": "MEDIA" } } ``` ```toml [media] binding = "MEDIA" # available in your Worker on env.MEDIA ``` Within your Worker code, you can interact with this binding by using `env.MEDIA.input()` to build an object that can manipulate the video (passed as a `ReadableStream`). ## Methods The Media Transformations binding is similar to the [Images binding](https://developers.cloudflare.com/images/optimization/binding/), except the method chain order is fixed and the result of an `input()` cannot be reused across multiple transformations. ### `.input()` The starting point for the Media binding, which accepts raw content. * Accepts a `ReadableStream` containing the video bytes. ### `.transform()` (optional) Defines how the video input should be transformed by resizing or cropping. This method is optional — if you do not need to resize or crop, you can call `.output()` directly on the result of `.input()`. * Accepts the following parameters (all optional): * `width`: Target width in pixels (10-2000). * `height`: Target height in pixels (10-2000). * `fit`: How to resize the video to fit the specified dimensions. * `contain`: Scales the video to fit entirely within the output dimensions while respecting aspect ratio. * `cover`: Scales the video to entirely cover the output dimensions with a center-weighted crop. * `scale-down`: Same as `contain`, but only scales down. Does not upscale. * Refer to [Transform videos options](https://developers.cloudflare.com/stream/transform-videos/#options) for more details. ### `.output()` Defines what to extract from the video and how the output will be formatted. Refer to [source video requirements](https://developers.cloudflare.com/stream/transform-videos/#source-video-requirements) and [limitations](https://developers.cloudflare.com/stream/transform-videos/#limitations) for input and output constraints. * Accepts the following parameters: * `mode`: The type of output to generate. * `video`: Outputs an H.264/AAC optimized MP4 file. * `frame`: Outputs a still image (JPEG or PNG). * `spritesheet`: Outputs a JPEG containing multiple frames. * `audio`: Outputs an AAC encoded M4A file. * `time`: Start timestamp for extraction (for example, `"2s"`, `"1m"`). Default: `"0s"`. * `duration`: Duration of the output for `video`, `audio`, or `spritesheet` modes (for example, `"5s"`). * `imageCount`: Number of frames to include in a spritesheet. * `format`: Output format for `frame` mode (`jpg`, `png`) or `audio` mode (`m4a`). * `audio`: Boolean to include or exclude audio in `video` mode. Default: `true`. ### Result methods Finally, after configuring the output, three methods are available to receive results. These methods return Promises and must be awaited: * `.response()`: Returns a `Promise` — the transformed media as an HTTP Response object, ready to return to the client or store in cache. * `.media()`: Returns a `Promise>` — the transformed media as a byte stream. * `.contentType()`: Returns a `Promise` — the MIME type of the output (for example, `video/mp4`, `image/jpeg`, `audio/mp4`). ## Examples ### Generate an optimized video clip Resize a video and extract a five-second clip: ```ts export default { async fetch(request, env) { const video = await env.R2_BUCKET.get("input.mp4"); const result = env.MEDIA.input(video.body) .transform({ width: 480, height: 270 }) .output({ mode: "video", time: "0s", duration: "5s" }); return await result.response(); }, }; ``` ### Extract a still frame Extract a single frame as a JPEG thumbnail: ```ts export default { async fetch(request, env) { const video = await env.R2_BUCKET.get("input.mp4"); const result = env.MEDIA.input(video.body) .transform({ width: 640, height: 360 }) .output({ mode: "frame", time: "2s", format: "jpg" }); return await result.response(); }, }; ``` #### Identify content with Media Transformations and Workers AI Extract a frame (still image) from a video, then use a model like [UForm-Gen on Workers AI](https://developers.cloudflare.com/workers-ai/models/uform-gen2-qwen-500m/) to generate a caption. ```ts export default { async fetch(request, env) { // First, load the video file from a source like R2 (or a fetch) // Loading from R2 const video = await env.R2_BUCKET.get("input.mp4"); // Or using a fetch: // const video = await fetch('https://example.com/video.mp4'); // Isolate a frame (still image) const frame = await env.MEDIA.input(video.body) .transform({ width: 720 }) .output({ mode: 'frame', time: '3s', }) .response(); // Set up the payload for Workers AI const payload = { image: [...new Uint8Array(await frame.arrayBuffer())], prompt: "Generate a caption for this image", max_tokens: 512, }; const response = await env.AI.run( "@cf/unum/uform-gen2-qwen-500m", payload ); return new Response(JSON.stringify(response)); } } ``` ### Extract audio Extract the audio track from a video as an M4A file. This example demonstrates skipping `.transform()` since no resizing is needed: ```ts export default { async fetch(request, env) { const video = await env.R2_BUCKET.get("input.mp4"); const result = env.MEDIA.input(video.body).output({ mode: "audio", time: "0s", duration: "30s", }); return await result.response(); }, }; ``` #### Transcribe audio with Media Transformations and Workers AI Extract audio, then transcribe using [Whisper on Workers AI](https://developers.cloudflare.com/workers-ai/models/whisper/). ```ts export default { async fetch(request, env) { // First, load the video file from a source like R2 (or a fetch) // Loading from R2 const video = await env.R2_BUCKET.get("input.mp4"); // Or using a fetch: // const video = await fetch('https://example.com/video.mp4'); // Extract audio using the media transformations binding: const audio = await env.MEDIA.input(video.body) .transform() .output({ mode: 'audio', }) .response(); // Prepare and run Workers AI inference const payload = { audio: [...new Uint8Array(await audio.arrayBuffer())], }; const response = await env.AI.run( "@cf/openai/whisper", payload ); // response will have props {text, word_count, vtt, words} return new Response( JSON.stringify(response, null, 2), { headers: {'Content-Type': 'application/json'} } ); } } ``` ### Store transformed output in R2 Transform a video and store the result directly in R2: ```ts export default { async fetch(request, env) { const video = await env.R2_BUCKET.get("input.mp4"); const result = env.MEDIA.input(video.body) .transform({ width: 480, height: 270, fit: "contain" }) .output({ mode: "video", time: "0s", duration: "10s", audio: false }); // Store the transformed video directly in R2 await env.R2_BUCKET.put("output-480p.mp4", await result.media(), { httpMetadata: { contentType: await result.contentType() }, }); return new Response("Video transformed and stored", { status: 200 }); }, }; ``` ## Error handling Errors can be thrown at different points in the method chain: * `.input()` can throw errors related to account limits (free tier or subscription) or service disruptions. * `.output()` can throw errors related to the transformation operation itself, such as invalid parameters or unsupported input formats. Errors throw a `MediaError`, which extends the standard `Error` interface with additional information: * `code`: A numeric error code. * `message`: A description of the error. * `stack`: Optional stack trace. Use a `try...catch` block to handle errors: ```ts export default { async fetch(request, env) { const video = await env.R2_BUCKET.get("input.mp4"); try { const result = env.MEDIA.input(video.body) .transform({ width: 480, height: 270 }) .output({ mode: "video", time: "0s", duration: "5s" }); return await result.response(); } catch (e) { if (e instanceof Error && "code" in e) { // Handle MediaError return new Response(`Transformation failed: ${e.message}`, { status: 500, }); } throw e; } }, }; ``` ## Caching Unlike transformations via URL, responses from the Media binding are _not_ automatically cached. Workers lets you interact directly with the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) to customize cache behavior. You can implement logic in your script to store transformations in Cloudflare's cache or R2 storage. ## Billing Refer to Stream's [Pricing](https://developers.cloudflare.com/stream/pricing/) information for costs. Transformations executed via the binding are billed on a per-operation basis, not based on request uniqueness. For best cost and performance optimization, cache or store outputs for reuse. ## Local development The Media Transformations API is available _in remote mode_ for local development through [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), the command-line interface for Workers. Transformations operations will be performed using a remote resource and are subject to usage charges beyond the included free tier. To enable usage in local development, add `remote` to the binding configuration: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "media": { "binding": "MEDIA", "remote": true } } ``` ```toml [media] binding = "MEDIA" # available in your Worker on env.MEDIA remote = true ``` Then run: ```sh npx wrangler dev ``` Note The Media Transformation binding does not support local simulation. If `remote = true` is not specified, the binding will produce an error during local development. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/stream/transform-videos/bindings/#page","headline":"Bind to Workers API · Cloudflare Stream docs","description":"Bind the Media Transformations API to a Cloudflare Worker to transform videos programmatically.","url":"https://developers.cloudflare.com/stream/transform-videos/bindings/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-10","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Configure your Worker to present a client certificate to services that enforce an mTLS connection. title: mTLS image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # mTLS Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) When using [HTTPS ↗](https://www.cloudflare.com/learning/ssl/what-is-https/), a server presents a certificate for the client to authenticate in order to prove their identity. For even tighter security, some services require that the client also present a certificate. This process - known as [mTLS ↗](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/) \- moves authentication to the protocol of TLS, rather than managing it in application code. Connections from unauthorized clients are rejected during the TLS handshake instead. To present a client certificate when communicating with a service, create a mTLS certificate [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) in your Worker project's Wrangler file. This will allow your Worker to present a client certificate to a service on your behalf. Caution Currently, mTLS for Workers cannot be used for requests made to a service that is a [proxied zone](https://developers.cloudflare.com/dns/proxy-status/) on Cloudflare. If your Worker presents a client certificate to a service proxied by Cloudflare, Cloudflare will return a `520` error. First, upload a certificate and its private key to your account using the [wrangler mtls-certificate](https://developers.cloudflare.com/workers/wrangler/commands/certificates/#mtls-certificate) command: Caution The `wrangler mtls-certificate upload` command requires the [SSL and Certificates Edit API token scope](https://developers.cloudflare.com/fundamentals/api/reference/permissions/). If you are using the OAuth flow triggered by `wrangler login`, the correct scope is set automatically. If you are using API tokens, refer to [Create an API token ↗](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) to set the right scope for your API token. ```sh npx wrangler mtls-certificate upload --cert cert.pem --key key.pem --name my-client-cert ``` Then, update your Worker project's Wrangler file to create an mTLS certificate binding: ```jsonc { "mtls_certificates": [ { "binding": "MY_CERT", "certificate_id": "" } ] } ``` ```toml [[mtls_certificates]] binding = "MY_CERT" certificate_id = "" ``` Note Certificate IDs are displayed after uploading, and can also be viewed with the command `wrangler mtls-certificate list`. Adding an mTLS certificate binding includes a variable in the Worker's environment on which the `fetch()` method is available. This `fetch()` method uses the standard [Fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/) API and has the exact same signature as the global `fetch`, but always presents the client certificate when establishing the TLS connection. Note mTLS certificate bindings present an API similar to [service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings). ### Interface ```js export default { async fetch(request, environment) { return await environment.MY_CERT.fetch("https://a-secured-origin.com"); }, }; ``` ```js interface Env { MY_CERT: Fetcher; } export default { async fetch(request, environment): Promise { return await environment.MY_CERT.fetch("https://a-secured-origin.com") } } satisfies ExportedHandler; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls/#page","headline":"mTLS · Cloudflare Workers docs","description":"Configure your Worker to present a client certificate to services that enforce an mTLS connection.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Produce and consume Cloudflare Queues messages using the Workers JavaScript API. title: JavaScript APIs image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/queues/llms.txt > Use this file to discover all available pages before exploring further. # JavaScript APIs Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/queues/configuration/javascript-apis/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Cloudflare Queues is integrated with [Cloudflare Workers](https://developers.cloudflare.com/workers). To send and receive messages, you must use a Worker. A Worker that can send messages to a Queue is a producer Worker, while a Worker that can receive messages from a Queue is a consumer Worker. It is possible for the same Worker to be a producer and consumer, if desired. In the future, we expect to support other APIs, such as HTTP endpoints to send or receive messages. To report bugs or request features, go to the [Cloudflare Community Forums ↗](https://community.cloudflare.com/c/developers/workers/40). To give feedback, go to the [#queues ↗](https://discord.cloudflare.com) Discord channel. ## Producer These APIs allow a producer Worker to send messages to a Queue. An example of writing a single message to a Queue: ```js export default { async fetch(req, env, ctx) { await env.MY_QUEUE.send({ url: req.url, method: req.method, headers: Object.fromEntries(req.headers), }); return new Response("Sent!"); }, }; ``` ```ts interface Env { readonly MY_QUEUE: Queue; } export default { async fetch(req, env, ctx): Promise { await env.MY_QUEUE.send({ url: req.url, method: req.method, headers: Object.fromEntries(req.headers), }); return new Response("Sent!"); }, } satisfies ExportedHandler; ``` ```python from workers import Response, WorkerEntrypoint class Default(WorkerEntrypoint): async def fetch(self, request): await self.env.MY_QUEUE.send({ "url": request.url, "method": request.method, "headers": dict(request.headers), }) return Response("Sent!") ``` The Queues API also supports writing multiple messages at once: ```js const sendResultsToQueue = async (results, env) => { const batch = results.map((value) => ({ body: value, })); await env.MY_QUEUE.sendBatch(batch); }; ``` ```ts const sendResultsToQueue = async (results: Array, env: Env) => { const batch: MessageSendRequest[] = results.map((value) => ({ body: value, })); await env.MY_QUEUE.sendBatch(batch); }; ``` ```python async def send_results_to_queue(results, env): batch = [ {"body": value} for value in results ] await env.MY_QUEUE.sendBatch(batch) ``` ### `Queue` A binding that allows a producer to send messages to a Queue. ```ts interface Queue { send(body: Body, options?: QueueSendOptions): Promise; sendBatch(messages: Iterable>, options?: QueueSendBatchOptions): Promise; metrics(): Promise; } ``` * `send(body: unknown, options?: {contentType?: QueuesContentType })` `Promise` * Sends a message to the Queue. The body can be any type supported by the [structured clone algorithm ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm#supported%5Ftypes), as long as its size is less than 128 KB. * When the promise resolves, the message is confirmed to be written to disk. * Returns a [QueueSendResult](#queuesendresult) containing realtime metrics about the queue. * `sendBatch(messages: Iterable>, options?: QueueSendBatchOptions)` `Promise` * Sends a batch of messages to the Queue. Each item in the provided [Iterable ↗](https://www.typescriptlang.org/docs/handbook/iterators-and-generators.html) must be supported by the [structured clone algorithm ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm#supported%5Ftypes). A batch can contain up to 100 messages, though items are limited to 128 KB each, and the total size of the array cannot exceed 256 KB. * The optional `options` parameter can be used to apply settings (such as `delaySeconds`) to all messages in the batch. See [QueueSendBatchOptions](#queuesendbatchoptions). * When the promise resolves, the messages are confirmed to be written to disk. * `metrics()` `Promise` * Returns realtime [QueueMetrics](#queuemetrics) for the queue. ### `MessageSendRequest` A wrapper type used for sending message batches. ```ts interface MessageSendRequest { body: Body; contentType?: QueueContentType; delaySeconds?: number; } ``` * `body` `unknown` * The body of the message. * The body can be any type supported by the [structured clone algorithm ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm#supported%5Ftypes), as long as its size is less than 128 KB. * `contentType` `QueueContentType` * The explicit content type of a message so it can be previewed correctly with the [List messages from the dashboard](https://developers.cloudflare.com/queues/examples/list-messages-from-dash/) feature. Optional argument. * See [QueuesContentType](#queuescontenttype) for possible values. * `delaySeconds` `number` * The number of seconds to [delay a message](https://developers.cloudflare.com/queues/configuration/batching-retries/) for within the queue, before it can be delivered to a consumer. * Must be an integer between 0 and 86400 (24 hours). ### `QueueSendOptions` Optional configuration that applies when sending a message to a queue. * `contentType` `QueuesContentType` * The explicit content type of a message so it can be previewed correctly with the [List messages from the dashboard](https://developers.cloudflare.com/queues/examples/list-messages-from-dash/) feature. Optional argument. * As of now, this option is for internal use. In the future, `contentType` will be used by alternative consumer types to explicitly mark messages as serialized so they can be consumed in the desired type. * See [QueuesContentType](#queuescontenttype) for possible values. * `delaySeconds` `number` * The number of seconds to [delay a message](https://developers.cloudflare.com/queues/configuration/batching-retries/) for within the queue, before it can be delivered to a consumer. * Must be an integer between 0 and 86400 (24 hours). Setting this value to zero will explicitly prevent the message from being delayed, even if there is a global (default) delay at the queue level. ### `QueueSendBatchOptions` Optional configuration that applies when sending a batch of messages to a queue. * `delaySeconds` `number` * The number of seconds to [delay messages](https://developers.cloudflare.com/queues/configuration/batching-retries/) for within the queue, before it can be delivered to a consumer. * Must be a positive integer. ### `QueuesContentType` A union type containing valid message content types. ```ts // Default: json type QueuesContentType = "text" | "bytes" | "json" | "v8"; ``` * Use `"json"` to send a JavaScript object that can be JSON-serialized. This content type can be previewed from the [Cloudflare dashboard ↗](https://dash.cloudflare.com). The `json` content type is the default. * Use `"text"` to send a `String`. This content type can be previewed with the [List messages from the dashboard](https://developers.cloudflare.com/queues/examples/list-messages-from-dash/) feature. * Use `"bytes"` to send an `ArrayBuffer`. This content type cannot be previewed from the [Cloudflare dashboard ↗](https://dash.cloudflare.com) and will display as Base64-encoded. * Use `"v8"` to send a JavaScript object that cannot be JSON-serialized but is supported by [structured clone ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm#supported%5Ftypes) (for example `Date` and `Map`). This content type cannot be previewed from the [Cloudflare dashboard ↗](https://dash.cloudflare.com) and will display as Base64-encoded. Note The default content type for Queues changed to `json` (from `v8`) to improve compatibility with pull-based consumers for any Workers with a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#queues-send-messages-in-json-format) after `2024-03-18`. If you specify an invalid content type, or if your specified content type does not match the message content's type, the send operation will fail with an error. ### `QueueSendResult` The result of a successful send operation. ```ts interface QueueSendResult { metadata: { metrics: QueueMetrics; }; } ``` * `metadata` `object` * Contains metadata about the queue after the send operation. * `metadata.metrics` `QueueMetrics` * Realtime metrics for the queue. See [QueueMetrics](#queuemetrics). ### `QueueMetrics` Realtime metrics for a queue. ```ts interface QueueMetrics { backlogCount: number; backlogBytes: number; oldestMessageTimestamp: number; } ``` * `backlogCount` `number` * The number of messages currently in the queue. * `backlogBytes` `number` * The total size of messages in the queue, in bytes. * `oldestMessageTimestamp` `number` * The timestamp (in milliseconds since epoch) of the oldest message in the queue. ## Consumer These APIs allow a consumer Worker to consume messages from a Queue. To define a consumer Worker, add a `queue()` function to the default export of the Worker. This will allow it to receive messages from the Queue. By default, all messages in the batch will be acknowledged as soon as all of the following conditions are met: 1. The `queue()` function has returned. 2. If the `queue()` function returned a promise, the promise has resolved. 3. Any promises passed to `waitUntil()` have resolved. If the `queue()` function throws, or the promise returned by it or any of the promises passed to `waitUntil()` were rejected, then the entire batch will be considered a failure and will be retried according to the consumer's retry settings. Note `waitUntil()` is the only supported method to run tasks (such as logging or metrics calls) that resolve after a queue handler has completed. Promises that have not resolved by the time the queue handler returns may not complete and will not block completion of execution. ```js export default { async queue(batch, env, ctx) { for (const message of batch.messages) { console.log("Received", message.body); } }, }; ``` ```ts interface Env { // Add your bindings here } export default { async queue(batch, env, ctx): Promise { for (const message of batch.messages) { console.log("Received", message.body); } }, } satisfies ExportedHandler; ``` ```python from workers import WorkerEntrypoint class Default(WorkerEntrypoint): async def queue(self, batch): for message in batch.messages: print("Received", message) ``` The `env` and `ctx` fields are as [documented in the Workers documentation](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/). ### TypeScript message types You can type queue messages with `Queue` on the producer and `ExportedHandler` on the consumer. ```ts type MyMessage = { id: string; }; interface Env { MY_QUEUE: Queue; } export default { async queue(batch) { for (const message of batch.messages) { console.log(message.body.id); } }, } satisfies ExportedHandler; ``` For primitive messages, use `Queue` or `satisfies ExportedHandler`. If you do not specify a type, `message.body` is `unknown`. Or alternatively, a queue consumer can be written using the (deprecated) service worker syntax: ```js addEventListener('queue', (event) => { event.waitUntil(handleMessages(event)); }); ``` In service worker syntax, `event` provides the same fields and methods as `MessageBatch`, as defined below, in addition to [waitUntil() ↗](https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent/waitUntil). Note When performing asynchronous tasks in your queue handler that iterates through messages, use an asynchronous version of iterating through your messages. For example, `for (const m of batch.messages)`or `await Promise.all(batch.messages.map(work))` allow for waiting for the results of asynchronous calls. `batch.messages.forEach()` does not. ### `MessageBatch` A batch of messages that are sent to a consumer Worker. ```ts interface MessageBatch { readonly queue: string; readonly messages: readonly Message[]; ackAll(): void; retryAll(options?: QueueRetryOptions): void; } ``` * `queue` `string` * The name of the Queue that belongs to this batch. * `messages` `Message[]` * An array of messages in the batch. Ordering of messages is best effort -- not guaranteed to be exactly the same as the order in which they were published. * `ackAll()` `void` * Marks every message as successfully delivered, regardless of whether your `queue()` consumer handler returns successfully or not. * `retryAll(options?: QueueRetryOptions)` `void` * Marks every message to be retried in the next batch. * Supports an optional `options` object. ### `Message` A message that is sent to a consumer Worker. ```ts interface Message { readonly id: string; readonly timestamp: Date; readonly body: Body; readonly attempts: number; ack(): void; retry(options?: QueueRetryOptions): void; } ``` * `id` `string` * A unique, system-generated ID for the message. * `timestamp` `Date` * A timestamp when the message was sent. * `body` `unknown` * The body of the message. * The body can be any type supported by the [structured clone algorithm ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm#supported%5Ftypes), as long as its size is less than 128 KB. * `attempts` `number` * The number of times the consumer has attempted to process this message. Starts at 1. * `ack()` `void` * Marks a message as successfully delivered, regardless of whether your `queue()` consumer handler returns successfully or not. * `retry(options?: QueueRetryOptions)` `void` * Marks a message to be retried in the next batch. * Supports an optional `options` object. ### `QueueRetryOptions` Optional configuration when marking a message or a batch of messages for retry. ```ts interface QueueRetryOptions { delaySeconds?: number; } ``` * `delaySeconds` `number` * The number of seconds to [delay a message](https://developers.cloudflare.com/queues/configuration/batching-retries/) for within the queue, before it can be delivered to a consumer. * Must be a positive integer. * When the promise resolves, the messages are written to disk. * Returns a [QueueSendResult](#queuesendresult) containing realtime metrics about the queue. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/queues/configuration/javascript-apis/#page","headline":"Cloudflare Queues - JavaScript APIs · Cloudflare Queues docs","description":"Produce and consume Cloudflare Queues messages using the Workers JavaScript API.","url":"https://developers.cloudflare.com/queues/configuration/javascript-apis/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Complete reference for the R2 in-Worker API, including bucket and object operations. title: Workers API reference image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/r2/llms.txt > Use this file to discover all available pages before exploring further. # Workers API reference Last updated Jul 31, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The in-Worker R2 API is accessed by binding an R2 bucket to a [Worker](https://developers.cloudflare.com/workers). The Worker you write can expose external access to buckets via a route or manipulate R2 objects internally. The R2 API includes some extensions and semantic differences from the S3 API. If you need S3 compatibility, consider using the [S3-compatible API](https://developers.cloudflare.com/r2/api/s3/). ## Concepts R2 organizes the data you store, called objects, into containers, called buckets. Buckets are the fundamental unit of performance, scaling, and access within R2. ## Create a binding Bindings A binding is how your Worker interacts with external resources such as [KV Namespaces](https://developers.cloudflare.com/kv/concepts/kv-namespaces/), [Durable Objects](https://developers.cloudflare.com/durable-objects/), or [R2 Buckets](https://developers.cloudflare.com/r2/buckets/). A binding is a runtime variable that the Workers runtime provides to your code. You can declare a variable name in your Wrangler file that will be bound to these resources at runtime, and interact with them through this variable. Every binding's variable name and behavior is determined by you when deploying the Worker. Refer to [Environment Variables](https://developers.cloudflare.com/workers/configuration/environment-variables/) for more information. A binding is defined in the Wrangler file of your Worker project's directory. To bind your R2 bucket to your Worker, add the following to your Wrangler file. Update the `binding` property to a valid JavaScript variable identifier and `bucket_name` to the name of your R2 bucket: ```jsonc { "r2_buckets": [ { "binding": "MY_BUCKET", // <~ valid JavaScript variable name "bucket_name": "" } ] } ``` ```toml [[r2_buckets]] binding = "MY_BUCKET" bucket_name = "" ``` Within your Worker, your bucket binding is now available under the `MY_BUCKET` variable and you can begin interacting with it using the [bucket methods](#bucket-method-definitions) described below. ## Bucket method definitions The following methods are available on the bucket binding object injected into your code. For example, to issue a `PUT` object request using the binding above: ```js export default { async fetch(request, env) { const url = new URL(request.url); const key = url.pathname.slice(1); switch (request.method) { case "PUT": await env.MY_BUCKET.put(key, request.body); return new Response(`Put ${key} successfully!`); default: return new Response(`${request.method} is not allowed.`, { status: 405, headers: { Allow: "PUT", }, }); } }, }; ``` ```py from workers import WorkerEntrypoint, Response from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): url = urlparse(request.url) key = url.path[1:] if request.method == "PUT": await self.env.MY_BUCKET.put(key, request.body) return Response(f"Put {key} successfully!") else: return Response( f"{request.method} is not allowed.", status=405, headers={"Allow": "PUT"} ) ``` * `head` `(key: string): Promise` * Retrieves the `R2Object` for the given key containing only object metadata, if the key exists, and `null` if the key does not exist. * `get` `(key: string, options?: R2GetOptions): Promise` * Retrieves the `R2ObjectBody` for the given key containing object metadata and the object body as a `ReadableStream`, if the key exists, and `null` if the key does not exist. * In the event that a precondition specified in `options` fails, `get()` returns an `R2Object` with `body` undefined. * `put` `(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise` * Stores the given `value` and metadata under the associated `key`. Once the write succeeds, returns an `R2Object` containing metadata about the stored Object. * In the event that a precondition specified in `options` fails, `put()` returns `null`, and the object will not be stored. * R2 writes are strongly consistent. Once the Promise resolves, all subsequent read operations will see this key value pair globally. * `delete` `(key: string | string[]): Promise` * Deletes the given `values` and metadata under the associated `keys`. Once the delete succeeds, returns `void`. * R2 deletes are strongly consistent. Once the Promise resolves, all subsequent read operations will no longer see the provided key value pairs globally. * Up to 1000 keys may be deleted per call. * `list` `(options?: R2ListOptions): Promise` * Returns an `R2Objects` containing a list of `R2Object` contained within the bucket. * The returned list of objects is ordered lexicographically. * Returns up to 1000 entries, but may return less in order to minimize memory pressure within the Worker. * To explicitly set the number of objects to list, provide an [R2ListOptions](https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#r2listoptions) object with the `limit` property set. * `createMultipartUpload` `(key: string, options?: R2MultipartOptions): Promise` * Creates a multipart upload. * Returns Promise which resolves to an `R2MultipartUpload` object representing the newly created multipart upload. Once the multipart upload has been created, the multipart upload can be immediately interacted with globally, either through the Workers API, or through the S3 API. * `resumeMultipartUpload` `(key: string, uploadId: string): R2MultipartUpload` * Returns an object representing a multipart upload with the given key and uploadId. * The resumeMultipartUpload operation does not perform any checks to ensure the validity of the uploadId, nor does it verify the existence of a corresponding active multipart upload. This is done to minimize latency before being able to call subsequent operations on the `R2MultipartUpload` object. ## `R2Object` definition `R2Object` is created when you `PUT` an object into an R2 bucket. `R2Object` represents the metadata of an object based on the information provided by the uploader. Every object that you `PUT` into an R2 bucket will have an `R2Object` created. * `key` `string` * The object's key. * `version` `string` * Random unique string associated with a specific upload of a key. * `size` `number` * Size of the object in bytes. * `etag` `string` Note Cloudflare recommends using the `httpEtag` field when returning an etag in a response header. This ensures the etag is quoted and conforms to [RFC 9110 ↗](https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3). * The etag associated with the object upload. * `httpEtag` `string` * The object's etag, in quotes so as to be returned as a header. * `uploaded` `Date` * A Date object representing the time the object was uploaded. * `httpMetadata` `R2HTTPMetadata` * Various HTTP headers associated with the object. Refer to [HTTP Metadata](#http-metadata). * `customMetadata` `Record` * A map of custom, user-defined metadata associated with the object. * `range` `R2Range`optional * A `R2Range` object containing the returned range of the object. * `checksums` `R2Checksums` * A `R2Checksums` object containing the stored checksums of the object. Refer to [checksums](#checksums). * `writeHttpMetadata` `(headers: Headers): void` * Retrieves the `httpMetadata` from the `R2Object` and applies their corresponding HTTP headers to the `Headers` input object. Refer to [HTTP Metadata](#http-metadata). * `storageClass` `'Standard' | 'InfrequentAccess'` * The storage class associated with the object. Refer to [Storage Classes](#storage-class). * `ssecKeyMd5` `string`optional * Hex-encoded MD5 hash of the [SSE-C](https://developers.cloudflare.com/r2/examples/ssec) key used for encryption (if one was provided). Hash can be used to identify which key is needed to decrypt object. ## `R2ObjectBody` definition `R2ObjectBody` represents an object's metadata combined with its body. It is returned when you `GET` an object from an R2 bucket. The full list of keys for `R2ObjectBody` includes the list below and all keys inherited from [R2Object](#r2object-definition). * `body` `ReadableStream` * The object's value. * `bodyUsed` `boolean` * Whether the object's value has been consumed or not. * `arrayBuffer` `(): Promise` * Returns a Promise that resolves to an `ArrayBuffer` containing the object's value. * `text` `(): Promise` * Returns a Promise that resolves to a string containing the object's value. * `json` `() : Promise` * Returns a Promise that resolves to the given object containing the object's value. * `blob` `(): Promise` * Returns a Promise that resolves to a binary Blob containing the object's value. ## `R2MultipartUpload` definition An `R2MultipartUpload` object is created when you call `createMultipartUpload` or `resumeMultipartUpload`. `R2MultipartUpload` is a representation of an ongoing multipart upload. Uncompleted multipart uploads will be automatically aborted after 7 days. Note An `R2MultipartUpload` object does not guarantee that there is an active underlying multipart upload corresponding to that object. A multipart upload can be completed or aborted at any time, either through the S3 API, or by a parallel invocation of your Worker. Therefore it is important to add the necessary error handling code around each operation on a `R2MultipartUpload` object in case the underlying multipart upload no longer exists. * `key` `string` * The `key` for the multipart upload. * `uploadId` `string` * The `uploadId` for the multipart upload. * `uploadPart` `(partNumber: number, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob, options?: R2MultipartOptions): Promise` * Uploads a single part with the specified part number to this multipart upload. Each part must be uniform in size with an exception for the final part which can be smaller. * Returns an `R2UploadedPart` object containing the `etag` and `partNumber`. These `R2UploadedPart` objects are required when completing the multipart upload. * `abort` `(): Promise` * Aborts the multipart upload. Returns a Promise that resolves when the upload has been successfully aborted. * `complete` `(uploadedParts: R2UploadedPart[]): Promise` * Completes the multipart upload with the given parts. * Returns a Promise that resolves when the complete operation has finished. Once this happens, the object is immediately accessible globally by any subsequent read operation. ## Method-specific types ### R2GetOptions * `onlyIf` `R2Conditional | Headers` * Specifies that the object should only be returned given satisfaction of certain conditions in the `R2Conditional` or in the conditional Headers. Refer to [Conditional operations](#conditional-operations). * `range` `R2Range | Headers`optional * Specifies that only a specific length (from an optional offset) or suffix of bytes from the object should be returned given the range in the `R2Range` or in the range `Headers`. Refer to [Ranged reads](#ranged-reads). * `ssecKey` `ArrayBuffer | string` * Specifies a key to be used for [SSE-C](https://developers.cloudflare.com/r2/examples/ssec). Key must be 32 bytes in length, in the form of a hex-encoded string or an ArrayBuffer. #### Ranged reads `R2GetOptions` accepts a `range` parameter, which can be used to restrict the data returned in `body`. There are 3 variations of arguments that can be used in a range: * An offset with an optional length. * An optional offset with a length. * A suffix. * `offset` `number` * The byte to begin returning data from, inclusive. * `length` `number` * The number of bytes to return. If more bytes are requested than exist in the object, fewer bytes than this number may be returned. * `suffix` `number` * The number of bytes to return from the end of the file, starting from the last byte. If more bytes are requested than exist in the object, fewer bytes than this number may be returned. ### R2PutOptions * `onlyIf` `R2Conditional | Headers` * Specifies that the object should only be stored given satisfaction of certain conditions in the `R2Conditional`. Refer to [Conditional operations](#conditional-operations). * `httpMetadata` `R2HTTPMetadata | Headers`optional * Various HTTP headers associated with the object. Refer to [HTTP Metadata](#http-metadata). * `customMetadata` `Record`optional * A map of custom, user-defined metadata that will be stored with the object. Note Only a single hashing algorithm can be specified at once. * `md5` `ArrayBuffer | string`optional * A md5 hash to use to check the received object's integrity. * `sha1` `ArrayBuffer | string`optional * A SHA-1 hash to use to check the received object's integrity. * `sha256` `ArrayBuffer | string`optional * A SHA-256 hash to use to check the received object's integrity. * `sha384` `ArrayBuffer | string`optional * A SHA-384 hash to use to check the received object's integrity. * `sha512` `ArrayBuffer | string`optional * A SHA-512 hash to use to check the received object's integrity. * `storageClass` `'Standard' | 'InfrequentAccess'` * Sets the storage class of the object if provided. Otherwise, the object will be stored in the default storage class associated with the bucket. Refer to [Storage Classes](#storage-class). * `ssecKey` `ArrayBuffer | string` * Specifies a key to be used for [SSE-C](https://developers.cloudflare.com/r2/examples/ssec). Key must be 32 bytes in length, in the form of a hex-encoded string or an ArrayBuffer. ### R2MultipartOptions * `httpMetadata` `R2HTTPMetadata | Headers`optional * Various HTTP headers associated with the object. Refer to [HTTP Metadata](#http-metadata). * `customMetadata` `Record`optional * A map of custom, user-defined metadata that will be stored with the object. * `storageClass` `string` * Sets the storage class of the object if provided. Otherwise, the object will be stored in the default storage class associated with the bucket. Refer to [Storage Classes](#storage-class). * `ssecKey` `ArrayBuffer | string` * Specifies a key to be used for [SSE-C](https://developers.cloudflare.com/r2/examples/ssec). Key must be 32 bytes in length, in the form of a hex-encoded string or an ArrayBuffer. ### R2ListOptions * `limit` `number`optional * The number of results to return. Defaults to `1000`, with a maximum of `1000`. * If `include` is set, you may receive fewer than `limit` results in your response to accommodate metadata. * `prefix` `string`optional * The prefix to match keys against. Keys will only be returned if they start with given prefix. * `cursor` `string`optional * An opaque token that indicates where to continue listing objects from. A cursor can be retrieved from a previous list operation. * `delimiter` `string`optional * The character to use when grouping keys. * `include` `Array`optional * Can include `httpMetadata` and/or `customMetadata`. If included, items returned by the list will include the specified metadata. * Note that there is a limit on the total amount of data that a single `list` operation can return. If you request data, you may receive fewer than `limit` results in your response to accommodate metadata. * The [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) must be set to `2022-08-04` or later in your Wrangler file. If not, then the `r2_list_honor_include` compatibility flag must be set. Otherwise it is treated as `include: ['httpMetadata', 'customMetadata']` regardless of what the `include` option provided actually is. This means applications must be careful to avoid comparing the amount of returned objects against your `limit`. Instead, use the `truncated` property to determine if the `list` request has more data to be returned. ```js const options = { limit: 500, include: ["customMetadata"], }; const listed = await env.MY_BUCKET.list(options); let truncated = listed.truncated; let cursor = truncated ? listed.cursor : undefined; // ❌ - if your limit can't fit into a single response or your // bucket has less objects than the limit, it will get stuck here. while (listed.objects.length < options.limit) { // ... } // ✅ - use the truncated property to check if there are more // objects to be returned while (truncated) { const next = await env.MY_BUCKET.list({ ...options, cursor: cursor, }); listed.objects.push(...next.objects); truncated = next.truncated; cursor = next.cursor; } ``` ```py limit = 500 include = ["customMetadata"] listed = await self.env.MY_BUCKET.list(limit=limit, include=include) truncated = listed.truncated cursor = listed.cursor if truncated else None # ❌ - if your limit can't fit into a single response or your # bucket has less objects than the limit, it will get stuck here. while len(listed.objects) < limit: ... # ✅ - use the truncated property to check if there are more # objects to be returned while truncated: next_page = await self.env.MY_BUCKET.list(limit=limit, include=include, cursor=cursor) listed.objects.extend(next_page.objects) truncated = next_page.truncated cursor = next_page.cursor ``` ### R2Objects An object containing an `R2Object` array, returned by `BUCKET_BINDING.list()`. * `objects` `Array` * An array of objects matching the `list` request. * `truncated` boolean * If true, indicates there are more results to be retrieved for the current `list` request. * `cursor` `string`optional * A token that can be passed to future `list` calls to resume listing from that point. Only present if truncated is true. * `delimitedPrefixes` `Array` * If a delimiter has been specified, contains all prefixes between the specified prefix and the next occurrence of the delimiter. * For example, if no prefix is provided and the delimiter is '/', `foo/bar/baz` would return `foo` as a delimited prefix. If `foo/` was passed as a prefix with the same structure and delimiter, `foo/bar` would be returned as a delimited prefix. ### Conditional operations You can pass an `R2Conditional` object to `R2GetOptions` and `R2PutOptions`. If the condition check for `get()` fails, the body will not be returned. This will make `get()` have lower latency. If the condition check for `put()` fails, `null` will be returned instead of the `R2Object`. * `etagMatches` `string`optional * Performs the operation if the object's etag matches the given string. * `etagDoesNotMatch` `string`optional * Performs the operation if the object's etag does not match the given string. * `uploadedBefore` `Date`optional * Performs the operation if the object was uploaded before the given date. * `uploadedAfter` `Date`optional * Performs the operation if the object was uploaded after the given date. Alternatively, you can pass a `Headers` object containing conditional headers to `R2GetOptions` and `R2PutOptions`. For information on these conditional headers, refer to [the MDN docs on conditional requests ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional%5Frequests#conditional%5Fheaders). All conditional headers aside from `If-Range` are supported. For more specific information about conditional requests, refer to [RFC 7232 ↗](https://datatracker.ietf.org/doc/html/rfc7232). ### HTTP Metadata Generally, these fields match the HTTP metadata passed when the object was created. They can be overridden when issuing `GET` requests, in which case, the given values will be echoed back in the response. * `contentType` `string`optional * `contentLanguage` `string`optional * `contentDisposition` `string`optional * `contentEncoding` `string`optional * `cacheControl` `string`optional * `cacheExpiry` `Date`optional ### Checksums If a checksum was provided when using the `put()` binding, it will be available on the returned object under the `checksums` property. The MD5 checksum will be included by default for non-multipart objects. * `md5` `ArrayBuffer`optional * The MD5 checksum of the object. * `sha1` `ArrayBuffer`optional * The SHA-1 checksum of the object. * `sha256` `ArrayBuffer`optional * The SHA-256 checksum of the object. * `sha384` `ArrayBuffer`optional * The SHA-384 checksum of the object. * `sha512` `ArrayBuffer`optional * The SHA-512 checksum of the object. ### `R2UploadedPart` An `R2UploadedPart` object represents a part that has been uploaded. `R2UploadedPart` objects are returned from `uploadPart` operations and must be passed to `completeMultipartUpload` operations. * `partNumber` `number` * The number of the part. * `etag` `string` * The `etag` of the part. ### Storage Class The storage class where an `R2Object` is stored. The available storage classes are `Standard` and `InfrequentAccess`. Refer to [Storage classes](https://developers.cloudflare.com/r2/buckets/storage-classes/)for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/r2/api/workers/workers-api-reference/#page","headline":"Workers API reference · Cloudflare R2 docs","description":"Complete reference for the R2 in-Worker API, including bucket and object operations.","url":"https://developers.cloudflare.com/r2/api/workers/workers-api-reference/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-31","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Define rate limits and interact with them directly from your Cloudflare Worker title: Rate Limiting image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Rate Limiting Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The Rate Limiting API lets you define rate limits and write code around them in your Worker. You can use it to enforce: * Rate limits that are applied after your Worker starts, only once a specific part of your code is reached * Different rate limits for different types of customers or users (ex: free vs. paid) * Resource-specific or path-specific limits (ex: limit per API route) * Any combination of the above The Rate Limiting API is backed by the same infrastructure that serves [rate limiting rules](https://developers.cloudflare.com/waf/rate-limiting-rules/). Note You must use version 4.36.0 or later of the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler). ## Get started First, add a [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) to your Worker that gives it access to the Rate Limiting API: ```jsonc { "main": "src/index.js", "ratelimits": [ { "name": "MY_RATE_LIMITER", // An identifier you define, that is unique to your Cloudflare account. // Must be an integer. "namespace_id": "1001", // Limit: the number of tokens allowed within a given period in a single // Cloudflare location // Period: the duration of the period, in seconds. Must be either 10 or 60 "simple": { "limit": 100, "period": 60 } } ] } ``` ```toml main = "src/index.js" [[ratelimits]] name = "MY_RATE_LIMITER" namespace_id = "1001" [ratelimits.simple] limit = 100 period = 60 ``` This binding makes the `MY_RATE_LIMITER` binding available, which provides a `limit()` method: ```javascript export default { async fetch(request, env) { const { pathname } = new URL(request.url) const { success } = await env.MY_RATE_LIMITER.limit({ key: pathname }) // key can be any string of your choosing if (!success) { return new Response(`429 Failure – rate limit exceeded for ${pathname}`, { status: 429 }) } return new Response(`Success!`) } } ``` ```ts interface Env { MY_RATE_LIMITER: RateLimit; } export default { async fetch(request, env): Promise { const { pathname } = new URL(request.url) const { success } = await env.MY_RATE_LIMITER.limit({ key: pathname }) // key can be any string of your choosing if (!success) { return new Response(`429 Failure – rate limit exceeded for ${pathname}`, { status: 429 }) } return new Response(`Success!`) } } satisfies ExportedHandler; ``` The `limit()` API accepts a single argument — a configuration object with the `key` field. * The key you provide can be any `string` value. * A common pattern is to define your key by combining a string that uniquely identifies the actor initiating the request (ex: a user ID or customer ID) and a string that identifies a specific resource (ex: a particular API route). You can define and configure multiple rate limiting configurations per Worker, which allows you to define different limits against incoming request and/or user parameters as needed to protect your application or upstream APIs. For example, here is how you can define two rate limiting configurations for free and paid tier users: ```jsonc { "main": "src/index.js", "ratelimits": [ // Free user rate limiting { "name": "FREE_USER_RATE_LIMITER", "namespace_id": "1001", "simple": { "limit": 100, "period": 60 } }, // Paid user rate limiting { "name": "PAID_USER_RATE_LIMITER", "namespace_id": "1002", "simple": { "limit": 1000, "period": 60 } } ] } ``` ```toml main = "src/index.js" [[ratelimits]] name = "FREE_USER_RATE_LIMITER" namespace_id = "1001" [ratelimits.simple] limit = 100 period = 60 [[ratelimits]] name = "PAID_USER_RATE_LIMITER" namespace_id = "1002" [ratelimits.simple] limit = 1_000 period = 60 ``` ## Configuration A rate limiting binding has the following settings: | Setting | Type | Description | | ------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | namespace\_id | string | A string containing a positive integer that uniquely defines this rate limiting namespace within your Cloudflare account (for example, "1001"). Although the value must be a valid integer, it is specified as a string. This is intentional. | | simple | object | The rate limit configuration. simple is the only supported type. | | simple.limit | number | The number of allowed requests (or calls to limit()) within the given period. | | simple.period | number | The duration of the rate limit window, in seconds. Must be either 10 or 60. | Note Two rate limiting bindings that share the same `namespace_id` — even across different Workers on the same account — share the same rate limit counters for a given key. This is intentional and allows you to enforce a single rate limit across multiple Workers. If you do not want to share rate limit state between bindings, use a unique `namespace_id` for each binding. For example, to apply a rate limit of 1500 requests per minute, you would define a rate limiting configuration as follows: ```jsonc { "ratelimits": [ { "name": "MY_RATE_LIMITER", "namespace_id": "1001", // 1500 requests - calls to limit() increment this "simple": { "limit": 1500, "period": 60 } } ] } ``` ```toml [[ratelimits]] name = "MY_RATE_LIMITER" namespace_id = "1001" [ratelimits.simple] limit = 1_500 period = 60 ``` ## Best practices The `key` passed to the `limit` function, that determines what to rate limit on, should represent a unique characteristic of a user or class of user that you wish to rate limit. * Good choices include API keys in `Authorization` HTTP headers, URL paths or routes, specific query parameters used by your application, and/or user IDs and tenant IDs. These are all stable identifiers and are unlikely to change from request-to-request. * It is not recommended to use IP addresses or locations (regions or countries), since these can be shared by many users in many valid cases. You may find yourself unintentionally rate limiting a wider group of users than you intended by rate limiting on these keys. ```ts // Recommended: use a key that represents a specific user or class of user const url = new URL(req.url) const userId = url.searchParams.get("userId") || "" const { success } = await env.MY_RATE_LIMITER.limit({ key: userId }) // Not recommended: many users may share a single IP, especially on mobile networks // or when using privacy-enabling proxies const ipAddress = req.headers.get("cf-connecting-ip") || "" const { success } = await env.MY_RATE_LIMITER.limit({ key: ipAddress }) ``` ## Locality Rate limits that you define and enforce in your Worker are local to the [Cloudflare location ↗](https://www.cloudflare.com/network/) that your Worker runs in. For example, if a request comes in from Sydney, Australia, to the Worker shown above, after 100 requests in a 60 second window, any further requests for a particular path would be rejected, and a 429 HTTP status code returned. But this would only apply to requests served in Sydney. For each unique key you pass to your rate limiting binding, there is a unique limit per Cloudflare location. ## Performance The Rate Limiting API in Workers is designed to be fast. The underlying counters are cached on the same machine that your Worker runs in, and updated asynchronously in the background by communicating with a backing store that is within the same Cloudflare location. This means that while in your code you `await` a call to the `limit()` method: ```javascript const { success } = await env.MY_RATE_LIMITER.limit({ key: customerId }) ``` You are not waiting on a network request. You can use the Rate Limiting API without introducing any meaningful latency to your Worker. ## Accuracy The above also means that the Rate Limiting API is permissive, eventually consistent, and intentionally designed to not be used as an accurate accounting system. For example, if many requests come in to your Worker in a single Cloudflare location, all rate limited on the same key, the [isolate](https://developers.cloudflare.com/workers/reference/how-workers-works) that serves each request will check against its locally cached value of the rate limit. Very quickly, but not immediately, these requests will count towards the rate limit within that Cloudflare location. ## Monitoring Rate limiting bindings are not currently visible in the Cloudflare dashboard. To monitor rate-limited requests from your Worker: * **[Workers Observability](https://developers.cloudflare.com/workers/observability/)** — Use [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/) and [Traces](https://developers.cloudflare.com/workers/observability/traces/) to observe HTTP 429 responses returned by your Worker when rate limits are exceeded. * **[Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/)** — Add an Analytics Engine binding to your Worker and emit custom data points (for example, a `rate_limited` event) when `limit()` returns `{ success: false }`. This lets you build dashboards and query rate limiting metrics over time. ## Examples * [@elithrar/workers-hono-rate-limit ↗](https://github.com/elithrar/workers-hono-rate-limit) — Middleware that lets you easily add rate limits to routes in your [Hono ↗](https://hono.dev/) application. * [@hono-rate-limiter/cloudflare ↗](https://github.com/rhinobase/hono-rate-limiter) — Middleware that lets you easily add rate limits to routes in your [Hono ↗](https://hono.dev/) application, with multiple data stores to choose from. * [hono-cf-rate-limit ↗](https://github.com/bytaesu/hono-cf-rate-limit) — Middleware for Hono applications that applies rate limiting in Cloudflare Workers, powered by Wrangler’s built-in features. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/#page","headline":"Rate Limiting · Cloudflare Workers docs","description":"Define rate limits and interact with them directly from your Cloudflare Worker","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Store sensitive information, like API keys and auth tokens, in your Worker. title: Secrets image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Secrets Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/configuration/secrets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Secrets are a type of binding that allow you to attach encrypted text values to your Worker. Secrets are used for storing sensitive information like API keys and auth tokens. You can access secrets in your Worker code through: * The [env parameter](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#parameters) passed to your Worker's [fetch event handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). * Importing `env` from [cloudflare:workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) to access secrets from anywhere in your code. * [process.env](https://developers.cloudflare.com/workers/configuration/environment-variables) in Workers that have [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) enabled. ## Access your secrets with Workers Secrets can be accessed from Workers as you would any other [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). For instance, given a `DB_CONNECTION_STRING` secret, you can access it in your Worker code through the `env` parameter: ```js import postgres from "postgres"; export default { async fetch(request, env, ctx) { const sql = postgres(env.DB_CONNECTION_STRING); const result = await sql`SELECT * FROM products;`; return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" }, }); }, }; ``` You can also import `env` from `cloudflare:workers` to access secrets from anywhere in your code, including outside of request handlers: ```js import { env } from "cloudflare:workers"; import postgres from "postgres"; // Initialize the database client at the top level using a secret const sql = postgres(env.DB_CONNECTION_STRING); export default { async fetch(request) { const result = await sql`SELECT * FROM products;`; return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" }, }); }, }; ``` ```ts import { env } from "cloudflare:workers"; import postgres from "postgres"; // Initialize the database client at the top level using a secret const sql = postgres(env.DB_CONNECTION_STRING); export default { async fetch(request: Request): Promise { const result = await sql`SELECT * FROM products;`; return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" }, }); }, }; ``` For more details on accessing `env` globally, refer to [Importing env as a global](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global). Secrets Store (beta) Secrets described on this page are defined and managed on a per-Worker level. If you want to use account-level secrets, refer to [Secrets Store](https://developers.cloudflare.com/secrets-store/). Account-level secrets are configured on your Worker as a [Secrets Store binding](https://developers.cloudflare.com/secrets-store/integrations/workers/). ## Local Development with Secrets Caution Do not use `vars` to store sensitive information in your Worker's Wrangler configuration file. Use secrets instead. Put secrets for use in local development in either a `.dev.vars` file or a `.env` file, in the same directory as the Wrangler configuration file. Note You can use the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) to declare which secret names your Worker requires. When defined, only the keys listed in `secrets.required` are loaded from `.dev.vars` or `.env`. Additional keys are excluded and missing keys produce a warning. Note Choose to use either `.dev.vars` or `.env` but not both. If you define a `.dev.vars` file, then values in `.env` files will not be included in the `env` object during local development. These files should be formatted using the [dotenv ↗](https://hexdocs.pm/dotenvy/dotenv-file-format.html) syntax. For example: ```bash SECRET_KEY="value" API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" ``` Do not commit secrets to git The `.dev.vars` and `.env` files should not be committed to git. Add `.dev.vars*` and `.env*` to your project's `.gitignore` file. To set different secrets for each Cloudflare environment, create files named `.dev.vars.` or `.env.`. When you select a Cloudflare environment in your local development, the corresponding environment-specific file will be loaded ahead of the generic `.dev.vars` (or `.env`) file. * When using `.dev.vars.` files, all secrets must be defined per environment. If `.dev.vars.` exists then only this will be loaded; the `.dev.vars` file will not be loaded. * In contrast, all matching `.env` files are loaded and the values are merged. For each variable, the value from the most specific file is used, with the following precedence: * `.env..local` (most specific) * `.env.local` * `.env.` * `.env` (least specific) Controlling \`.env\` handling It is possible to control how `.env` files are loaded in local development by setting environment variables on the process running the tools. * To disable loading local dev vars from `.env` files without providing a `.dev.vars` file, set the `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV` environment variable to `"false"`. * To include every environment variable defined in your system's process environment as a local development variable, ensure there is no `.dev.vars` and then set the `CLOUDFLARE_INCLUDE_PROCESS_ENV` environment variable to `"true"`. This is not needed when using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property), which loads from `process.env` automatically. ## Secrets on deployed Workers ### Validate secrets before deploy You can declare the secret names your Worker requires using the [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) in your Wrangler configuration. When defined, `wrangler deploy` and `wrangler versions upload` will fail with a clear error if any required secrets are not configured on the Worker. ### Adding secrets to your project #### Via Wrangler Secrets can be added through [wrangler secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) or [wrangler versions secret put](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-secret-put) commands. `wrangler secret put` creates a new version of the Worker and deploys it immediately. ```sh npx wrangler secret put ``` If using [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/), instead use the `wrangler versions secret put` command. This will only create a new version of the Worker, that can then be deploying using [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-deploy). Note Wrangler versions before 3.73.0 require you to specify a `--x-versions` flag. ```sh npx wrangler versions secret put ``` #### Via the dashboard To add a secret via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker > **Settings**. 3. Under **Variables and Secrets**, select **Add**. 4. Select the type **Secret**, input a **Variable name**, and input its **Value**. This secret will be made available to your Worker but the value will be hidden in Wrangler and the dashboard. 5. (Optional) To add more secrets, select **Add variable**. 6. Select **Deploy** to implement your changes. #### Upload secrets alongside code You can upload secrets at the same time as your Worker code using the `--secrets-file` flag on [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/workers/#deploy) or [wrangler versions upload](https://developers.cloudflare.com/workers/wrangler/commands/workers/#versions-upload). This accepts a path to a JSON or `.env` file — the same formats accepted by [wrangler secret bulk](https://developers.cloudflare.com/workers/wrangler/commands/workers/#secret-bulk). You can upload up to 100 secrets per bulk request for a single version. ```sh npx wrangler deploy --secrets-file .env.production ``` ```sh npx wrangler versions upload --secrets-file secrets.json ``` Secrets not included in the file are preserved from the previous version. This is useful in CI/CD pipelines where you want to deploy code and update secrets in a single operation. ### Delete secrets from your project #### Via Wrangler Secrets can be deleted through [wrangler secret delete](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret-delete) or [wrangler versions secret delete](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-secret-delete) commands. `wrangler secret delete` creates a new version of the Worker and deploys it immediately. ```sh npx wrangler secret delete ``` If using [gradual deployments](https://developers.cloudflare.com/workers/versions-and-deployments/gradual-deployments/), instead use the `wrangler versions secret delete` command. This will only create a new version of the Worker, that can then be deploying using [wrangler versions deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#versions-deploy). ```sh npx wrangler versions secret delete ``` #### Via the dashboard To delete a secret from your Worker project via the dashboard: 1. In the Cloudflare dashboard, go to the **Workers & Pages** page. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. In **Overview**, select your Worker > **Settings**. 3. Under **Variables and Secrets**, select **Edit**. 4. In the **Edit** drawer, select **X** next to the secret you want to delete. 5. Select **Deploy** to implement your changes. 6. (Optional) Instead of using the edit drawer, you can click the delete icon next to the secret. ## Compare secrets and environment variables Use secrets for sensitive information Do not use plaintext environment variables to store sensitive information. Use [secrets](https://developers.cloudflare.com/workers/configuration/secrets/) or [Secrets Store bindings](https://developers.cloudflare.com/secrets-store/integrations/workers/) instead. [Secrets](https://developers.cloudflare.com/workers/configuration/secrets/) are [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/). The difference is secret values are not visible within Wrangler or Cloudflare dashboard after you define them. This means that sensitive data, including passwords or API tokens, should always be encrypted to prevent data leaks. To your Worker, there is no difference between an environment variable and a secret. The secret's value is passed through as defined. ## Related resources * [Wrangler secret commands](https://developers.cloudflare.com/workers/wrangler/commands/general/#secret) \- Review the Wrangler commands to create, delete and list secrets. * [secrets configuration property](https://developers.cloudflare.com/workers/wrangler/configuration/#secrets-configuration-property) \- Declare required secret names in your Wrangler configuration. Used for validation during local development and deploy, and as the source of truth for type generation. * [Cloudflare Secrets Store](https://developers.cloudflare.com/secrets-store/) \- Encrypt and store sensitive information as secrets that are securely reusable across your account. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/configuration/secrets/#page","headline":"Secrets · Cloudflare Workers docs","description":"Store sensitive information, like API keys and auth tokens, in your Worker.","url":"https://developers.cloudflare.com/workers/configuration/secrets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Cloudflare Secrets Store is a secure, centralized location in which account-level secrets are stored and managed. The secrets are securely encrypted and stored across all Cloudflare data centers. title: Workers integration image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/secrets-store/llms.txt > Use this file to discover all available pages before exploring further. # Workers integration Last updated May 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/secrets-store/integrations/workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) [Cloudflare Secrets Store](https://developers.cloudflare.com/secrets-store/) is a secure, centralized location in which account-level secrets are stored and managed. The secrets are securely encrypted and stored across all Cloudflare data centers. Consider the steps below to learn how to use values from your account secrets store with [Cloudflare Workers](https://developers.cloudflare.com/workers/). Note This is different from Workers [Variables and Secrets](https://developers.cloudflare.com/workers/configuration/secrets/), where you define and manage your secrets on a per-Worker level. ## Before you begin * If [using the Dashboard](#via-dashboard), make sure you already have a Workers application. Refer to the [Workers get started](https://developers.cloudflare.com/workers/get-started/dashboard/) for guidance. * You should also have a store created under the **Secrets Store** tab on the Dashboard. The first store in your account is created automatically when a user with [Super Administrator or Secrets Store Admin role](https://developers.cloudflare.com/secrets-store/access-control/) interacts with it. * If no store exists in your account yet and you have the necessary permissions, you can use the [Wrangler command](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/#secrets-store-store) `secrets-store store create --remote` to create your first store. Local development mode This guide assumes you are working in production. To use Secrets Store locally, you must use `secrets-store secret` [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/) without the `--remote` flag. ## 1\. Set up account secrets in Secrets Store Follow the steps below to create secrets. You must have a [Super Administrator or a Secrets Store Admin role](https://developers.cloudflare.com/secrets-store/access-control/) within your Cloudflare account. Note You may also add account secrets directly from the Workers settings on the dashboard. You can skip to [step 2](#via-dashboard) to do that. Use the [Wrangler command](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/#secrets-store-secret) `secrets-store secret create`. To use the following example, replace the store ID and secret name by your actual data. You can find and copy the store ID from the [Secrets Store tab ↗](https://dash.cloudflare.com/?to=/:account/secrets-store/) on the dashboard or use `wrangler secrets-store store list`. Note that a secret name cannot contain spaces. ```sh npx wrangler secrets-store secret create --name MY_SECRET_NAME --scopes workers --remote ``` ```sh ✓ Enter a secret value: › *** 🔐 Creating secret... (Name: MY_SECRET_NAME, Value: REDACTED, Scopes: workers, Comment: undefined) ✓ Select an account: › My account ✅ Created secret! (ID: 13bc7498c6374a4e9d13be091c3c65f1) ``` 1. In the Cloudflare dashboard, go to the **Secrets Store** page. [Go to **Secrets Store** ↗](https://dash.cloudflare.com/?to=/:account/secrets-store) 2. Select **Create secret**. 3. Fill in the required fields, choosing _Workers_ as the **Permission scope**. Once the secret is saved, the secret value will no longer be available for viewing. 4. (Optional) Select **Add additional secret** to create more than one secret at a time. 5. Select **Save** to confirm. You can find and copy the store ID from the [Secrets Store tab ↗](https://dash.cloudflare.com/?to=/:account/secrets-store/) on the dashboard or use the [Wrangler command](https://developers.cloudflare.com/workers/wrangler/commands/secrets-store/#secrets-store-store). Also, make sure your secret `name` does not contain spaces. Refer to [Secrets Store API](https://developers.cloudflare.com/api/resources/secrets%5Fstore/) for the full API documentation. Required API token permissions At least one of the following [token permissions](https://developers.cloudflare.com/fundamentals/api/reference/permissions/) is required: * `Secrets Store Write` ```bash curl "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/secrets_store/stores/$STORE_ID/secrets" \ --request POST \ --header "X-Auth-Email: $CLOUDFLARE_EMAIL" \ --header "X-Auth-Key: $CLOUDFLARE_API_KEY" \ --json '[ { "name": "", "value": "", "scopes": [ "workers" ], "comment": "" }, { "name": "", "value": "", "scopes": [ "workers" ], "comment": "" } ]' ``` Refer to [manage account secrets](https://developers.cloudflare.com/secrets-store/manage-secrets/) for further options. ## 2\. Bind an account secret to your Worker [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) allow your Worker to interact with resources on your Cloudflare account. To bind an account secret to your Worker, you must have one of the following [roles within your Cloudflare account](https://developers.cloudflare.com/secrets-store/access-control/): * Super Administrator * Secrets Store Deployer ### Via Wrangler 1. Add a Secrets Store binding to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/): * `binding`: a descriptive name for your binding. This will be used in the Workers application when [accessing your secret on the env object](https://developers.cloudflare.com/secrets-store/integrations/workers/#3-access-the-secret-on-the-env-object). * `store_id`: the corresponding Secrets Store ID where your account secret was created. * `secret_name`: the unique secret name, defined when your account secret was created. ```jsonc { "main": "./src/index.js", "secrets_store_secrets": [ { "binding": "", "store_id": "", "secret_name": "" } ] } ``` ```toml main = "./src/index.js" [[secrets_store_secrets]] binding = "" store_id = "" secret_name = "" ``` ### Via Dashboard 1. In the Cloudflare dashboard, go to **Workers & Pages**. [Go to **Workers & Pages** ↗](https://dash.cloudflare.com/?to=/:account/workers-and-pages) 2. Select a Workers application. 3. Go to **Settings** \> **Bindings** and select **Add**. 4. On the **Add a resource binding** side panel, choose **Secrets Store**. 5. Fill in the required fields: * **Variable name**: a name for the binding. This will be used for your Worker to access the secret ([step 3](#3-access-the-secret-on-the-env-object) below). * **Secret name**: select from the list of available account secrets created in [step 1](#1-set-up-account-secrets-in-secrets-store). * (Optional - Admins only) If the secret you need does not exist yet, select **Create secret**. This will add an account level secret in the same way as if you had [created it on the Secrets Store](https://developers.cloudflare.com/secrets-store/manage-secrets/). 6. Select **Deploy** to deploy your binding. When deploying, there are two options: * **Deploy:** Immediately deploy the binding to 100% of your audience. * **Save version:** Save a version of the binding which you can deploy in the future. ## 3\. Access the secret on the `env` object [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) are located on the `env` object. To access the secret you first need an asynchronous call. ### Call `get()` on the binding variable Local development mode You cannot access production secrets (created on the dashboard, via API, or with the `--remote` flag) from your local development setup. To use Secrets Store locally, you must use `secrets-store secret` [Wrangler commands](https://developers.cloudflare.com/workers/wrangler/commands/) without the `--remote` flag. ```js export default { async fetch(request, env) { // Example of using the secret safely in an API request const APIkey = await env..get() const response = await fetch("https://api.example.com/data", { headers: { "Authorization": `Bearer ${APIKey}` }, }); if (!response.ok) { return new Response("Failed to fetch data", { status: response.status }); } const data = await response.json(); return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" }, }); }, }; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/secrets-store/integrations/workers/#page","headline":"Workers integration · Cloudflare Secrets Store docs","description":"Cloudflare Secrets Store is a secure, centralized location in which account-level secrets are stored and managed. The secrets are securely encrypted and stored across all Cloudflare data centers.","url":"https://developers.cloudflare.com/secrets-store/integrations/workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Facilitate Worker-to-Worker communication. title: Service bindings image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Service bindings Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## About Service bindings Service bindings allow one Worker to call into another, without going through a publicly-accessible URL. A Service binding allows Worker A to call a method on Worker B, or to forward a request from Worker A to Worker B. Service bindings provide the separation of concerns that microservice or service-oriented architectures provide, without configuration pain, performance overhead or need to learn RPC protocols. * **Service bindings are fast.** When you use Service Bindings, there is zero overhead or added latency. By default, both Workers run on the same thread of the same Cloudflare server. And when you enable [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/), each Worker runs in the optimal location for overall performance. * **Service bindings are not just HTTP.** Worker A can expose methods that can be directly called by Worker B. Communicating between services only requires writing JavaScript methods and classes. * **Service bindings don't increase costs.** You can split apart functionality into multiple Workers, without incurring additional costs. Learn more about [pricing for Service Bindings](https://developers.cloudflare.com/workers/platform/pricing/#service-bindings). ![Service bindings are a zero-cost abstraction](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1532,height=578,format=webp/_astro/service-bindings-comparison.CeB5uD1k.png) Service bindings are commonly used to: * **Provide a shared internal service to multiple Workers.** For example, you can deploy an authentication service as its own Worker, and then have any number of separate Workers communicate with it via Service bindings. * **Isolate services from the public Internet.** You can deploy a Worker that is not reachable via the public Internet, and can only be reached via an explicit Service binding that another Worker declares. * **Allow teams to deploy code independently.** Team A can deploy their Worker on their own release schedule, and Team B can deploy their Worker separately. ## Configuration You add a Service binding by modifying the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) of the caller — the Worker that you want to be able to initiate requests. For example, if you want Worker A to be able to call Worker B — you'd add the following to the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) for Worker A: ```jsonc { "services": [ { "binding": "", "service": "" } ] } ``` ```toml [[services]] binding = "" service = "" ``` * `binding`: The name of the key you want to expose on the `env` object. * `service`: The name of the target Worker you would like to communicate with. This Worker must be on your Cloudflare account. ## Interfaces Worker A that declares a Service binding to Worker B can call Worker B in two different ways: 1. [RPC](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc) lets you communicate between Workers using function calls that you define. For example, `await env.BINDING_NAME.myMethod(arg1)`. This is recommended for most use cases, and allows you to create your own internal APIs that your Worker makes available to other Workers. 2. [HTTP](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http) lets you communicate between Workers by calling the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch) from other Workers, sending `Request` objects and receiving `Response` objects back. For example, `env.BINDING_NAME.fetch(request)`. ## Example — build your first Service binding using RPC This example [extends the WorkerEntrypoint class](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#the-workerentrypoint-class) to support RPC-based Service bindings. First, create the Worker that you want to communicate with. Let's call this "Worker B". Worker B exposes the public method, `add(a, b)`: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_b", "main": "./src/workerB.js" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_b" main = "./src/workerB.js" ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class WorkerB extends WorkerEntrypoint { // Currently, entrypoints without a named handler are not supported async fetch() { return new Response(null, { status: 404 }); } async add(a, b) { return a + b; } } ``` Next, create the Worker that will call Worker B. Let's call this "Worker A". Worker A declares a binding to Worker B. This is what gives it permission to call public methods on Worker B. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_a", "main": "./src/workerA.js", "services": [ { "binding": "WORKER_B", "service": "worker_b" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_a" main = "./src/workerA.js" [[services]] binding = "WORKER_B" service = "worker_b" ``` ```js export default { async fetch(request, env) { const result = await env.WORKER_B.add(1, 2); return new Response(result); }, }; ``` To run both Worker A and Worker B in local development, you must run two instances of [Wrangler](https://developers.cloudflare.com/workers/wrangler) in your terminal. For each Worker, open a new terminal and run [npx wrangler@latest dev](https://developers.cloudflare.com/workers/wrangler/commands#dev). Each Worker is deployed separately. ## Lifecycle The Service bindings API is asynchronous — you must `await` any method you call. If Worker A invokes Worker B via a Service binding, and Worker A does not await the completion of Worker B, Worker B will be terminated early. For more about the lifecycle of calling a Worker over a Service Binding via RPC, refer to the [RPC Lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle) docs. ## Local development Local development is supported for Service bindings. For each Worker, open a new terminal and use [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) in the relevant directory. When running `wrangler dev`, service bindings will show as `connected`/`not connected` depending on whether Wrangler can find a running `wrangler dev` session for that Worker. For example: ```sh $ wrangler dev ... Your worker has access to the following bindings: - Services: - SOME_OTHER_WORKER: some-other-worker [connected] - ANOTHER_WORKER: another-worker [not connected] ``` Wrangler also supports running multiple Workers at once with one command. To try it out, pass multiple `-c` flags to Wrangler, like this: `wrangler dev -c wrangler.json -c ../other-worker/wrangler.json`. The first config will be treated as the _primary_ worker, which will be exposed over HTTP as usual at `http://localhost:8787`. The remaining config files will be treated as _secondary_ and will only be accessible via a service binding from the primary worker. Caution Support for running multiple Workers at once with one Wrangler command is experimental, and subject to change as we work on the experience. If you run into bugs or have any feedback, [open an issue on the workers-sdk repository ↗](https://github.com/cloudflare/workers-sdk/issues/new) ## Deployment Workers using Service bindings are deployed separately. When getting started and deploying for the first time, this means that the target Worker (Worker B in the examples above) must be deployed first, before Worker A. Otherwise, when you attempt to deploy Worker A, deployment will fail, because Worker A declares a binding to Worker B, which does not yet exist. When making changes to existing Workers, in most cases you should: * Deploy changes to Worker B first, in a way that is compatible with the existing Worker A. For example, add a new method to Worker B. * Next, deploy changes to Worker A. For example, call the new method on Worker B, from Worker A. * Finally, remove any unused code. For example, delete the previously used method on Worker B. ## Smart Placement [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) automatically places your Worker in an optimal location that minimizes latency. You can use Smart Placement together with Service bindings to split your Worker into two services: ![Smart Placement and Service Bindings](https://developers.cloudflare.com/cdn-cgi/image/onerror=redirect,width=1444,height=692,format=webp/_astro/smart-placement-service-bindings.Ce58BYeF.png) Refer to the [docs on Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/#multiple-workers) for more. ## Limits Service bindings have the following limits: * Each request to a Worker via a Service binding counts toward your [subrequest limit](https://developers.cloudflare.com/workers/platform/limits/#subrequests). * A single request has a maximum of 32 Worker invocations, and each call to a Service binding counts towards this limit. Subsequent calls will throw an exception. * Calling a service binding does not count towards [simultaneous open connection limits](https://developers.cloudflare.com/workers/platform/limits/#simultaneous-open-connections) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/#page","headline":"Service bindings - Runtime APIs · Cloudflare Workers docs","description":"Facilitate Worker-to-Worker communication.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Bindings"]} ``` --- --- description: Facilitate Worker-to-Worker communication by forwarding Request objects. title: HTTP image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # HTTP Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Worker A that declares a Service binding to Worker B can forward a [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) object to Worker B, by calling the `fetch()` method that is exposed on the binding object. For example, consider the following Worker that implements a [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/): ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_b", "main": "./src/workerB.js" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_b" main = "./src/workerB.js" ``` ```js export default { async fetch(request, env, ctx) { return new Response("Hello World!"); } } ``` The following Worker declares a binding to the Worker above: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_a", "main": "./src/workerA.js", "services": [ { "binding": "WORKER_B", "service": "worker_b" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_a" main = "./src/workerA.js" [[services]] binding = "WORKER_B" service = "worker_b" ``` And then can forward a request to it: ```js export default { async fetch(request, env) { return await env.WORKER_B.fetch(request); }, }; ``` Note If you construct a new request manually, rather than forwarding an existing one, ensure that you provide a valid and fully-qualified URL with a hostname. For example: ```js export default { async fetch(request, env) { // provide a valid URL let newRequest = new Request("https://valid-url.com", { method: "GET" }); let response = await env.WORKER_B.fetch(newRequest); return response; } }; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/#page","headline":"Service bindings - HTTP · Cloudflare Workers docs","description":"Facilitate Worker-to-Worker communication by forwarding Request objects.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Facilitate Worker-to-Worker communication via RPC. title: RPC (WorkerEntrypoint) image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # RPC (WorkerEntrypoint) Last updated Aug 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings) allow one Worker to call into another, without going through a publicly-accessible URL. You can use Service bindings to create your own internal APIs that your Worker makes available to other Workers. This can be done by extending the built-in `WorkerEntrypoint` class, and adding your own public methods. These public methods can then be directly called by other Workers on your Cloudflare account that declare a [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) to this Worker. The [RPC system in Workers](https://developers.cloudflare.com/workers/runtime-apis/rpc) is designed feel as similar as possible to calling a JavaScript function in the same Worker. In most cases, you should be able to write code in the same way you would if everything was in a single Worker. Note You can also use RPC to communicate between Workers and [Durable Objects](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoke-rpc-methods). ## Example For example, the following Worker implements the public method `add(a, b)`: For example, if Worker B implements the public method `add(a, b)`: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_b", "main": "./src/workerB.js" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_b" main = "./src/workerB.js" ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async fetch() { return new Response("Hello from Worker B"); } add(a, b) { return a + b; } } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async fetch() { return new Response("Hello from Worker B"); } add(a: number, b: number) { return a + b; } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response("Hello from Worker B") def add(self, a: int, b: int) -> int: return a + b ``` Worker A can declare a [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) to Worker B: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_a", "main": "./src/workerA.js", "services": [ { "binding": "WORKER_B", "service": "worker_b" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_a" main = "./src/workerA.js" [[services]] binding = "WORKER_B" service = "worker_b" ``` Making it possible for Worker A to call the `add()` method from Worker B: ```js export default { async fetch(request, env) { const result = await env.WORKER_B.add(1, 2); return new Response(result); }, }; ``` ```ts export default { async fetch(request, env) { const result = await env.WORKER_B.add(1, 2); return new Response(result); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): result = await self.env.WORKER_B.add(1, 2) return Response(f"Result: {result}") ``` You do not need to learn, implement, or think about special protocols to use the RPC system. The client, in this case Worker A, calls Worker B and tells it to execute a specific procedure using specific arguments that the client provides. This is accomplished with standard JavaScript classes. ## The `WorkerEntrypoint` Class To provide RPC methods from your Worker, you must extend the `WorkerEntrypoint` class, as shown in the example below: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async add(a, b) { return a + b; } } ``` ```python from workers import WorkerEntrypoint class Default(WorkerEntrypoint): async def add(self, a, b): return a + b ``` A new instance of the class is created every time the Worker is called. Note that even though the Worker is implemented as a class, it is still stateless — the class instance only lasts for the duration of the invocation. If you need to persist or coordinate state in Workers, you should use [Durable Objects](https://developers.cloudflare.com/durable-objects). ### Bindings (`env`) The [env](https://developers.cloudflare.com/workers/runtime-apis/bindings) object is exposed as a class property of the `WorkerEntrypoint` class. For example, a Worker that declares a binding to the [environment variable](https://developers.cloudflare.com/workers/configuration/environment-variables/) `GREETING`: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "vars": { "GREETING": "Hello" } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" [vars] GREETING = "Hello" ``` Can access it by calling `this.env.GREETING`: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { fetch() { return new Response("Hello from my-worker"); } async greet(name) { return this.env.GREETING + name; } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response("Hello from my-worker") async def greet(self, name): return self.env.GREETING + name ``` You can use any type of [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) this way. ### Lifecycle methods (`ctx`) The [ctx](https://developers.cloudflare.com/workers/runtime-apis/context) object is exposed as a class property of the `WorkerEntrypoint` class. For example, you can extend the lifetime of the invocation context by calling the `waitUntil()` method: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { fetch() { return new Response("Hello from my-worker"); } async signup(email, name) { // sendEvent() will continue running, even after this method returns a value to the caller this.ctx.waitUntil(this.#sendEvent("signup", email)) // Perform any other work return "Success"; } async #sendEvent(eventName, email) { //... } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response("Hello from my-worker") async def signup(self, email, name): # _send_event() will continue running, even after this method returns a value to the caller self.ctx.waitUntil(self._send_event("signup", email)) # Perform any other work return "Success" async def _send_event(self, event_name, email): # ... pass ``` ### Fetching static assets If your Worker has a [static assets binding](https://developers.cloudflare.com/workers/static-assets/binding/), you can call `this.env.ASSETS.fetch()` from within an RPC method. Since RPC methods do not receive a `request` parameter, construct a `Request` or URL with any hostname — the hostname is ignored by the assets binding, only the pathname matters: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class ImageWorker extends WorkerEntrypoint { async getImage(path) { return this.env.ASSETS.fetch(new Request(`https://assets.local${path}`)); } } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export class ImageWorker extends WorkerEntrypoint { async getImage(path: string): Promise { return this.env.ASSETS.fetch( new Request(`https://assets.local${path}`) ); } } ``` ```python from workers import WorkerEntrypoint, Request class ImageWorker(WorkerEntrypoint): async def get_image(self, path: str): return await self.env.ASSETS.fetch( Request.new(f"https://assets.local{path}") ) ``` The caller can then invoke this method via RPC: ```js const response = await env.IMAGE_SERVICE.getImage("/images/logo.png"); ``` ```ts const response = await env.IMAGE_SERVICE.getImage("/images/logo.png"); ``` ```python response = await self.env.IMAGE_SERVICE.get_image("/images/logo.png") ``` Note When fetching assets via the binding, the hostname (for example, `assets.local`) is not meaningful — any valid hostname will work. Only the URL pathname is used to match assets. The convention `assets.local` is used for clarity. ## Named entrypoints You can also export any number of named `WorkerEntrypoint` classes from within a single Worker, in addition to the default export. You can then declare a Service binding to a specific named entrypoint. You can use this to group multiple pieces of compute together. For example, you might create a distinct `WorkerEntrypoint` for each permission role in your application, and use these to provide role-specific RPC methods: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "todo-app", "d1_databases": [ { "binding": "D1", "database_name": "todo-app-db", "database_id": "" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "todo-app" [[d1_databases]] binding = "D1" database_name = "todo-app-db" database_id = "" ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class AdminEntrypoint extends WorkerEntrypoint { async createUser(username) { await this.env.D1.prepare("INSERT INTO users (username) VALUES (?)") .bind(username) .run(); } async deleteUser(username) { await this.env.D1.prepare("DELETE FROM users WHERE username = ?") .bind(username) .run(); } } export class UserEntrypoint extends WorkerEntrypoint { async getTasks(userId) { return await this.env.D1.prepare( "SELECT title FROM tasks WHERE user_id = ?" ) .bind(userId) .run(); } async createTask(userId, title) { await this.env.D1.prepare( "INSERT INTO tasks (user_id, title) VALUES (?, ?)" ) .bind(userId, title) .run(); } } export default class extends WorkerEntrypoint { async fetch(request, env) { return new Response("Hello from my to do app"); } } ``` ```python from workers import WorkerEntrypoint, Response class AdminEntrypoint(WorkerEntrypoint): async def create_user(self, username): await self.env.D1.prepare("INSERT INTO users (username) VALUES (?)").bind(username).run() async def delete_user(self, username): await self.env.D1.prepare("DELETE FROM users WHERE username = ?").bind(username).run() class UserEntrypoint(WorkerEntrypoint): async def get_tasks(self, user_id): return await self.env.D1.prepare("SELECT title FROM tasks WHERE user_id = ?").bind(user_id).run() async def create_task(self, user_id, title): await self.env.D1.prepare("INSERT INTO tasks (user_id, title) VALUES (?, ?)").bind(user_id, title).run() class Default(WorkerEntrypoint): async def fetch(self, request): return Response("Hello from my to do app") ``` You can then declare a Service binding directly to `AdminEntrypoint` in another Worker: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "admin-app", "services": [ { "binding": "ADMIN", "service": "todo-app", "entrypoint": "AdminEntrypoint" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "admin-app" [[services]] binding = "ADMIN" service = "todo-app" entrypoint = "AdminEntrypoint" ``` ```js export default { async fetch(request, env) { await env.ADMIN.createUser("aNewUser"); return new Response("Hello from admin app"); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): await self.env.ADMIN.create_user("aNewUser") return Response("Hello from admin app") ``` You can learn more about how to configure D1 in the [D1 documentation](https://developers.cloudflare.com/d1/get-started/#3-bind-your-worker-to-your-d1-database). You can try out a complete example of this to do app, as well as a Discord bot built with named entrypoints, by cloning the [cloudflare/js-rpc-and-entrypoints-demo repository ↗](https://github.com/cloudflare/js-rpc-and-entrypoints-demo) from GitHub. ## Further reading * [Lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/) * [Reserved Methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/reserved-methods/) * [Visibility and Security Model](https://developers.cloudflare.com/workers/runtime-apis/rpc/visibility/) * [TypeScript](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/) * [Error handling](https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#page","headline":"Service bindings - RPC (WorkerEntrypoint) · Cloudflare Workers docs","description":"Facilitate Worker-to-Worker communication via RPC.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["RPC"]} ``` --- --- description: A binding connects your Worker to external resources on the Developer Platform, like Stream, R2 buckets, or KV namespaces. title: Bind to Workers API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/stream/llms.txt > Use this file to discover all available pages before exploring further. # Bind to Workers API Last updated May 7, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/stream/manage-video-library/bindings/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) A [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) connects your [Worker](https://developers.cloudflare.com/workers/) to external resources on the Developer Platform, like [Stream](https://developers.cloudflare.com/stream/), [R2 buckets](https://developers.cloudflare.com/r2/buckets/), or [KV namespaces](https://developers.cloudflare.com/kv/concepts/kv-namespaces/). For example, when you use Stream within Workers, you can: * Upload videos from a URL and manage their lifecycle * Create direct uploads for client-side uploads without having to expose API keys * List and search videos * Manage captions and downloads for videos * Create and manage watermark profiles ## Setup The Stream binding is enabled on a per-Worker basis. To bind Stream to your Worker, add the following to the end of your Wrangler configuration file: ```jsonc { "stream": { "binding": "STREAM" } } ``` ```toml [stream] binding = "STREAM" ``` For more detailed information on configuring your Worker, refer to the [Wrangler Configuration documentation](https://developers.cloudflare.com/workers/wrangler/configuration/). ## Methods ### Binding-level methods The following methods are available on the `env.STREAM` binding directly. #### `upload(url, params?)` Upload a video from a URL. Returns `Promise<`[StreamVideo](#streamvideo)`>`. * `url` (required): The URL of the video to upload. * `params` (optional): A [StreamUrlUploadParams](#streamurluploadparams) object with the following properties: * `allowedOrigins`: Array of allowed origins for the video. * `creator`: Creator identifier. * `meta`: Arbitrary metadata object. * `requireSignedURLs`: Whether signed URLs are required. * `scheduledDeletion`: ISO 8601 timestamp for scheduled deletion. * `thumbnailTimestampPct`: Thumbnail timestamp as a percentage (0.0 to 1.0). * `watermarkId`: ID of a watermark profile to apply. Throws: `BadRequestError`, `QuotaReachedError`, `MaxFileSizeError`, `RateLimitedError`, `AlreadyUploadedError`, `InternalError`. #### `createDirectUpload(params)` Create a basic direct upload URL for client-side uploads without an API key. Returns `Promise<`[StreamDirectUpload](#streamdirectupload)`>` with `uploadURL` and `id`. _This method does not currently support files over 200MB._ For larger direct uploads, refer to the [API request for provisioning a TUS endpoint ↗](http://localhost:1111/stream/uploading-videos/direct-creator-uploads/#direct-creator-uploads-with-tus-protocol).\_ * `params` (required): A [StreamDirectUploadCreateParams](#streamdirectuploadcreateparams) object with the following properties: * `maxDurationSeconds` (required): Maximum duration of the uploaded video in seconds. * `expiry` (optional): ISO 8601 timestamp when the upload URL expires. * `creator` (optional): Creator identifier. * `meta` (optional): Arbitrary metadata object. * `allowedOrigins` (optional): Array of allowed origins for the video. * `requireSignedURLs` (optional): Whether signed URLs are required. * `thumbnailTimestampPct` (optional): Thumbnail timestamp as a percentage (0.0 to 1.0). * `scheduledDeletion` (optional): ISO 8601 timestamp for scheduled deletion. * `watermark` (optional): ID of a watermark profile to apply. #### `videos.list(params?)` List all videos in the account. Returns `Promise<`[StreamVideo](#streamvideo)`[]>`. * `params` (optional): A [StreamVideosListParams](#streamvideoslistparams) object with the following properties: * `limit`: Maximum number of videos to return. * `before`: Return videos created before this ISO 8601 timestamp. * `beforeComp`: Comparison operator for `before` — `eq`, `gt`, `gte`, `lt`, or `lte`. * `after`: Return videos created after this ISO 8601 timestamp. * `afterComp`: Comparison operator for `after` — `eq`, `gt`, `gte`, `lt`, or `lte`. ### Video-scoped methods Calling `env.STREAM.video(id)` returns a handle scoped to a single video, with the following methods. #### `details()` Get full video details. Returns `Promise<`[StreamVideo](#streamvideo)`>`. #### `update(params)` Update video metadata. Returns `Promise<`[StreamVideo](#streamvideo)`>`. * `params` (required): A [StreamUpdateVideoParams](#streamupdatevideoparams) object with the following properties: * `allowedOrigins`: Array of allowed origins for the video. * `creator`: Creator identifier. * `maxDurationSeconds`: Maximum duration in seconds. * `meta`: Arbitrary metadata object. * `requireSignedURLs`: Whether signed URLs are required. * `scheduledDeletion`: ISO 8601 timestamp for scheduled deletion. * `thumbnailTimestampPct`: Thumbnail timestamp as a percentage (0.0 to 1.0). #### `delete()` Delete a video and its copies. Returns `Promise`. #### `generateToken()` Create a signed URL token for a video. Returns `Promise`. #### `downloads` Namespace for download operations on a video. * `generate(downloadType?)`: Generate a download. `downloadType` is a [StreamDownloadType](#streamdownloadtype) of `default` or `audio`. Defaults to `default`. Returns `Promise<`[StreamDownloadGetResponse](#streamdownloadgetresponse)`>`. * `get()`: List existing downloads. Returns `Promise<`[StreamDownloadGetResponse](#streamdownloadgetresponse)`>`. * `delete(downloadType?)`: Delete downloads. `downloadType` is `default` or `audio`. #### `captions` Namespace for caption operations on a video. * `upload(language, input)`: Upload a caption file for a BCP 47 language tag. `input` is a `ReadableStream`. Returns `Promise<`[StreamCaption](#streamcaption)`>`. * `generate(language)`: Generate captions via AI for a BCP 47 language tag. Returns `Promise<`[StreamCaption](#streamcaption)`>`. * `list(language?)`: List captions, optionally filtered by language. Returns `Promise<`[StreamCaption](#streamcaption)`[]>`. * `delete(language)`: Delete captions for a language. Returns `Promise`. ### Watermark methods The following methods are available on the `env.STREAM.watermarks` namespace. #### `watermarks.generate(input, params)` Create a watermark profile. Accepts either a `ReadableStream` or a URL string. Returns `Promise<`[StreamWatermark](#streamwatermark)`>`. * `input` (required): A `ReadableStream` or URL string of the watermark image. * `params` (optional): A [StreamWatermarkCreateParams](#streamwatermarkcreateparams) object with the following properties: * `name`: Name of the watermark profile. * `opacity`: Opacity of the watermark (0.0 to 1.0). * `padding`: Padding around the watermark as a proportion of the video resolution. * `scale`: Scale of the watermark as a proportion of the video resolution. * `position`: Position of the watermark — `upperRight`, `upperLeft`, `lowerLeft`, `lowerRight`, or `center`. #### `watermarks.list()` List all watermark profiles. Returns `Promise<`[StreamWatermark](#streamwatermark)`[]>`. #### `watermarks.get(watermarkId)` Get a single watermark profile. Returns `Promise<`[StreamWatermark](#streamwatermark)`>`. * `watermarkId` (required): The ID of the watermark profile. #### `watermarks.delete(watermarkId)` Delete a watermark profile. Returns `Promise`. * `watermarkId` (required): The ID of the watermark profile. ## Examples ### Upload a video from a URL ```js export default { async fetch(request, env) { const video = await env.STREAM.upload("https://example.com/video.mp4", { creator: "user-123", meta: { category: "tutorial" }, allowedOrigins: ["example.com"], }); return Response.json(video); }, }; ``` ```ts export default { async fetch(request, env) { const video = await env.STREAM.upload("https://example.com/video.mp4", { creator: "user-123", meta: { category: "tutorial" }, allowedOrigins: ["example.com"], }); return Response.json(video); }, }; ``` ### Create a direct upload ```js export default { async fetch(request, env) { const directUpload = await env.STREAM.createDirectUpload({ maxDurationSeconds: 300, creator: "user-123", meta: { source: "browser-upload" }, }); return Response.json(directUpload); }, }; ``` ```ts export default { async fetch(request, env) { const directUpload = await env.STREAM.createDirectUpload({ maxDurationSeconds: 300, creator: "user-123", meta: { source: "browser-upload" }, }); return Response.json(directUpload); }, }; ``` ### List videos ```js export default { async fetch(request, env) { const videos = await env.STREAM.videos.list({ limit: 10, after: "2025-01-01T00:00:00Z", }); return Response.json(videos); }, }; ``` ```ts export default { async fetch(request, env) { const videos = await env.STREAM.videos.list({ limit: 10, after: "2025-01-01T00:00:00Z", }); return Response.json(videos); }, }; ``` ### Get video details ```js export default { async fetch(request, env) { const videoDetails = await env.STREAM.video("VIDEO_ID").details(); return Response.json(videoDetails); }, }; ``` ```ts export default { async fetch(request, env) { const videoDetails = await env.STREAM.video("VIDEO_ID").details(); return Response.json(videoDetails); }, }; ``` ### Update video metadata ```js export default { async fetch(request, env) { const videoDetails = await env.STREAM.video("VIDEO_ID").update({ meta: { category: "updated-tutorial" }, allowedOrigins: ["example.com", "*.example.com"], }); return Response.json(videoDetails); }, }; ``` ```ts export default { async fetch(request, env) { const videoDetails = await env.STREAM.video("VIDEO_ID").update({ meta: { category: "updated-tutorial" }, allowedOrigins: ["example.com", "*.example.com"], }); return Response.json(videoDetails); }, }; ``` ### Delete a video ```js export default { async fetch(request, env) { await env.STREAM.video("VIDEO_ID").delete(); return new Response("Video deleted", { status: 200 }); }, }; ``` ```ts export default { async fetch(request, env) { await env.STREAM.video("VIDEO_ID").delete(); return new Response("Video deleted", { status: 200 }); }, }; ``` ### Generate a signed URL token ```js export default { async fetch(request, env) { const token = await env.STREAM.video("VIDEO_ID").generateToken(); return Response.json({ token }); }, }; ``` ```ts export default { async fetch(request, env) { const token = await env.STREAM.video("VIDEO_ID").generateToken(); return Response.json({ token }); }, }; ``` ### Upload captions ```js export default { async fetch(request, env) { const captionResponse = await fetch("https://example.com/captions-en.vtt"); const caption = await env.STREAM.video("VIDEO_ID").captions.upload( "en", captionResponse.body, ); return Response.json(caption); }, }; ``` ```ts export default { async fetch(request, env) { const captionResponse = await fetch("https://example.com/captions-en.vtt"); const caption = await env.STREAM.video("VIDEO_ID").captions.upload( "en", captionResponse.body, ); return Response.json(caption); }, }; ``` ### Generate AI captions ```js export default { async fetch(request, env) { const caption = await env.STREAM.video("VIDEO_ID").captions.generate("en"); return Response.json(caption); }, }; ``` ```ts export default { async fetch(request, env) { const caption = await env.STREAM.video("VIDEO_ID").captions.generate("en"); return Response.json(caption); }, }; ``` ### List and delete captions ```js export default { async fetch(request, env) { const video = env.STREAM.video("VIDEO_ID"); const captions = await video.captions.list(); await video.captions.delete("en"); return Response.json(captions); }, }; ``` ```ts export default { async fetch(request, env) { const video = env.STREAM.video("VIDEO_ID"); const captions = await video.captions.list(); await video.captions.delete("en"); return Response.json(captions); }, }; ``` ### Generate and list downloads ```js export default { async fetch(request, env) { const video = env.STREAM.video("VIDEO_ID"); const downloads = await video.downloads.generate(); const audioDownloads = await video.downloads.generate("audio"); const allDownloads = await video.downloads.get(); return Response.json({ downloads, audioDownloads, allDownloads }); }, }; ``` ```ts export default { async fetch(request, env) { const video = env.STREAM.video("VIDEO_ID"); const downloads = await video.downloads.generate(); const audioDownloads = await video.downloads.generate("audio"); const allDownloads = await video.downloads.get(); return Response.json({ downloads, audioDownloads, allDownloads }); }, }; ``` ### Create a watermark profile ```js export default { async fetch(request, env) { const watermark = await env.STREAM.watermarks.generate( "https://example.com/watermark.png", { name: "My Watermark", opacity: 0.5, position: "lowerRight", padding: 0.05, scale: 0.1, }, ); return Response.json(watermark); }, }; ``` ```ts export default { async fetch(request, env) { const watermark = await env.STREAM.watermarks.generate( "https://example.com/watermark.png", { name: "My Watermark", opacity: 0.5, position: "lowerRight", padding: 0.05, scale: 0.1, }, ); return Response.json(watermark); }, }; ``` ### List and delete watermark profiles ```js export default { async fetch(request, env) { const watermarks = await env.STREAM.watermarks.list(); const watermark = await env.STREAM.watermarks.get("WATERMARK_ID"); await env.STREAM.watermarks.delete("WATERMARK_ID"); return Response.json({ watermarks, watermark }); }, }; ``` ```ts export default { async fetch(request, env) { const watermarks = await env.STREAM.watermarks.list(); const watermark = await env.STREAM.watermarks.get("WATERMARK_ID"); await env.STREAM.watermarks.delete("WATERMARK_ID"); return Response.json({ watermarks, watermark }); }, }; ``` ## Type definitions ### StreamVideo `StreamVideo` is returned by operations that retrieve or create a video. It contains the full metadata for a video. * `id` `string` * The unique identifier for the video. * `creator` `string | null` * A user-defined identifier for the media creator. * `thumbnail` `string` * The thumbnail URL for the video. * `thumbnailTimestampPct` `number` * The thumbnail timestamp percentage. * `readyToStream` `boolean` * Indicates whether the video is ready to stream. * `readyToStreamAt` `string | null` * The date and time the video became ready to stream. * `status` `StreamVideoStatus` * Processing status information. Refer to [StreamVideoStatus](#streamvideostatus). * `meta` `Record` * A user modifiable key-value store. * `created` `string` * The date and time the video was created. * `modified` `string` * The date and time the video was last modified. * `scheduledDeletion` `string | null` * The date and time at which the video will be deleted. * `size` `number` * The size of the video in bytes. * `preview` `string`optional * The preview URL for the video. * `allowedOrigins` `Array` * Origins allowed to display the video. * `requireSignedURLs` `boolean | null` * Indicates whether signed URLs are required. * `uploaded` `string | null` * The date and time the video was uploaded. * `uploadExpiry` `string | null` * The date and time when the upload URL expires. * `maxSizeBytes` `number | null` * The maximum size in bytes for direct uploads. * `maxDurationSeconds` `number | null` * The maximum duration in seconds for direct uploads. * `duration` `number` * The video duration in seconds. `-1` indicates unknown. * `input` `StreamVideoInput` * Input metadata for the original upload. Refer to [StreamVideoInput](#streamvideoinput). * `hlsPlaybackUrl` `string` * The HLS playback URL for the video. * `dashPlaybackUrl` `string` * The DASH playback URL for the video. * `watermark` `StreamWatermark | null` * The watermark applied to the video, if any. Refer to [StreamWatermark](#streamwatermark). * `liveInputId` `string | null`optional * The live input ID associated with the video, if any. * `clippedFromId` `string | null` * The source video ID if this is a clip. * `publicDetails` `StreamPublicDetails | null` * Public details associated with the video. Refer to [StreamPublicDetails](#streampublicdetails). ### StreamVideoStatus Processing status information for a video. * `state` `string` * The current processing state. * `step` `string`optional * The current processing step. * `pctComplete` `string`optional * The percent complete as a string. * `errorReasonCode` `string` * An error reason code, if applicable. * `errorReasonText` `string` * An error reason text, if applicable. ### StreamVideoInput Input metadata for the original upload. * `width` `number` * The input width in pixels. * `height` `number` * The input height in pixels. ### StreamPublicDetails Public details associated with a video. * `title` `string | null` * The public title for the video. * `share_link` `string | null` * The public share link. * `channel_link` `string | null` * The public channel link. * `logo` `string | null` * The public logo URL. ### StreamDirectUpload Returned by `createDirectUpload()`. Contains the upload URL and video identifier for a direct upload. * `uploadURL` `string` * The URL an unauthenticated upload can use for a single multipart request. * `id` `string` * A Cloudflare-generated unique identifier for a media item. * `watermark` `StreamWatermark | null` * The watermark profile applied to the upload. Refer to [StreamWatermark](#streamwatermark). * `scheduledDeletion` `string | null` * The scheduled deletion time, if any. ### StreamCaption Represents a caption or subtitle track for a video. * `generated` `boolean`optional * Whether the caption was generated via AI. * `label` `string` * The language label displayed in the native language to users. * `language` `string` * The language tag in BCP 47 format. * `status` `'ready' | 'inprogress' | 'error'`optional * The status of a generated caption. ### StreamDownloadGetResponse An object with download type keys. Each key is optional and only present if that download type has been created. * `default` `StreamDownload`optional * The default video download. Only present if this download type has been created. Refer to [StreamDownload](#streamdownload). * `audio` `StreamDownload`optional * The audio-only download. Only present if this download type has been created. Refer to [StreamDownload](#streamdownload). ### StreamDownload Represents a generated download for a video. * `percentComplete` `number` * Indicates the progress as a percentage between 0 and 100. * `status` `StreamDownloadStatus` * The status of a generated download. * `url` `string`optional * The URL to access the generated download. ### StreamWatermark Represents a watermark profile. * `id` `string` * The unique identifier for a watermark profile. * `name` `string` * A short description of the watermark profile. * `opacity` `number` * The translucency of the image. A value of `0.0` makes the image completely transparent, and `1.0` makes the image completely opaque. Note that if the image is already semi-transparent, setting this to `1.0` will not make the image completely opaque. * `padding` `number` * The whitespace between the adjacent edges (determined by position) of the video and the image. `0.0` indicates no padding, and `1.0` indicates a fully padded video width or length. * `scale` `number` * The size of the image relative to the overall size of the video. `0.0` indicates no scaling, and `1.0` fills the entire video. * `position` `StreamWatermarkPosition` * The location of the image. Refer to [StreamWatermarkPosition](#streamwatermarkposition). * `size` `number` * The size of the image in bytes. * `height` `number` * The height of the image in pixels. * `width` `number` * The width of the image in pixels. * `created` `string` * The date and time a watermark profile was created. * `downloadedFrom` `string | null` * The source URL for a downloaded image. If the watermark profile was created via direct upload, this field is `null`. ### StreamWatermarkPosition The position of a watermark on a video. `'upperRight' | 'upperLeft' | 'lowerLeft' | 'lowerRight' | 'center'` * `upperRight` — Top-right corner of the video. * `upperLeft` — Top-left corner of the video. * `lowerLeft` — Bottom-left corner of the video. * `lowerRight` — Bottom-right corner of the video. * `center` — Center of the video. Note that `center` ignores the `padding` parameter. ### StreamDownloadStatus The status of a generated download. `'ready' | 'inprogress' | 'error'` * `ready` — The download is ready. * `inprogress` — The download is being generated. * `error` — An error occurred during generation. ### StreamDownloadType The type of download to generate. `'default' | 'audio'` * `default` — A video download. * `audio` — An audio-only download. ### StreamUrlUploadParams Parameters for uploading a video from a URL. * `allowedOrigins` `Array`optional * Lists the origins allowed to display the video. Enter allowed origin domains in an array and use `*` for wildcard subdomains. Empty arrays allow the video to be viewed on any origin. * `creator` `string`optional * A user-defined identifier for the media creator. * `meta` `Record`optional * A user modifiable key-value store used to reference other systems of record for managing videos. * `requireSignedURLs` `boolean`optional * Indicates whether the video can be accessed using the ID. When set to `true`, a signed token must be generated with a signing key to view the video. * `scheduledDeletion` `string | null`optional * Indicates the date and time at which the video will be deleted. Omit the field to indicate no change, or include with a `null` value to remove an existing scheduled deletion. If specified, must be at least 30 days from upload time. * `thumbnailTimestampPct` `number`optional * The timestamp for a thumbnail image calculated as a percentage value of the video's duration. To convert from a second-wise timestamp to a percentage, divide the desired timestamp by the total duration of the video. If this value is not set, the default thumbnail image is taken from 0s of the video. * `watermarkId` `string`optional * The identifier for the watermark profile. ### StreamDirectUploadCreateParams Parameters for creating a direct upload. * `maxDurationSeconds` `number` * The maximum duration in seconds for a video upload. * `expiry` `string`optional * The date and time after upload when videos will not be accepted. * `creator` `string`optional * A user-defined identifier for the media creator. * `meta` `Record`optional * A user modifiable key-value store used to reference other systems of record for managing videos. * `allowedOrigins` `Array`optional * Lists the origins allowed to display the video. * `requireSignedURLs` `boolean`optional * Indicates whether the video can be accessed using the ID. When set to `true`, a signed token must be generated with a signing key to view the video. * `thumbnailTimestampPct` `number`optional * The timestamp for a thumbnail image calculated as a percentage value of the video's duration. * `scheduledDeletion` `string | null`optional * The date and time at which the video will be deleted. Include `null` to remove a scheduled deletion. * `watermark` `StreamDirectUploadWatermark`optional * The watermark profile to apply. Refer to [StreamDirectUploadWatermark](#streamdirectuploadwatermark). ### StreamDirectUploadWatermark Watermark configuration for a direct upload. * `id` `string` * The unique identifier for the watermark profile. ### StreamUpdateVideoParams Parameters for updating a video. * `allowedOrigins` `Array`optional * Lists the origins allowed to display the video. Enter allowed origin domains in an array and use `*` for wildcard subdomains. Empty arrays allow the video to be viewed on any origin. * `creator` `string`optional * A user-defined identifier for the media creator. * `maxDurationSeconds` `number`optional * The maximum duration in seconds for a video upload. Can be set for a video that is not yet uploaded to limit its duration. Uploads that exceed the specified duration will fail during processing. A value of `-1` means the value is unknown. * `meta` `Record`optional * A user modifiable key-value store used to reference other systems of record for managing videos. * `requireSignedURLs` `boolean`optional * Indicates whether the video can be accessed using the ID. When set to `true`, a signed token must be generated with a signing key to view the video. * `scheduledDeletion` `string | null`optional * Indicates the date and time at which the video will be deleted. Omit the field to indicate no change, or include with a `null` value to remove an existing scheduled deletion. If specified, must be at least 30 days from upload time. * `thumbnailTimestampPct` `number`optional * The timestamp for a thumbnail image calculated as a percentage value of the video's duration. To convert from a second-wise timestamp to a percentage, divide the desired timestamp by the total duration of the video. If this value is not set, the default thumbnail image is taken from 0s of the video. ### StreamVideosListParams Parameters for listing videos. * `limit` `number`optional * The maximum number of videos to return. * `before` `string`optional * Return videos created before this timestamp (RFC3339/RFC3339Nano). * `beforeComp` `StreamPaginationComparison`optional * Comparison operator for the `before` field. Defaults to `lt`. Refer to [StreamPaginationComparison](#streampaginationcomparison). * `after` `string`optional * Return videos created after this timestamp (RFC3339/RFC3339Nano). * `afterComp` `StreamPaginationComparison`optional * Comparison operator for the `after` field. Defaults to `gte`. Refer to [StreamPaginationComparison](#streampaginationcomparison). ### StreamPaginationComparison Comparison operators for pagination queries. `'eq' | 'gt' | 'gte' | 'lt' | 'lte'` * `eq` — Equal to * `gt` — Greater than * `gte` — Greater than or equal to * `lt` — Less than * `lte` — Less than or equal to ### StreamWatermarkCreateParams Parameters for creating a watermark profile. * `name` `string`optional * A short description of the watermark profile. * `opacity` `number`optional * The translucency of the image. A value of `0.0` makes the image completely transparent, and `1.0` makes the image completely opaque. Note that if the image is already semi-transparent, setting this to `1.0` will not make the image completely opaque. * `padding` `number`optional * The whitespace between the adjacent edges (determined by position) of the video and the image. `0.0` indicates no padding, and `1.0` indicates a fully padded video width or length. * `scale` `number`optional * The size of the image relative to the overall size of the video. `0.0` indicates no scaling, and `1.0` fills the entire video. * `position` `StreamWatermarkPosition`optional * The location of the image. Refer to [StreamWatermarkPosition](#streamwatermarkposition). ## Error handling Errors throw a `StreamError`, which extends the standard `Error` interface with additional information: * `code`: A numeric error code. * `statusCode`: An HTTP status code. * `message`: A description of the error. * `stack`: Optional stack trace. The following error subtypes may be thrown: | Error type | Description | | ---------------------- | ---------------------------------------------------------- | | InternalError | An internal server error occurred. | | BadRequestError | The request was malformed or contained invalid parameters. | | NotFoundError | The requested resource was not found. | | ForbiddenError | The request was not authorized. | | RateLimitedError | The request was rate limited. | | QuotaReachedError | The account has reached its video quota. | | MaxFileSizeError | The uploaded file exceeds the maximum allowed size. | | InvalidURLError | The provided URL is invalid or unreachable. | | AlreadyUploadedError | The video has already been uploaded. | | TooManyWatermarksError | The account has reached the watermark profile limit. | Use a `try...catch` block to handle errors: ```js export default { async fetch(request, env) { try { const videoDetails = await env.STREAM.upload( "https://example.com/video.mp4", ); return Response.json(videoDetails); } catch (e) { if (e instanceof Error) { return new Response(`Stream error: ${e.message}`, { status: 500 }); } throw e; } }, }; ``` ```ts export default { async fetch(request, env) { try { const videoDetails = await env.STREAM.upload("https://example.com/video.mp4"); return Response.json(videoDetails); } catch (e) { if (e instanceof Error) { return new Response(`Stream error: ${e.message}`, { status: 500 }); } throw e; } }, }; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/stream/manage-video-library/bindings/#page","headline":"Bind to Workers API · Cloudflare Stream docs","description":"A binding connects your Worker to external resources on the Developer Platform, like Stream, R2 buckets, or KV namespaces.","url":"https://developers.cloudflare.com/stream/manage-video-library/bindings/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-07","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Vectorize Workers binding API for inserting, upserting, querying, and managing vectors. title: Vectorize API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/vectorize/llms.txt > Use this file to discover all available pages before exploring further. # Vectorize API Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/vectorize/reference/client-api/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) This page covers the Vectorize API available within [Cloudflare Workers](https://developers.cloudflare.com/workers/), including usage examples. ## Operations ### Insert vectors ```ts let vectorsToInsert = [ { id: "123", values: [32.4, 6.5, 11.2, 10.3, 87.9] }, { id: "456", values: [2.5, 7.8, 9.1, 76.9, 8.5] }, ]; let inserted = await env.YOUR_INDEX.insert(vectorsToInsert); ``` Inserts vectors into the index. Vectorize inserts are asynchronous and the insert operation returns a mutation identifier unique for that operation. It typically takes a few seconds for inserted vectors to be available for querying in a Vectorize index. If vectors with the same vector ID already exist in the index, only the vectors with new IDs will be inserted. If you need to update existing vectors, use the [upsert](#upsert-vectors) operation. ### Upsert vectors ```ts let vectorsToUpsert = [ { id: "123", values: [32.4, 6.5, 11.2, 10.3, 87.9] }, { id: "456", values: [2.5, 7.8, 9.1, 76.9, 8.5] }, { id: "768", values: [29.1, 5.7, 12.9, 15.4, 1.1] }, ]; let upserted = await env.YOUR_INDEX.upsert(vectorsToUpsert); ``` Upserts vectors into an index. Vectorize upserts are asynchronous and the upsert operation returns a mutation identifier unique for that operation. It typically takes a few seconds for upserted vectors to be available for querying in a Vectorize index. An upsert operation will insert vectors into the index if vectors with the same ID do not exist, and overwrite vectors with the same ID. Upserting does not merge or combine the values or metadata of an existing vector with the upserted vector: the upserted vector replaces the existing vector in full. ### Query vectors ```ts let queryVector = [32.4, 6.55, 11.2, 10.3, 87.9]; let matches = await env.YOUR_INDEX.query(queryVector); ``` Query an index with the provided vector, returning the score(s) of the closest vectors based on the configured distance metric. * Configure the number of returned matches by setting `topK` (default: 5) * Return vector values by setting `returnValues: true` (default: false) * Return vector metadata by setting `returnMetadata: 'indexed'` or `returnMetadata: 'all'` (default: 'none') ```ts let matches = await env.YOUR_INDEX.query(queryVector, { topK: 5, returnValues: true, returnMetadata: "all", }); ``` #### topK The `topK` can be configured to specify the number of matches returned by the query operation. Vectorize now supports an upper limit of `100` for the `topK` value. However, for a query operation with `returnValues` set to `true` or `returnMetadata` set to `all`, `topK` is limited to a maximum value of `50`. #### returnMetadata The `returnMetadata` field provides three ways to fetch vector metadata while querying: 1. `none`: Do not fetch metadata. 2. `indexed`: Fetched metadata only for the indexed metadata fields. There is no latency overhead with this option, but long text fields may be truncated. 3. `all`: Fetch all metadata associated with a vector. Queries may run slower with this option, and `topK` is limited to 50. \`topK\` and \`returnMetadata\` for legacy Vectorize indexes For legacy Vectorize (V1) indexes, `topK` is limited to 20, and the `returnMetadata` is a boolean field. ### Query vectors by ID ```ts let matches = await env.YOUR_INDEX.queryById("some-vector-id"); ``` Query an index using a vector that is already present in the index. Query options remain the same as the query operation described above. ```ts let matches = await env.YOUR_INDEX.queryById("some-vector-id", { topK: 5, returnValues: true, returnMetadata: "all", }); ``` ### Get vectors by ID ```ts let ids = ["11", "22", "33", "44"]; const vectors = await env.YOUR_INDEX.getByIds(ids); ``` Retrieves the specified vectors by their ID, including values and metadata. ### Delete vectors by ID ```ts let idsToDelete = ["11", "22", "33", "44"]; const deleted = await env.YOUR_INDEX.deleteByIds(idsToDelete); ``` Deletes the vector IDs provided from the current index. Vectorize deletes are asynchronous and the delete operation returns a mutation identifier unique for that operation. It typically takes a few seconds for vectors to be removed from the Vectorize index. ### Retrieve index details ```ts const details = await env.YOUR_INDEX.describe(); ``` Retrieves the configuration of a given index directly, including its configured `dimensions` and distance `metric`. ### List Vectors Python SDK availability The `client.vectorize.indexes.list_vectors()` method is not yet available in the current release of the [Cloudflare Python SDK ↗](https://pypi.org/project/cloudflare/). While the method appears in the [API reference](https://developers.cloudflare.com/api/python/resources/vectorize/subresources/indexes/methods/list%5Fvectors/), it has not been included in a published SDK version as of v4.3.1\. In the meantime, you can use the [REST API](https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/list%5Fvectors/) or the Wrangler CLI to list vectors. List all vector identifiers in an index using paginated requests, returning up to 1000 vector identifiers per page. ```sh wrangler vectorize list-vectors [--count=] [--cursor=] ``` **Parameters:** * `` \- The name of your Vectorize index * `--count` (optional) - Number of vector IDs to return per page. Must be between 1 and 1000 (default: 100) * `--cursor` (optional) - Pagination cursor from the previous response to continue listing from that position For detailed guidance on pagination behavior and best practices, refer to [List vectors best practices](https://developers.cloudflare.com/vectorize/best-practices/list-vectors/). ### Create Metadata Index Enable metadata filtering on the specified property. Limited to 10 properties. Wrangler version 3.71.0 required Vectorize V2 requires [wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) version `3.71.0` or later. Ensure you have the latest version of `wrangler` installed, or use `npx wrangler@latest vectorize` to always use the latest version. Run the following `wrangler vectorize` command: ```sh wrangler vectorize create-metadata-index --property-name='some-prop' --type='string' ``` ### Delete Metadata Index Allow Vectorize to delete the specified metadata index. Wrangler version 3.71.0 required Vectorize V2 requires [wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) version `3.71.0` or later. Ensure you have the latest version of `wrangler` installed, or use `npx wrangler@latest vectorize` to always use the latest version. Run the following `wrangler vectorize` command: ```sh wrangler vectorize delete-metadata-index --property-name='some-prop' ``` ### List Metadata Indexes List metadata properties on which metadata filtering is enabled. Wrangler version 3.71.0 required Vectorize V2 requires [wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) version `3.71.0` or later. Ensure you have the latest version of `wrangler` installed, or use `npx wrangler@latest vectorize` to always use the latest version. Run the following `wrangler vectorize` command: ```sh wrangler vectorize list-metadata-index ``` ### Get Index Info Get additional details about the index. Wrangler version 3.71.0 required Vectorize V2 requires [wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) version `3.71.0` or later. Ensure you have the latest version of `wrangler` installed, or use `npx wrangler@latest vectorize` to always use the latest version. Run the following `wrangler vectorize` command: ```sh wrangler vectorize info ``` ## Vectors A vector represents the vector embedding output from a machine learning model. * `id` \- a unique `string` identifying the vector in the index. This should map back to the ID of the document, object or database identifier that the vector values were generated from. * `namespace` \- an optional partition key within a index. Operations are performed per-namespace, so this can be used to create isolated segments within a larger index. * `values` \- an array of `number`, `Float32Array`, or `Float64Array` as the vector embedding itself. This must be a dense array, and the length of this array must match the `dimensions` configured on the index. * `metadata` \- an optional set of key-value pairs that can be used to store additional metadata alongside a vector. ```ts let vectorExample = { id: "12345", values: [32.4, 6.55, 11.2, 10.3, 87.9], metadata: { key: "value", hello: "world", url: "r2://bucket/some/object.json", }, }; ``` ## Binding to a Worker [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) allow you to attach resources, including Vectorize indexes or R2 buckets, to your Worker. Bindings are defined in either the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) associated with your Workers project, or via the Cloudflare dashboard for your project. Vectorize indexes are bound by name. A binding for an index named `production-doc-search` would resemble the below: ```jsonc { "vectorize": [ { "binding": "PROD_SEARCH", // the index will be available as env.PROD_SEARCH in your Worker "index_name": "production-doc-search", }, ], } ``` ```toml [[vectorize]] binding = "PROD_SEARCH" index_name = "production-doc-search" ``` Refer to the [bindings documentation](https://developers.cloudflare.com/workers/wrangler/configuration/#vectorize-indexes) for more details. ## TypeScript Types If you're using TypeScript, run [wrangler types](https://developers.cloudflare.com/workers/wrangler/commands/general/#types) whenever you modify your Wrangler configuration file. This generates types for the `env` object based on your bindings, as well as [runtime types](https://developers.cloudflare.com/workers/languages/typescript/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/vectorize/reference/client-api/#page","headline":"Vectorize API · Cloudflare Vectorize docs","description":"Vectorize Workers binding API for inserting, upserting, querying, and managing vectors.","url":"https://developers.cloudflare.com/vectorize/reference/client-api/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Exposes Worker version metadata (`versionID` and `versionTag`). These fields can be added to events emitted from the Worker to send to downstream observability systems. title: Version metadata image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Version metadata Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The version metadata binding can be used to access metadata associated with a [version](https://developers.cloudflare.com/workers/versions-and-deployments/#versions) from inside the Workers runtime. Worker version ID, version tag and timestamp of when the version was created are available through the version metadata binding. They can be used in events sent to [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) or to any third-party analytics/metrics service in order to aggregate by Worker version. To use the version metadata binding, update your Worker's Wrangler file: ```jsonc { "version_metadata": { "binding": "CF_VERSION_METADATA" } } ``` ```toml [version_metadata] binding = "CF_VERSION_METADATA" ``` ### Interface An example of how to access the version ID and version tag from within a Worker to send events to [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/): ```js export default { async fetch(request, env, ctx) { const { id: versionId, tag: versionTag, timestamp: versionTimestamp } = env.CF_VERSION_METADATA; env.WAE.writeDataPoint({ indexes: [versionId], blobs: [versionTag, versionTimestamp], //... }); //... }, }; ``` ```ts interface Environment { CF_VERSION_METADATA: WorkerVersionMetadata; WAE: AnalyticsEngineDataset; } export default { async fetch(request, env, ctx) { const { id: versionId, tag: versionTag } = env.CF_VERSION_METADATA; env.WAE.writeDataPoint({ indexes: [versionId], blobs: [versionTag], //... }); //... }, } satisfies ExportedHandler; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/#page","headline":"Version metadata binding · Cloudflare Workers docs","description":"Exposes Worker version metadata (versionID and versionTag). These fields can be added to events emitted from the Worker to send to downstream observability systems.","url":"https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Spin up isolated Workers on demand to execute code. title: Dynamic Workers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/dynamic-workers/llms.txt > Use this file to discover all available pages before exploring further. # Dynamic Workers Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/dynamic-workers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Spin up Workers at runtime to execute code on-demand in a secure, sandboxed environment. Dynamic Workers let you spin up an unlimited number of Workers to execute arbitrary code specified at runtime. Dynamic Workers can be used as a lightweight alternative to containers for securely sandboxing code you don't trust. Dynamic Workers are the lowest-level primitive for spinning up a Worker, giving you full control over defining how the Worker is composed, which bindings it receives, whether it can reach the network, and more. ### Get started Deploy the [Dynamic Workers Playground ↗](https://github.com/cloudflare/agents/tree/main/examples/dynamic-workers-playground) to create and run Workers dynamically from code you write or import from GitHub, with real-time logs and observability. [![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/dinasaur404/dynamic-workers-playground) ## Use Dynamic Workers for Use this pattern when code needs to run quickly in a secure, isolated environment. * **AI Agent "Code Mode"**: LLMs are trained to write code. Instead of supplying an agent with tool calls to perform tasks, give it an API and let it write and execute code. Save up to 80% in inference tokens and cost by allowing the agent to programmatically process data instead of sending it all through the LLM. * **AI-generated applications / "Vibe Code"**: Run generated code for prototypes, projects, and automations in a secure, isolated sandboxed environment. * **Fast development and previews**: Load prototypes, previews, and playgrounds in milliseconds. * **Custom automations**: Create custom tools on the fly that execute a task, call an integration, or automate a workflow. * **Platforms**: Run applications uploaded by your users. ## Features Because you compose the Worker that runs the code at runtime, you control how that Worker is configured and what it can access. * **[Bindings](https://developers.cloudflare.com/dynamic-workers/usage/bindings/)**: Decide which bindings and structured data the dynamic Worker receives. * **[Observability](https://developers.cloudflare.com/dynamic-workers/usage/observability/)**: Attach Tail Workers and capture logs for each run. * **[Network access](https://developers.cloudflare.com/dynamic-workers/usage/egress-control/)**: Intercept or block Internet access for outbound requests. * **[Limits](https://developers.cloudflare.com/dynamic-workers/usage/limits/)**: Enforce custom limits on the dynamic Worker's resource usage. * **[Durable Object Facets](https://developers.cloudflare.com/dynamic-workers/usage/durable-object-facets/)**: Run dynamically-loaded code as a Durable Object with its own isolated SQLite storage. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/dynamic-workers/#page","headline":"Dynamic Workers · Cloudflare Dynamic Workers docs","description":"Spin up isolated Workers on demand to execute code.","url":"https://developers.cloudflare.com/dynamic-workers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Build durable, multi-step applications on Cloudflare Workers that automatically retry and persist state. title: Cloudflare Workflows image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workflows/llms.txt > Use this file to discover all available pages before exploring further. # Cloudflare Workflows Last updated Jun 2, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workflows/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Build durable multi-step applications on Cloudflare Workers with Workflows. Available on Free and Paid plans With Workflows, you can build applications that chain together multiple steps, automatically retry failed tasks, and persist state for minutes, hours, or even weeks - with no infrastructure to manage. Use Workflows to build reliable AI applications, process data pipelines, manage user lifecycle with automated emails and trial expirations, and implement human-in-the-loop approval systems. **Workflows give you:** * Durable multi-step execution without timeouts * The ability to pause for external events or approvals * Automatic retries and error handling * Built-in observability and debugging ## Example An image processing workflow that fetches from R2, generates an AI description, waits for approval, then publishes: ```ts export class ImageProcessingWorkflow extends WorkflowEntrypoint { async run(event: WorkflowEvent, step: WorkflowStep) { const imageData = await step.do('fetch image', async () => { const object = await this.env.BUCKET.get(event.payload.imageKey); return await object.arrayBuffer(); }); const description = await step.do('generate description', async () => { const imageArray = Array.from(new Uint8Array(imageData)); return await this.env.AI.run('@cf/llava-hf/llava-1.5-7b-hf', { image: imageArray, prompt: 'Describe this image in one sentence', max_tokens: 50, }); }); await step.waitForEvent('await approval', { event: 'approved', timeout: '24 hours', }); await step.do('publish', async () => { await this.env.BUCKET.put(`public/${event.payload.imageKey}`, imageData); }); } } ``` [Get started](https://developers.cloudflare.com/workflows/get-started/guide/)[Browse the examples](https://developers.cloudflare.com/workflows/examples/) --- ## Features [Durable step execution](https://developers.cloudflare.com/workflows/build/workers-api/) Break complex operations into durable steps with automatic retries and error handling. Learn about steps [Sleep and scheduling](https://developers.cloudflare.com/workflows/build/sleeping-and-retrying/) Pause workflows for seconds, hours, or days with `step.sleep()` and `step.sleepUntil()`. Add delays [Wait for external events](https://developers.cloudflare.com/workflows/build/events-and-parameters/) Wait for webhooks, user input, or external system responses before continuing execution. Handle events [Workflow lifecycle management](https://developers.cloudflare.com/workflows/build/trigger-workflows/) Trigger, pause, resume, and terminate workflow instances programmatically or via API. Manage instances --- ## Related products [Workers](https://developers.cloudflare.com/workers/) Build serverless applications and deploy instantly across the globe for exceptional performance, reliability, and scale. [Pages](https://developers.cloudflare.com/pages/) Deploy dynamic front-end applications in record time. --- ## More resources ### [Pricing](https://developers.cloudflare.com/workflows/reference/pricing/) Learn more about how Workflows is priced. ### [Limits](https://developers.cloudflare.com/workflows/reference/limits/) Learn more about Workflow limits, and how to work within them. ### [Storage options](https://developers.cloudflare.com/workers/platform/storage-options/) Learn more about the storage and database options you can build on with Workers. ### [Developer Discord](https://discord.cloudflare.com) Connect with the Workers community on Discord to ask questions, show what you are building, and discuss the platform with other developers. ### [@CloudflareDev](https://x.com/cloudflaredev) Follow @CloudflareDev on Twitter to learn about product announcements, and what is new in Cloudflare Developer Platform. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workflows/#page","headline":"Overview · Cloudflare Workflows docs","description":"Build durable, multi-step applications on Cloudflare Workers that automatically retry and persist state.","url":"https://developers.cloudflare.com/workflows/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-02","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Control reading and writing from the Cloudflare global network cache. title: Cache image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Cache Last updated Jul 6, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/cache/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The [Cache API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Cache) allows fine grained control of reading and writing from the [Cloudflare global network ↗](https://www.cloudflare.com/network/) cache. Note The Cache API is a programmatic interface for reading from and writing to Cloudflare's cache from inside a Worker. To cache responses from your Worker so that Cloudflare returns them without executing your Worker, use [Workers Caching](https://developers.cloudflare.com/workers/cache/) instead. The two mechanisms are independent. The Cache API is available globally but the contents of the cache do not replicate outside of the originating data center. A `GET /users` response can be cached in the originating data center, but will not exist in another data center unless it has been explicitly created. Tiered caching The `cache.put` method is not compatible with tiered caching. Refer to [Cache API](https://developers.cloudflare.com/workers/reference/how-the-cache-works/#cache-api) for more information. To perform tiered caching, use the [fetch API](https://developers.cloudflare.com/workers/reference/how-the-cache-works/#interact-with-the-cloudflare-cache). Workers deployed to custom domains have access to functional `cache` operations. So do [Pages functions](https://developers.cloudflare.com/pages/functions/), whether attached to custom domains or `*.pages.dev` domains. However, any Cache API operations in the Cloudflare Workers dashboard editor and [Playground](https://developers.cloudflare.com/workers/playground/) previews will have no impact. For Workers fronted by [Cloudflare Access ↗](https://www.cloudflare.com/teams/access/), the Cache API is not currently available. Note This individualized zone cache object differs from Cloudflare’s Global CDN. For details, refer to [How the cache works](https://developers.cloudflare.com/workers/reference/how-the-cache-works/). --- ## Accessing Cache The `caches.default` API is strongly influenced by the web browsers’ Cache API, but there are some important differences. For instance, Cloudflare Workers runtime exposes a single global cache object. ```js let cache = caches.default; await cache.match(request); ``` You may create and manage additional Cache instances via the [caches.open ↗](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/open) method. ```js let myCache = await caches.open('custom:cache'); await myCache.match(request); ``` --- ## Headers Our implementation of the Cache API respects the following HTTP headers on the response passed to `put()`: * `Cache-Control` * Controls caching directives. This is consistent with [Cloudflare Cache-Control Directives](https://developers.cloudflare.com/cache/concepts/cache-control#cache-control-directives). Refer to [Edge TTL](https://developers.cloudflare.com/cache/how-to/configure-cache-status-code#edge-ttl) for a list of HTTP response codes and their TTL when `Cache-Control` directives are not present. * `Cache-Tag` * Allows resource purging by tag(s) later. * `ETag` * Allows `cache.match()` to evaluate conditional requests with `If-None-Match`. * `Expires` string * A string that specifies when the resource becomes invalid. * `Last-Modified` * Allows `cache.match()` to evaluate conditional requests with `If-Modified-Since`. This differs from the web browser Cache API as they do not honor any headers on the request or response. Note Responses with `Set-Cookie` headers are never cached, because this sometimes indicates that the response contains unique data. To store a response with a `Set-Cookie` header, either delete that header or set `Cache-Control: private=Set-Cookie` on the response before calling `cache.put()`. Use the `Cache-Control` method to store the response without the `Set-Cookie` header. --- ## Methods ### `Put` ```js cache.put(request, response); ``` * `put(request, response)` : Promise * Attempts to add a response to the cache, using the given request as the key. Returns a promise that resolves to `undefined` regardless of whether the cache successfully stored the response. Note The `stale-while-revalidate` and `stale-if-error` directives are not supported when using the `cache.put` or `cache.match` methods. #### Parameters * `request` string | Request * Either a string or a [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) object to serve as the key. If a string is passed, it is interpreted as the URL for a new Request object. * `response` Response * A [Response](https://developers.cloudflare.com/workers/runtime-apis/response/) object to store under the given key. #### Invalid parameters `cache.put` will throw an error if: * The `request` passed is a method other than `GET`. * The `response` passed has a `status` of [206 Partial Content ↗](https://www.webfx.com/web-development/glossary/http-status-codes/what-is-a-206-status-code/). * The `response` passed contains the header `Vary: *`. The value of the `Vary` header is an asterisk (`*`). Refer to the [Cache API specification ↗](https://w3c.github.io/ServiceWorker/#cache-put) for more information. #### Errors `cache.put` returns a `413` error if `Cache-Control` instructs not to cache or if the response is too large. Note `cache.put` will silently reject a `301` or `302` redirect response if both of the following are true: 1. The cache key does not include the query string (for example, a custom cache key set via Workers or Page Rules that strips the query string). 2. The `Location` header in the redirect response contains the request's query string. This is a cache-poisoning mitigation. To cache redirect responses with query strings, either include the query string in your cache key, or remove the query string from the `Location` header before calling `cache.put()`. This restriction does not apply on `.workers.dev` domains, which include the query string in the cache key by default. ### `Match` ```js cache.match(request, options); ``` * `match(request, options)` : Promise`` * Returns a promise wrapping the response object keyed to that request. Note The `stale-while-revalidate` and `stale-if-error` directives are not supported when using the `cache.put` or `cache.match` methods. #### Parameters * `request` string | Request * The string or [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) object used as the lookup key. Strings are interpreted as the URL for a new `Request` object. * `options` * Can contain one possible property: `ignoreMethod` (Boolean). When `true`, the request is considered to be a `GET` request regardless of its actual value. Unlike the browser Cache API, Cloudflare Workers do not support the `ignoreSearch` or `ignoreVary` options on `match()`. You can accomplish this behavior by removing query strings or HTTP headers at `put()` time. Our implementation of the Cache API respects the following HTTP headers on the request passed to `match()`: * `Range` * Results in a `206` response if a matching response with a Content-Length header is found. Your Cloudflare cache always respects range requests, even if an `Accept-Ranges` header is on the response. * `If-Modified-Since` * Results in a `304` response if a matching response is found with a `Last-Modified` header with a value before the time specified in `If-Modified-Since`. * `If-None-Match` * Results in a `304` response if a matching response is found with an `ETag` header with a value that matches a value in `If-None-Match`. Note `cache.match()` never sends a subrequest to the origin. If no matching response is found in cache, the promise that `cache.match()` returns is fulfilled with `undefined`. #### Errors `cache.match` generates a `504` error response when the requested content is missing or expired. The Cache API does not expose this `504` directly to the Worker script, instead returning `undefined`. Nevertheless, the underlying `504` is still visible in Cloudflare Logs. If you use Cloudflare Logs, you may see these `504` responses with the `RequestSource` of `edgeWorkerCacheAPI`. Again, these are expected if the cached asset was missing or expired. Note that `edgeWorkerCacheAPI` requests are already filtered out in other views, such as Cache Analytics. To filter out these requests or to filter requests by end users of your website only, refer to [Filter end users](https://developers.cloudflare.com/analytics/graphql-api/features/filtering/#filter-end-users). ### `Delete` ```js cache.delete(request, options); ``` * `delete(request, options)` : Promise`` Deletes the `Response` object from the cache and returns a `Promise` for a Boolean response: * `true`: The response was cached but is now deleted * `false`: The response was not in the cache at the time of deletion. Global purges The `cache.delete` method only purges content of the cache in the data center that the Worker was invoked. For global purges, refer to [Purging assets stored with the Cache API](https://developers.cloudflare.com/workers/reference/how-the-cache-works/#purge-assets-stored-with-the-cache-api). #### Parameters * `request` string | Request * The string or [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) object used as the lookup key. Strings are interpreted as the URL for a new `Request` object. * `options` object * Can contain one possible property: `ignoreMethod` (Boolean). Consider the request method a GET regardless of its actual value. --- ## Related resources * [How the cache works](https://developers.cloudflare.com/workers/reference/how-the-cache-works/) * [Example: Cache using fetch()](https://developers.cloudflare.com/workers/examples/cache-using-fetch/) * [Example: using the Cache API](https://developers.cloudflare.com/workers/examples/cache-api/) * [Example: caching POST requests](https://developers.cloudflare.com/workers/examples/cache-post-request/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/cache/#page","headline":"Cache · Cloudflare Workers docs","description":"Control reading and writing from the Cloudflare global network cache.","url":"https://developers.cloudflare.com/workers/runtime-apis/cache/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-06","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Supported methods of the `console` API in Cloudflare Workers title: Console image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Console Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/console/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The `console` object provides a set of methods to help you emit logs, warnings, and debug code. All standard [methods of the console API ↗](https://developer.mozilla.org/en-US/docs/Web/API/console) are present on the `console` object in Workers. However, some methods are no ops — they can be called, and do not emit an error, but do not do anything. This ensures compatibility with libraries which may use these APIs. The table below enumerates each method, and the extent to which it is supported in Workers. All methods noted as "✅ supported" have the following behavior: * They will be written to the console in local dev (`npx wrangler@latest dev`) * They will appear in live logs, when tailing logs in the dashboard or running [wrangler tail ↗](https://developers.cloudflare.com/workers/observability/log-from-workers/#use-wrangler-tail) * They will create entries in the `logs` field of [Tail Worker ↗](https://developers.cloudflare.com/workers/observability/tail-workers/) events and [Workers Trace Events ↗](https://developers.cloudflare.com/logs/logpush/logpush-job/datasets/account/workers%5Ftrace%5Fevents/), which can be pushed to a destination of your choice via [Logpush ↗](https://developers.cloudflare.com/workers/observability/logpush/). All methods noted as "🟡 partial support" have the following behavior: * In both production and local development the method can be safely called, but will do nothing (no op) * In the [Workers Playground ↗](https://workers.cloudflare.com/playground), Quick Editor in the Workers dashboard, and remote preview mode (`wrangler dev --remote`) calling the method will behave as expected, print to the console, etc. Refer to [Log from Workers ↗](https://developers.cloudflare.com/workers/observability/log-from-workers/) for more on debugging and adding logs to Workers. | Method | Behavior | | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | [console.debug() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/debug%5Fstatic) | ✅ supported | | [console.error() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/error%5Fstatic) | ✅ supported | | [console.info() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/info%5Fstatic) | ✅ supported | | [console.log() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/log%5Fstatic) | ✅ supported | | [console.warn() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/warn%5Fstatic) | ✅ supported | | [console.clear() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/clear%5Fstatic) | 🟡 partial support | | [console.count() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/count%5Fstatic) | 🟡 partial support | | [console.group() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/group%5Fstatic) | 🟡 partial support | | [console.table() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/table%5Fstatic) | 🟡 partial support | | [console.trace() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/trace%5Fstatic) | 🟡 partial support | | [console.assert() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/assert%5Fstatic) | ⚪ no op | | [console.countReset() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/countreset%5Fstatic) | ⚪ no op | | [console.dir() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/dir%5Fstatic) | ⚪ no op | | [console.dirxml() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/dirxml%5Fstatic) | ⚪ no op | | [console.groupCollapsed() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/groupcollapsed%5Fstatic) | ⚪ no op | | [console.groupEnd ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/groupend%5Fstatic) | ⚪ no op | | [console.profile() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/profile%5Fstatic) | ⚪ no op | | [console.profileEnd() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/profileend%5Fstatic) | ⚪ no op | | [console.time() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/time%5Fstatic) | ⚪ no op | | [console.timeEnd() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/timeend%5Fstatic) | ⚪ no op | | [console.timeLog() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/timelog%5Fstatic) | ⚪ no op | | [console.timeStamp() ↗](https://developer.mozilla.org/en-US/docs/Web/API/console/timestamp%5Fstatic) | ⚪ no op | | [console.createTask() ↗](https://developer.chrome.com/blog/devtools-modern-web-debugging/#linked-stack-traces) | 🔴 Will throw an exception in production, but works in local dev, Quick Editor, and remote preview | Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/console/#page","headline":"Console · Cloudflare Workers docs","description":"Supported methods of the console API in Cloudflare Workers","url":"https://developers.cloudflare.com/workers/runtime-apis/console/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: The Context API in Cloudflare Workers, including props, exports, waitUntil and passThroughOnException. title: Context (ctx) image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Context (ctx) Last updated Aug 12, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/context/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The Context API provides methods to manage the lifecycle of your Worker or Durable Object. Context is exposed via the following places: * As the third parameter in all [handlers](https://developers.cloudflare.com/workers/runtime-apis/handlers/), including the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). (`fetch(request, env, ctx)`) * As a class property of the [WorkerEntrypoint class](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc) (`this.ctx`) Note that the Context API is available strictly in stateless contexts, that is, not [Durable Objects](https://developers.cloudflare.com/durable-objects/). However, Durable Objects have a different object, the [Durable Object State](https://developers.cloudflare.com/durable-objects/api/state/), which is available as `this.ctx` inside a Durable Object class, and provides some of the same functionality as the Context API. ## `props` `ctx.props` provides a way to pass additional configuration to a worker based on the context in which it was invoked. For example, when your Worker is called by another Worker, `ctx.props` can provide information about the calling worker. For example, imagine that you are configuring a Worker called "frontend-worker", which must talk to another Worker called "doc-worker" in order to manipulate documents. You might configure "frontend-worker" with a [Service Binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings) like: ```jsonc { "services": [ { "binding": "DOC_SERVICE", "service": "doc-worker", "entrypoint": "DocServiceApi", "props": { "clientId": "frontend-worker", "permissions": [ "read", "write" ] } } ] } ``` ```toml [[services]] binding = "DOC_SERVICE" service = "doc-worker" entrypoint = "DocServiceApi" [services.props] clientId = "frontend-worker" permissions = [ "read", "write" ] ``` Now frontend-worker can make calls to doc-worker with code like `env.DOC_SERVICE.getDoc(id)`. This will make a [Remote Procedure Call](https://developers.cloudflare.com/workers/runtime-apis/rpc/) invoking the method `getDoc()` of the class `DocServiceApi`, a [WorkerEntrypoint class](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc) exported by doc-worker. The configuration contains a `props` value. This in an arbitrary JSON value. When the `DOC_SERVICE` binding is used, the `DocServiceApi` instance receiving the call will be able to access this `props` value as `this.ctx.props`. Here, we've configured `props` to specify that the call comes from frontend-worker, and that it should be allowed to read and write documents. However, the contents of `props` can be anything you want. The Workers platform is designed to ensure that `ctx.props` can only be set by someone who has permission to edit and deploy the worker to which it is being delivered. This means that you can trust that the content of `ctx.props` is authentic. There is no need to use secret keys or cryptographic signatures in a `ctx.props` value. `ctx.props` can also be used to configure an RPC interface to represent a _specific_ resource, thus creating a "custom binding". For example, we could configure a Service Binding to our "doc-worker" which grants access only to a specific document: ```jsonc { "services": [ { "binding": "FOO_DOCUMENT", "service": "doc-worker", "entrypoint": "DocumentApi", "props": { "docId": "e366592caec1d88dff724f74136b58b5", "permissions": [ "read", "write" ] } } ] } ``` ```toml [[services]] binding = "FOO_DOCUMENT" service = "doc-worker" entrypoint = "DocumentApi" [services.props] docId = "e366592caec1d88dff724f74136b58b5" permissions = [ "read", "write" ] ``` Here, we've placed a `docId` property in `ctx.props`. The `DocumentApi` class could be designed to provide an API to the specific document identified by `ctx.props.docId`, and enforcing the given permissions. ## `exports` Compatibility flag required To use `ctx.exports`, you must use [the enable\_ctx\_exports compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags#enable-ctxexports). `ctx.exports` provides automatically-configured "loopback" bindings for all of your top-level exports. * For each top-level export that `extends WorkerEntrypoint` (or simply implements a fetch handler), `ctx.exports` automatically contains a [Service Binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings). * For each top-level export that `extends DurableObject` (and which has been configured with storage via a [migration](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/)), `ctx.exports` automatically contains a [Durable Object namespace binding](https://developers.cloudflare.com/durable-objects/api/namespace/). For example: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class Greeter extends WorkerEntrypoint { greet(name) { return `Hello, ${name}!`; } } export default { async fetch(request, env, ctx) { let greeting = await ctx.exports.Greeter.greet("World"); return new Response(greeting); }, }; ``` In this example, the default fetch handler calls the `Greeter` class over RPC, like how you'd use a Service Binding. However, there is no external configuration required. `ctx.exports` is populated _automatically_ from your top-level imports. ### Specifying `ctx.props` when using `ctx.exports` Loopback Service Bindings in `ctx.exports` have an extra capability that regular Service Bindings do not: the caller can specify the value of `ctx.props` that should be delivered to the callee. ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class Greeter extends WorkerEntrypoint { greet(name) { return `${this.ctx.props.greeting}, ${name}!`; } } export default { async fetch(request, env, ctx) { // Make a custom greeter that uses the greeting "Welcome". let greeter = ctx.exports.Greeter({ props: { greeting: "Welcome" } }); // Greet the world. Returns "Welcome, World!" let greeting = await greeter.greet("World"); return new Response(greeting); }, }; ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; type Props = { greeting: string; }; export class Greeter extends WorkerEntrypoint { greet(name) { return `${this.ctx.props.greeting}, ${name}!`; } } export default { async fetch(request, env, ctx) { // Make a custom greeter that uses the greeting "Welcome". let greeter = ctx.exports.Greeter({ props: { greeting: "Welcome" } }); // Greet the world. Returns "Welcome, World!" let greeting = await greeter.greet("World"); return new Response(greeting); }, } satisfies ExportedHandler; ``` Specifying props dynamically is permitted in this case because the caller is the same Worker, and thus can be presumed to be trusted to specify any props. The ability to customize props is particularly useful when the resulting binding is to be passed to another Worker over RPC or used in the `env` of a [dynamically-loaded worker](https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/). Note that `props` values specified in this way are allowed to contain any "persistently" serializable type. This includes all basic [structured clonable data types ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm). It also includes Service Bindings themselves: you can place a Service Binding into the `props` of another Service Binding. ### TypeScript types for `ctx.exports` and `ctx.props` If using TypeScript, you should use [the wrangler types command](https://developers.cloudflare.com/workers/wrangler/commands/general/#types) to auto-generate types for your project. The generated types will ensure `ctx.exports` is typed correctly. When declaring an entrypoint class that accepts `props`, make sure to declare it as `extends WorkerEntrypoint`, where `Props` is the type of `ctx.props`. See the example above. ## `tracing` `ctx.tracing` provides access to the [custom spans API](https://developers.cloudflare.com/workers/observability/traces/custom-spans/) for creating user-defined trace spans. This is the same object available via `import { tracing } from "cloudflare:workers"`. [Tracing must be enabled](https://developers.cloudflare.com/workers/observability/traces/#how-to-enable-tracing) on your Worker for spans to be recorded. ```js export default { async fetch(request, env, ctx) { return ctx.tracing.enterSpan("handleRequest", async (span) => { span.setAttribute("url.path", new URL(request.url).pathname); const data = await env.MY_KV.get("key"); return new Response(data); }); }, }; ``` For full API details, refer to [Custom spans](https://developers.cloudflare.com/workers/observability/traces/custom-spans/). ## `waitUntil` `ctx.waitUntil()` extends the lifetime of your Worker, allowing you to perform work without blocking returning a response, and that may continue after a response is returned. It accepts a `Promise`, which the Workers runtime will continue executing, even after a response has been returned by the Worker's [handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/). Background work needs \`await\` or \`waitUntil()\` An async call that is neither awaited nor passed to `ctx.waitUntil()` can be canceled when the invocation ends — dropping logs, leaving writes unfinished, or failing silently. Use `ctx.waitUntil()` for work that can run after the response is sent, such as logging, analytics, or cache writes, as long as the work can finish within the `waitUntil()` time limit. If the client is still receiving the response, including a streamed response body, the Worker invocation remains active without `ctx.waitUntil()`. If your response depends on the work, `await` the work before returning the response or stream the response as the work completes. `waitUntil` is commonly used to: * Fire off events to external analytics providers. (note that when you use [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/), you do not need to use `waitUntil`) * Put items into cache using the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) \`waitUntil\` has a 30-second time limit after invocation end For HTTP-triggered Workers, `ctx.waitUntil()` can extend execution for up to 30 seconds after the response is sent or the client disconnects. This is not a limit on the total wall time of an HTTP request. This time limit is shared across all `waitUntil()` calls within the same request. If any Promises have not settled after 30 seconds, they are canceled. When `waitUntil` tasks are canceled, the following warning will be logged to [Workers Logs](https://developers.cloudflare.com/workers/observability/logs/workers-logs/) and any attached [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/): `waitUntil() tasks did not complete within the allowed time after invocation end and have been cancelled.` If the work cannot finish within the `waitUntil()` time limit, send messages to a [Queue](https://developers.cloudflare.com/queues/) and process them in a separate consumer Worker. Queues provide reliable delivery and automatic retries. Alternatives to waitUntil If you are using `waitUntil()` to emit logs or exceptions, we recommend using [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) instead. Even if your Worker throws an uncaught exception, the Tail Worker will execute, ensuring that you can emit logs or exceptions regardless of the Worker's invocation status. [Cloudflare Queues](https://developers.cloudflare.com/queues/) is purpose-built for performing work out-of-band, without blocking returning a response back to the client Worker. \`waitUntil\` in Durable Objects Do not use `waitUntil()` to keep a Durable Object alive during normal request or RPC handling. Durable Objects remain active while they are handling requests, RPC calls, response streams, WebSockets, or pending I/O. [DurableObjectState.waitUntil()](https://developers.cloudflare.com/durable-objects/api/state/#waituntil) exists for API compatibility and is not needed for this behavior. You can call `waitUntil()` multiple times. Similar to `Promise.allSettled`, even if a promise passed to one `waitUntil` call is rejected, promises passed to other `waitUntil()` calls will still continue to execute. For example: ```js export default { async fetch(request, env, ctx) { // Forward / proxy original request let res = await fetch(request); // Add custom header(s) res = new Response(res.body, res); res.headers.set("x-foo", "bar"); // Cache the response // NOTE: Does NOT block / wait ctx.waitUntil(caches.default.put(request, res.clone())); // Done return res; }, }; ``` ## `passThroughOnException` Reuse of body The Workers runtime uses streaming for request and response bodies. It does not buffer the body. If an exception occurs after the body has been consumed, `passThroughOnException()` cannot send the body again. For a Worker that proxies requests to an origin, avoid relying on the runtime fallback when the origin `fetch()` fails. If the origin `fetch()` throws after consuming the request body, the fallback request may reach your origin without the original body and fail with an unrelated `4xx` error. Catch origin fetch errors and return a `5xx` response instead. This protects against uncaught code exceptions. It does not mitigate failures such as exceeding CPU or memory limits. The `passThroughOnException` method allows a Worker to [fail open ↗](https://community.microfocus.com/cyberres/b/sws-22/posts/security-fundamentals-part-1-fail-open-vs-fail-closed), and pass a request through to an origin server when a Worker throws an unhandled exception. This can be useful when using Workers as a layer in front of an existing service, allowing the service behind the Worker to handle any unexpected error cases that arise in your Worker. ```js export default { async fetch(request, env, ctx) { ctx.passThroughOnException(); try { return await fetch(request); } catch (error) { console.error("Origin fetch failed", error); return new Response("Bad Gateway", { status: 502 }); } }, }; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/context/#page","headline":"Context (ctx) · Cloudflare Workers docs","description":"The Context API in Cloudflare Workers, including props, exports, waitUntil and passThroughOnException.","url":"https://developers.cloudflare.com/workers/runtime-apis/context/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-12","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Takes a stream of code points as input and emits a stream of bytes. title: Encoding image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Encoding Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/encoding/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## TextEncoder ### Background The `TextEncoder` takes a stream of code points as input and emits a stream of bytes. Encoding types passed to the constructor are ignored and a UTF-8 `TextEncoder` is created. [TextEncoder() ↗](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/TextEncoder) returns a newly constructed `TextEncoder` that generates a byte stream with UTF-8 encoding. `TextEncoder` takes no parameters and throws no exceptions. ### Constructor ```js let encoder = new TextEncoder(); ``` ### Properties * `encoder.encoding` DOMString read-only * The name of the encoder as a string describing the method the `TextEncoder` uses (always `utf-8`). ### Methods * `encode(inputUSVString)` : Uint8Array * Encodes a string input. --- ## TextDecoder ### Background The `TextDecoder` interface represents a UTF-8 decoder. Decoders take a stream of bytes as input and emit a stream of code points. [TextDecoder() ↗](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder) returns a newly constructed `TextDecoder` that generates a code-point stream. ### Constructor ```js let decoder = new TextDecoder(); ``` ### Properties * `decoder.encoding` DOMString read-only * The name of the decoder that describes the method the `TextDecoder` uses. * `decoder.fatal` boolean read-only * Indicates if the error mode is fatal. * `decoder.ignoreBOM` boolean read-only * Indicates if the byte-order marker is ignored. ### Methods * `decode()` : DOMString * Decodes using the method specified in the `TextDecoder` object. Learn more at [MDN’s TextDecoder documentation ↗](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/decode). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/encoding/#page","headline":"Encoding · Cloudflare Workers docs","description":"Takes a stream of code points as input and emits a stream of bytes.","url":"https://developers.cloudflare.com/workers/runtime-apis/encoding/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: EventSource is a server-sent event API that allows a server to push events to a client. title: EventSource image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # EventSource Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/eventsource/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The [EventSource ↗](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) interface is a server-sent event API that allows a server to push events to a client. The `EventSource` object is used to receive server-sent events. It connects to a server over HTTP and receives events in a text-based format. ### Constructor ```js let eventSource = new EventSource(url, options); ``` * `url` USVString - The URL to which to connect. * `options` EventSourceInit - An optional dictionary containing any optional settings. By default, the `EventSource` will use the global `fetch()` function under the covers to make requests. If you need to use a different fetch implementation as provided by a Cloudflare Workers binding, you can pass the `fetcher` option: ```js export default { async fetch(req, env) { let eventSource = new EventSource(url, { fetcher: env.MYFETCHER }); // ... } }; ``` Note that the `fetcher` option is a Cloudflare Workers specific extension. ### Properties * `eventSource.url` USVString read-only * The URL of the event source. * `eventSource.readyState` USVString read-only * The state of the connection. * `eventSource.withCredentials` Boolean read-only * A Boolean indicating whether the `EventSource` object was instantiated with cross-origin (CORS) credentials set (`true`), or not (`false`). ### Methods * `eventSource.close()` * Closes the connection. * `eventSource.onopen` * An event handler called when a connection is opened. * `eventSource.onmessage` * An event handler called when a message is received. * `eventSource.onerror` * An event handler called when an error occurs. ### Events * `message` * Fired when a message is received. * `open` * Fired when the connection is opened. * `error` * Fired when an error occurs. ### Class Methods * `EventSource.from(readableStreamReadableStream) : EventSource` * This is a Cloudflare Workers specific extension that creates a new `EventSource` object from an existing `ReadableStream`. Such an instance does not initiate a new connection but instead attaches to the provided stream. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/eventsource/#page","headline":"EventSource · Cloudflare Workers docs","description":"EventSource is a server-sent event API that allows a server to push events to a client.","url":"https://developers.cloudflare.com/workers/runtime-apis/eventsource/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: An interface for asynchronously fetching resources via HTTP requests inside of a Worker. title: Fetch image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Fetch Last updated Jul 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/fetch/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The [Fetch API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI) provides an interface for asynchronously fetching resources via HTTP requests inside of a Worker. Note Asynchronous tasks such as `fetch` must be executed within a [handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/). If you try to call `fetch()` within [global scope ↗](https://developer.mozilla.org/en-US/docs/Glossary/Global%5Fscope), your Worker will throw an error. Learn more about [the Request context](https://developers.cloudflare.com/workers/runtime-apis/request/#the-request-context). Worker to Worker Worker-to-Worker `fetch` requests are possible with [Service bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/) or by enabling the [global\_fetch\_strictly\_public compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public). ## Syntax ```js export default { async scheduled(controller, env, ctx) { return await fetch("https://example.com", { headers: { "X-Source": "Cloudflare-Workers", }, }); }, }; ``` Service Workers are deprecated Service Workers are deprecated, but still supported. We recommend using [Module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) instead. New features may not be supported for Service Workers. ```js addEventListener("fetch", (event) => { // NOTE: can’t use fetch here, as we’re not in an async scope yet event.respondWith(eventHandler(event)); }); async function eventHandler(event) { // fetch can be awaited here since `event.respondWith()` waits for the Promise it receives to settle const resp = await fetch(event.request); return resp; } ``` ```python from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx): return await fetch("https://example.com", headers={"X-Source": "Cloudflare-Workers"}) ``` * `fetch(resource, options optional)` : Promise`` * Fetch returns a promise to a Response. ### Parameters * [resource ↗](https://developer.mozilla.org/en-US/docs/Web/API/fetch#resource) Request | string | URL * `options` options * `cache` `undefined | 'no-store' | 'no-cache'` optional * Standard HTTP `cache` header. Only `cache: 'no-store'` and `cache: 'no-cache'` are supported. Any other `cache` header will result in a `TypeError` with the message `Unsupported cache mode: `. \_ For all requests this forwards the `Pragma: no-cache` and `Cache-Control: no-cache` headers to the origin. \_ For `no-store`, requests to origins not hosted by Cloudflare bypass the use of Cloudflare's caches. \_ For `no-cache`, requests to origins not hosted by Cloudflare are forced to revalidate with the origin before responding. * An object that defines the content and behavior of the request. --- ## How the `Accept-Encoding` header is handled When making a subrequest with the `fetch()` API, you can specify which forms of compression to prefer that the server will respond with (if the server supports it) by including the [Accept-Encoding ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding) header. Workers supports both the gzip and brotli compression algorithms. Usually it is not necessary to specify `Accept-Encoding` or `Content-Encoding` headers in the Workers Runtime production environment – brotli or gzip compression is automatically requested when fetching from an origin and applied to the response when returning data to the client, depending on the capabilities of the client and origin server. To support requesting brotli from the origin, you must enable the [brotli\_content\_encoding](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#brotli-content-encoding-support) compatibility flag in your Worker. Soon, this compatibility flag will be enabled by default for all Workers past an upcoming compatibility date. ### Passthrough behavior One scenario where the Accept-Encoding header is useful is for passing through compressed data from a server to the client, where the Accept-Encoding allows the worker to directly receive the compressed data stream from the server without it being decompressed beforehand. As long as you do not read the body of the compressed response prior to returning it to the client and keep the `Content-Encoding` header intact, it will "pass through" without being decompressed and then recompressed again. This can be helpful when using Workers in front of origin servers or when fetching compressed media assets, to ensure that the same compression used by the origin server is used in the response that your Worker returns. In addition to a change in the content encoding, recompression is also needed when a response uses an encoding not supported by the client. As an example, when a Worker requests either brotli or gzip as the encoding but the client only supports gzip, recompression will still be needed if the server returns brotli-encoded data to the server (and will be applied automatically). Note that this behavior may also vary based on the [compression rules](https://developers.cloudflare.com/rules/compression-rules/), which can be used to configure what compression should be applied for different types of data on the server side. ```typescript export default { async fetch(request) { // Accept brotli or gzip compression const headers = new Headers({ "Accept-Encoding": "br, gzip", }); let response = await fetch("https://developers.cloudflare.com", { method: "GET", headers, }); // As long as the original response body is returned and the Content-Encoding header is // preserved, the same encoded data will be returned without needing to be compressed again. return new Response(response.body, { status: response.status, statusText: response.statusText, headers: response.headers, }); }, }; ``` ## Related resources * [Example: use fetch to respond with another site](https://developers.cloudflare.com/workers/examples/respond-with-another-site/) * [Example: Fetch HTML](https://developers.cloudflare.com/workers/examples/fetch-html/) * [Example: Fetch JSON](https://developers.cloudflare.com/workers/examples/fetch-json/) * [Example: cache using Fetch](https://developers.cloudflare.com/workers/examples/cache-using-fetch/) * Write your Worker code in [ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) for an optimized experience. * [Error 526](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-526/#error-526-in-the-workers-context) * [Fetch API in a partial setup](https://developers.cloudflare.com/workers/platform/known-issues/#fetch-api-in-cname-setup) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/fetch/#page","headline":"Fetch · Cloudflare Workers docs","description":"An interface for asynchronously fetching resources via HTTP requests inside of a Worker.","url":"https://developers.cloudflare.com/workers/runtime-apis/fetch/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Methods, such as `fetch()`, on Workers that can receive and process external inputs. title: Handlers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Handlers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/handlers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Handlers are methods on Workers that can receive and process external inputs, and can be invoked from outside your Worker. For example, the `fetch()` handler receives an HTTP request, and can return a response: ```js export default { async fetch(request, env, ctx) { return new Response('Hello World!'); }, }; ``` The following handlers are available within Workers: * [Alarm Handler](https://developers.cloudflare.com/durable-objects/api/alarms/) * [Email Handler](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/) * [Fetch Handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) * [Queue Handler](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer) * [Scheduled Handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) * [Tail Handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/) ## Handlers in Python Workers When you [write Workers in Python](https://developers.cloudflare.com/workers/languages/python/), handlers are placed in a class named `Default` that extends the [WorkerEntrypoint class](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/) (which you can import from the `workers` SDK module). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/handlers/#page","headline":"Handlers · Cloudflare Workers docs","description":"Methods, such as fetch(), on Workers that can receive and process external inputs.","url":"https://developers.cloudflare.com/workers/runtime-apis/handlers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Schedule future wake-ups for Durable Objects using the Alarms API with guaranteed at-least-once execution. title: Alarms image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/durable-objects/llms.txt > Use this file to discover all available pages before exploring further. # Alarms Last updated Apr 21, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/durable-objects/api/alarms/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Durable Objects alarms allow you to schedule the Durable Object to be woken up at a time in the future. When the alarm's scheduled time comes, the `alarm()` handler method will be called. Alarms are modified using the Storage API, and alarm operations follow the same rules as other storage operations. Notably: * Each Durable Object is able to schedule a single alarm at a time by calling `setAlarm()`. * Alarms have guaranteed at-least-once execution and are retried automatically when the `alarm()` handler throws. * Retries are performed using exponential backoff starting at a 2 second delay from the first failure with up to 6 retries allowed. How are alarms different from Cron Triggers? Alarms are more fine grained than [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). A Worker can have up to three Cron Triggers configured at once, but it can have an unlimited amount of Durable Objects, each of which can have an alarm set. Alarms are directly scheduled from within your Durable Object. Cron Triggers, on the other hand, are not programmatic. [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) execute based on their schedules, which have to be configured through the Cloudflare dashboard or API. Alarms can be used to build distributed primitives, like queues or batching of work atop Durable Objects. Alarms also provide a mechanism to guarantee that operations within a Durable Object will complete without relying on incoming requests to keep the Durable Object alive. For a complete example, refer to [Use the Alarms API](https://developers.cloudflare.com/durable-objects/examples/alarms-api/). ## Scheduling multiple events with a single alarm Although each Durable Object can only have one alarm set at a time, you can manage many scheduled and recurring events by storing your event schedule in storage and having the `alarm()` handler process due events, then reschedule itself for the next one. ```js import { DurableObject } from "cloudflare:workers"; export class AgentServer extends DurableObject { // Schedule a one-time or recurring event async scheduleEvent(id, runAt, repeatMs = null) { await this.ctx.storage.put(`event:${id}`, { id, runAt, repeatMs }); const currentAlarm = await this.ctx.storage.getAlarm(); if (!currentAlarm || runAt < currentAlarm) { await this.ctx.storage.setAlarm(runAt); } } async alarm() { const now = Date.now(); const events = await this.ctx.storage.list({ prefix: "event:" }); let nextAlarm = null; for (const [key, event] of events) { if (event.runAt <= now) { await this.processEvent(event); if (event.repeatMs) { event.runAt = now + event.repeatMs; await this.ctx.storage.put(key, event); } else { await this.ctx.storage.delete(key); } } // Track the next event time if (event.runAt > now && (!nextAlarm || event.runAt < nextAlarm)) { nextAlarm = event.runAt; } } if (nextAlarm) await this.ctx.storage.setAlarm(nextAlarm); } async processEvent(event) { // Your event handling logic here } } ``` ## Storage methods ### `getAlarm` * `getAlarm()`: `number | null` * If there is an alarm set, then return the currently set alarm time as the number of milliseconds elapsed since the UNIX epoch. Otherwise, return `null`. * If `getAlarm` is called while an [alarm](https://developers.cloudflare.com/durable-objects/api/alarms/#alarm) is already running, it returns `null` unless `setAlarm` has also been called since the alarm handler started running. ### `setAlarm` * `` setAlarm(scheduledTimeMs `number`) ``: `void` * Set the time for the alarm to run. Specify the time as the number of milliseconds elapsed since the UNIX epoch. * If you call `setAlarm` when there is already one scheduled, it will override the existing alarm. Calling \`setAlarm\` inside the constructor If you wish to call `setAlarm` inside the constructor of a Durable Object, ensure that you are first checking whether an alarm has already been set. This is due to the fact that, if the Durable Object wakes up after being inactive, the constructor is invoked before the [alarm handler](https://developers.cloudflare.com/durable-objects/api/alarms/#alarm). Therefore, if the constructor calls `setAlarm`, it could interfere with the next alarm which has already been set. ### `deleteAlarm` * `deleteAlarm()`: `void` * Unset the alarm if there is a currently set alarm. * Calling `deleteAlarm()` inside the `alarm()` handler may prevent retries on a best-effort basis, but is not guaranteed. ## Handler methods ### `alarm` * `` alarm(alarmInfo `Object`) ``: `void` * Called by the system when a scheduled alarm time is reached. * The optional parameter `alarmInfo` object has two properties: * `retryCount` `number`: The number of times this alarm event has been retried. * `isRetry` `boolean`: A boolean value to indicate if the alarm has been retried. This value is `true` if this alarm event is a retry. * Only one instance of `alarm()` will ever run at a given time per Durable Object instance. * The `alarm()` handler has guaranteed at-least-once execution and will be retried upon failure using exponential backoff, starting at 2 second delays for up to 6 retries. This only applies to the most recent `setAlarm()` call. Retries will be performed if the method fails with an uncaught exception. * This method can be `async`. Catching exceptions in alarm handlers Because alarms are only retried up to 6 times on error, it's recommended to catch any exceptions inside your `alarm()` handler and schedule a new alarm before returning if you want to make sure your alarm handler will be retried indefinitely. Otherwise, a sufficiently long outage in a downstream service that you depend on or a bug in your code that goes unfixed for hours can exhaust the limited number of retries, causing the alarm to not be re-run in the future until the next time you call `setAlarm`. ## Example This example shows how to both set alarms with the `setAlarm(timestamp)` method and handle alarms with the `alarm()` handler within your Durable Object. * The `alarm()` handler will be called once every time an alarm fires. * If an unexpected error terminates the Durable Object, the `alarm()` handler may be re-instantiated on another machine. * Following a short delay, the `alarm()` handler will run from the beginning on the other machine. ```js import { DurableObject } from "cloudflare:workers"; export default { async fetch(request, env) { return await env.ALARM_EXAMPLE.getByName("foo").fetch(request); }, }; const SECONDS = 1000; export class AlarmExample extends DurableObject { constructor(ctx, env) { super(ctx, env); this.storage = ctx.storage; } async fetch(request) { // If there is no alarm currently set, set one for 10 seconds from now let currentAlarm = await this.storage.getAlarm(); if (currentAlarm == null) { this.storage.setAlarm(Date.now() + 10 * SECONDS); } } async alarm() { // The alarm handler will be invoked whenever an alarm fires. // You can use this to do work, read from the Storage API, make HTTP calls // and set future alarms to run using this.storage.setAlarm() from within this handler. } } ``` ```python import time from workers import DurableObject, WorkerEntrypoint class Default(WorkerEntrypoint): async def fetch(self, request): return await self.env.ALARM_EXAMPLE.getByName("foo").fetch(request) SECONDS = 1000 class AlarmExample(DurableObject): def __init__(self, ctx, env): super().__init__(ctx, env) self.storage = ctx.storage async def fetch(self, request): # If there is no alarm currently set, set one for 10 seconds from now current_alarm = await self.storage.getAlarm() if current_alarm is None: self.storage.setAlarm(int(time.time() * 1000) + 10 * SECONDS) async def alarm(self): # The alarm handler will be invoked whenever an alarm fires. # You can use this to do work, read from the Storage API, make HTTP calls # and set future alarms to run using self.storage.setAlarm() from within this handler. pass ``` The following example shows how to use the `alarmInfo` property to identify if the alarm event has been attempted before. ```js class MyDurableObject extends DurableObject { async alarm(alarmInfo) { if (alarmInfo?.retryCount != 0) { console.log( "This alarm event has been attempted ${alarmInfo?.retryCount} times before.", ); } } } ``` ```python class MyDurableObject(DurableObject): async def alarm(self, alarm_info): if alarm_info and alarm_info.get('retryCount', 0) != 0: print(f"This alarm event has been attempted {alarm_info.get('retryCount')} times before.") ``` ## Related resources * Understand how to [use the Alarms API](https://developers.cloudflare.com/durable-objects/examples/alarms-api/) in an end-to-end example. * Read the [Durable Objects alarms announcement blog post ↗](https://blog.cloudflare.com/durable-objects-alarms/). * Review the [Storage API](https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/) documentation for Durable Objects. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/durable-objects/api/alarms/#page","headline":"Alarms · Cloudflare Durable Objects docs","description":"Schedule future wake-ups for Durable Objects using the Alarms API with guaranteed at-least-once execution.","url":"https://developers.cloudflare.com/durable-objects/api/alarms/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-21","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Process incoming emails with the email() handler in Cloudflare Workers to forward, reply, or reject messages. title: Workers API image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/email-service/llms.txt > Use this file to discover all available pages before exploring further. # Workers API Process incoming emails with the email() handler in Cloudflare Workers. Forward, reply, reject, or process emails programmatically. Last updated Jun 15, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Process incoming emails using the `email()` handler in your Cloudflare Workers. This allows you to programmatically handle email routing with custom logic. ## Email handler syntax Add the `email` handler function to your Worker's exported handlers: ```ts export default { async email(message, env, ctx): Promise { // Process incoming email await message.forward("destination@example.com"); }, } satisfies ExportedHandler; ``` ```python from workers import WorkerEntrypoint class Default(WorkerEntrypoint): async def email(self, message, env, ctx): await message.forward("destination@example.com") ``` ### Parameters | Parameter | Type | Description | | --------- | ----------------------- | --------------------------------------------- | | message | ForwardableEmailMessage | The incoming email message | | env | object | Worker environment bindings (KV, EMAIL, etc.) | | ctx | object | Execution context with waitUntil function | ## `ForwardableEmailMessage` interface The `message` parameter provides access to the incoming email: ```ts interface ForwardableEmailMessage { readonly from: string; // Sender email address (envelope MAIL FROM) readonly to: string; // Recipient email address (envelope RCPT TO) readonly headers: Headers; // Email headers (Subject, Message-ID, etc.) readonly raw: ReadableStream; // Raw MIME email content stream readonly rawSize: number; // Size of raw email in bytes readonly canBeForwarded: boolean; // Whether the message can be forwarded // Actions setReject(reason: string): void; forward(rcptTo: string, headers?: Headers): Promise; reply(message: EmailMessage): Promise; } ``` ### Properties ```ts export default { async email(message, env, ctx): Promise { // Access email metadata console.log(`From: ${message.from}`); console.log(`To: ${message.to}`); console.log(`Size: ${message.rawSize} bytes`); // Access headers const subject = message.headers.get("subject"); const date = message.headers.get("date"); const messageId = message.headers.get("message-id"); console.log(`Subject: ${subject}`); console.log(`Date: ${date}`); console.log(`Message-ID: ${messageId}`); }, }; ``` Use [postal-mime ↗](https://www.npmjs.com/package/postal-mime) to parse the MIME structure of an incoming email. The parser handles multipart boundaries, transfer encodings, and character sets correctly. ```ts import PostalMime from "postal-mime"; export default { async email(message, env, ctx): Promise { const email = await PostalMime.parse(message.raw); console.log(`Subject: ${email.subject}`); console.log(`Text: ${email.text}`); console.log(`HTML: ${email.html}`); }, }; ``` ## Email actions ### Forward emails Forward incoming emails to verified destination addresses: ```ts export default { async email(message, env, ctx): Promise { // Forward to a single address await message.forward("team@example.com"); }, }; ``` ```ts export default { async email(message, env, ctx): Promise { const recipient = message.to; const subject = message.headers.get("subject") || ""; // Route based on recipient if (recipient.includes("support@")) { await message.forward("support-team@example.com"); } else if (recipient.includes("sales@")) { await message.forward("sales-team@example.com"); } else if (subject.toLowerCase().includes("urgent")) { await message.forward("urgent@example.com"); } else { // Default routing await message.forward("general@example.com"); } }, }; ``` ```ts export default { async email(message, env, ctx): Promise { const subject = message.headers.get("subject") || ""; if (subject.toLowerCase().includes("security")) { // Forward to multiple addresses for security issues await Promise.all([ message.forward("security@example.com"), message.forward("admin@example.com"), message.forward("ciso@example.com"), ]); } else { await message.forward("general@example.com"); } }, }; ``` ### Forward with custom headers Add custom headers when forwarding. Only headers with an `X-` prefix can be added through `forward()`. Other headers are removed. ```ts export default { async email(message, env, ctx): Promise { // Create custom headers const customHeaders = new Headers(); customHeaders.set("X-Processed-By", "Email-Worker"); customHeaders.set("X-Processing-Time", new Date().toISOString()); customHeaders.set("X-Original-Recipient", message.to); customHeaders.set("X-Spam-Score", "0.1"); // Example spam score // Forward with custom headers await message.forward("processed@example.com", customHeaders); }, }; ``` ### Reply to emails Send automatic replies with `message.reply()`. Replies built this way are threaded with the original message and pass through the same SMTP session, so they preserve the original `Message-ID` chain. Replies through the Workers API must satisfy the following requirements, otherwise `reply()` throws an exception: * The incoming email must have a valid DMARC result. * An email can only be replied to once per `EmailMessage` event. * The recipient in the reply must match the sender of the incoming email. * The outgoing sender domain must match the domain that received the email. * The reply is rejected if the incoming email has more than 100 entries in its `References` header, to prevent reply loops and abuse. The reply payload is an `EmailMessage` built from a raw MIME string. The examples below use [mimetext ↗](https://www.npmjs.com/package/mimetext) to build the MIME body. The `mimetext` package requires the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) compatibility flag. ```ts import { EmailMessage } from "cloudflare:email"; import { createMimeMessage } from "mimetext"; export default { async email(message, env, ctx): Promise { const subject = message.headers.get("subject") || ""; const messageId = message.headers.get("Message-ID"); const reply = createMimeMessage(); if (messageId) { reply.setHeader("In-Reply-To", messageId); reply.setHeader("References", messageId); } reply.setSender(message.to); reply.setRecipient(message.from); reply.setSubject(`Re: ${subject}`); reply.addMessage({ contentType: "text/plain", data: "Thank you for your message. We have received your email and will respond shortly.", }); reply.addMessage({ contentType: "text/html", data: "

Thank you for your message

We have received your email and will respond shortly.

", }); await message.reply( new EmailMessage(message.to, message.from, reply.asRaw()), ); // Also forward to human team await message.forward("team@example.com"); }, }; ``` ```ts import { EmailMessage } from "cloudflare:email"; import { createMimeMessage } from "mimetext"; export default { async email(message, env, ctx): Promise { const sender = message.from; const recipient = message.to; const subject = message.headers.get("subject") || ""; const messageId = message.headers.get("Message-ID"); // Don't reply to automated emails if ( sender.includes("noreply") || sender.includes("no-reply") || subject.toLowerCase().includes("automated") ) { await message.forward("team@example.com"); return; } // Customized auto-reply based on recipient let html = ""; if (recipient.includes("support@")) { html = `

Support Request Received

Thank you for contacting support. Your request has been assigned ticket #${Date.now()}.

Expected response time: 2-4 hours during business hours.

`; } else if (recipient.includes("sales@")) { html = `

Sales Inquiry Received

Thank you for your interest in our products.

A sales representative will contact you within 24 hours.

`; } else { html = `

Message Received

Thank you for your message. We will respond within 2 business days.

`; } const reply = createMimeMessage(); if (messageId) { reply.setHeader("In-Reply-To", messageId); reply.setHeader("References", messageId); } reply.setSender(recipient); reply.setRecipient(sender); reply.setSubject(`Re: ${subject}`); reply.addMessage({ contentType: "text/plain", data: html.replace(/<[^>]*>/g, ""), }); reply.addMessage({ contentType: "text/html", data: html }); await message.reply(new EmailMessage(recipient, sender, reply.asRaw())); // Forward to appropriate team await message.forward("team@example.com"); }, }; ``` ### Reject emails Reject emails with a permanent SMTP error: ```ts export default { async email(message, env, ctx): Promise { const sender = message.from; // Block specific senders const blockedDomains = ["spam.com", "unwanted.net"]; const senderDomain = sender.split("@")[1]; if (blockedDomains.includes(senderDomain)) { message.setReject("Sender domain not allowed"); return; } // Continue processing await message.forward("inbox@example.com"); }, }; ``` ```ts export default { async email(message, env, ctx): Promise { const subject = message.headers.get("subject") || ""; // Reject based on subject content const spamKeywords = ["buy now", "limited time", "act fast", "urgent"]; const containsSpam = spamKeywords.some((keyword) => subject.toLowerCase().includes(keyword), ); if (containsSpam) { message.setReject("Message appears to be spam"); return; } // Check message size if (message.rawSize > 25 * 1024 * 1024) { // 25 MiB limit (inbound message size) message.setReject("Message too large"); return; } // Continue processing await message.forward("inbox@example.com"); }, }; ``` ## Error handling Handle errors gracefully in email processing: ```ts export default { async email(message, env, ctx): Promise { try { // Main email processing logic await processEmail(message, env); } catch (error) { console.error("Email processing failed:", error); // Log error for monitoring if (env.ERROR_LOGS) { await env.ERROR_LOGS.put( `error-${Date.now()}`, JSON.stringify({ error: error.message, stack: error.stack, from: message.from, to: message.to, timestamp: new Date().toISOString(), }), ); } // Fallback: forward to admin try { await message.forward("admin@example.com"); } catch (fallbackError) { console.error("Fallback forwarding failed:", fallbackError); // Last resort: reject the email message.setReject("Internal processing error"); } } }, }; async function processEmail(message, env) { // Your main email processing logic here const recipient = message.to; if (recipient.includes("support@")) { await message.forward("support@example.com"); } else if (recipient.includes("sales@")) { await message.forward("sales@example.com"); } else { await message.forward("general@example.com"); } } ``` ## Next steps * Test locally: [Email routing development](https://developers.cloudflare.com/email-service/local-development/routing/) * Manage rules and addresses programmatically with the [Email Routing REST API](https://developers.cloudflare.com/email-service/platform/email-routing-rest-api/) * Set up [email routing configuration](https://developers.cloudflare.com/email-service/configuration/email-routing-addresses/) * See [email routing examples](https://developers.cloudflare.com/email-service/examples/email-routing/) for advanced email processing * Learn about [spam filtering](https://developers.cloudflare.com/email-service/examples/email-routing/spam-filtering/) with Workers Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/email-service/api/route-emails/email-handler/#page","headline":"Workers API · Cloudflare Email Service docs","description":"Process incoming emails with the email() handler in Cloudflare Workers to forward, reply, or reject messages.","url":"https://developers.cloudflare.com/email-service/api/route-emails/email-handler/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-15","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Handle incoming HTTP requests in Cloudflare Workers using the fetch() handler and return responses. title: Fetch Handler image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Fetch Handler Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Incoming HTTP requests to a Worker are passed to the `fetch()` handler as a [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) object. To respond to the request with a response, return a [Response](https://developers.cloudflare.com/workers/runtime-apis/response/) object: ```js export default { async fetch(request, env, ctx) { return new Response('Hello World!'); }, }; ``` Note The Workers runtime does not support `XMLHttpRequest` (XHR). Learn the difference between `XMLHttpRequest` and `fetch()` in the [MDN ↗](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) documentation. ### Parameters * `request` Request * The incoming HTTP request. * `env` object * The [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) available to the Worker. As long as the [environment](https://developers.cloudflare.com/workers/wrangler/environments/) has not changed, the same object (equal by identity) may be passed to multiple requests. You can also [import env from cloudflare:workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) to access bindings from anywhere in your code. * `ctx.waitUntil(promisePromise)` : void * Refer to [waitUntil](https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil). * `ctx.passThroughOnException()` : void * Refer to [passThroughOnException](https://developers.cloudflare.com/workers/runtime-apis/context/#passthroughonexception). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#page","headline":"Fetch Handler · Cloudflare Workers docs","description":"Handle incoming HTTP requests in Cloudflare Workers using the fetch() handler and return responses.","url":"https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Run Workers on a recurring schedule using the scheduled() handler and Cron Triggers. title: Scheduled Handler image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Scheduled Handler Last updated Jun 20, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background When a Worker is invoked via a [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/), the `scheduled()` handler handles the invocation. Testing scheduled() handlers in local development You can test the behavior of your `scheduled()` handler in local development by sending an HTTP request to `/cdn-cgi/handler/scheduled` to trigger the handler. Pass `?format=json` to return the structured scheduled handler result. ```sh curl "http://localhost:8787/cdn-cgi/handler/scheduled?format=json" ``` --- ## Syntax ```js export default { async scheduled(controller, env, ctx) { await doSomeTaskOnASchedule(); }, }; ``` ```ts interface Env {} export default { async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { await doSomeTaskOnASchedule(); }, }; ``` ```python from workers import WorkerEntrypoint class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx): # controller.cron contains the cron pattern that triggered this event # controller.scheduledTime contains the scheduled time in ms since epoch print(f"Cron triggered: {controller.cron}") ``` ### Properties * `controller.cron` string * The value of the [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) that started the `ScheduledEvent`. * `controller.type` string * The type of controller. This will always return `"scheduled"`. * `controller.scheduledTime` number * The time the `ScheduledEvent` was scheduled to be executed in milliseconds since January 1, 1970, UTC. It can be parsed as `new Date(controller.scheduledTime)`. * `env` object * An object containing the bindings associated with your Worker using ES modules format, such as KV namespaces and Durable Objects. * `ctx` object * An object containing the context associated with your Worker using ES modules format. Currently, this object just contains the `waitUntil` function. ### Handle multiple cron triggers When you configure multiple [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) for a single Worker, each trigger invokes the same `scheduled()` handler. Use `controller.cron` to distinguish which schedule fired and run different logic for each. ```jsonc { "triggers": { "crons": ["*/5 * * * *", "0 0 * * *"], }, } ``` ```toml [triggers] crons = [ "*/5 * * * *", "0 0 * * *" ] ``` ```js export default { async scheduled(controller, env, ctx) { switch (controller.cron) { case "*/5 * * * *": await fetch("https://example.com/api/sync"); break; case "0 0 * * *": await env.MY_KV.put("last-cleanup", new Date().toISOString()); break; } }, }; ``` ```ts export default { async scheduled( controller: ScheduledController, env: Env, ctx: ExecutionContext, ) { switch (controller.cron) { case "*/5 * * * *": await fetch("https://example.com/api/sync"); break; case "0 0 * * *": await env.MY_KV.put("last-cleanup", new Date().toISOString()); break; } }, } satisfies ExportedHandler; ``` ```python from workers import WorkerEntrypoint, fetch from datetime import datetime, timezone class Default(WorkerEntrypoint): async def scheduled(self, controller, env, ctx): if controller.cron == "*/5 * * * *": await fetch("https://example.com/api/sync") elif controller.cron == "0 0 * * *": await env.MY_KV.put("last-cleanup", datetime.now(timezone.utc).isoformat()) ``` The value of `controller.cron` is the exact cron expression string from your configuration. It must match character-for-character, including spacing. ### Methods When a Workers script is invoked by a [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/), the Workers runtime starts a `ScheduledEvent` which will be handled by the `scheduled` function in your Workers Module class. The `ctx` argument represents the context your function runs in, and contains the following methods to control what happens next: * `ctx.waitUntil(promise)` : void - Use this method to register asynchronous tasks (for example, logging, analytics to third-party services, streaming and caching) that should settle before the invocation completes. The first `ctx.waitUntil` to fail will be observed and recorded as the status in the [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) Past Events table. Otherwise, it will be reported as a success. Note The runtime waits for the promise returned by the `scheduled()` handler to resolve (up to the 15-minute duration limit). You do not need to use `waitUntil()` for the runtime to wait for a single asynchronous task. `waitUntil()` is most useful when you need to run multiple concurrent tasks, or when you want the outcome of a specific promise to be recorded as the Cron Trigger invocation status. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/#page","headline":"Scheduled Handler · Cloudflare Workers docs","description":"Run Workers on a recurring schedule using the scheduled() handler and Cron Triggers.","url":"https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-20","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Process real-time logs from producer Workers using the tail() handler in Tail Workers. title: Tail Handler image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Tail Handler Last updated Jun 15, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The `tail()` handler is the handler you implement when writing a [Tail Worker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/). Tail Workers can be used to process logs in real-time and send them to a logging or analytics service. The `tail()` handler is called once each time the connected producer Worker is invoked. To configure a Tail Worker, refer to [Tail Workers documentation](https://developers.cloudflare.com/workers/observability/logs/tail-workers/). ## Syntax ```js export default { async tail(events, env, ctx) { fetch("", { method: "POST", body: JSON.stringify(events), }) } } ``` ```python from workers import WorkerEntrypoint, fetch import json class Default(WorkerEntrypoint): async def tail(self, events, env, ctx): await fetch("", method="POST", body=json.dumps(events)) ``` ### Parameters * `events` array * An array of [TailItems](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#tailitems). One `TailItem` is collected for each event that triggers a Worker. For Workers for Platforms customers with a Tail Worker installed on the dynamic dispatch Worker, `events` will contain two elements: one for the dynamic dispatch Worker and one for the User Worker. * `env` object * An object containing the bindings associated with your Worker using [ES modules format](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/), such as KV namespaces and Durable Objects. * `ctx` object * An object containing the context associated with your Worker using [ES modules format](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/). Currently, this object just contains the `waitUntil` function. ### Properties * `event.type` string * The type of event. This will always return `"tail"`. * `event.traces` array * An array of [TailItems](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#tailitems). One `TailItem` is collected for each event that triggers a Worker. For Workers for Platforms customers with a Tail Worker installed on the dynamic dispatch Worker, `events` will contain two elements: one for the dynamic dispatch Worker and one for the user Worker. * `event.waitUntil(promisePromise)` : void * Refer to [waitUntil](https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil). Note that unlike fetch event handlers, tail handlers do not return a value, so this is the only way for trace Workers to do asynchronous work. ### `TailItems` #### Properties * `scriptName` string * The name of the producer script. * `event` object * Contains information about the Worker’s triggering event. * For fetch events: a [FetchEventInfo object](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#fetcheventinfo) * For other event types: `null`, currently. * `eventTimestamp` number * Measured in epoch time. * `logs` array * An array of [TailLogs](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#taillog). * `exceptions` array * An array of [TailExceptions](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#tailexception). A single Worker invocation might result in multiple unhandled exceptions, since a Worker can register multiple asynchronous tasks. * `outcome` string * The outcome of the Worker invocation, one of: * `unknown`: outcome status was not set. * `ok`: The worker invocation succeeded. * `exception`: An unhandled exception was thrown. This can happen for many reasons, including: * An uncaught JavaScript exception. * A fetch handler that does not result in a Response. * An internal error. * `exceededCpu`: The Worker invocation exceeded either its CPU limits. * `exceededMemory`: The Worker invocation exceeded memory limits. * `scriptNotFound`: An internal error from difficulty retrieving the Worker script. * `canceled`: The worker invocation was canceled before it completed. Commonly because the client disconnected before a response could be sent. * `responseStreamDisconnected`: The response stream was disconnected during deferred proxying. Happens when either the client or server hangs up early. Outcome is not the same as HTTP status. Outcome is equivalent to the exit status of a script and an indicator of whether it has fully run to completion. A Worker outcome may differ from a response code if, for example: * a script successfully processes a request but is logically designed to return a `4xx`/`5xx` response. * a script sends a successful `200` response but an asynchronous task registered via `waitUntil()` later exceeds CPU or memory limits. ### `FetchEventInfo` #### Properties * `request` object * A [TailRequest object](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#tailrequest). * `response` object * A [TailResponse object](https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#tailresponse). ### `TailRequest` #### Properties * `cf` object * Contains the data from [IncomingRequestCfProperties](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties). * `headers` object * Header name/value entries (redacted by default). Header names are lowercased, and the values associated with duplicate header names are concatenated, with the string `", "` (comma space) interleaved, similar to [the Fetch standard ↗](https://fetch.spec.whatwg.org/#concept-header-list-get). * `method` string * The HTTP request method. * `url` string * The HTTP request URL (redacted by default). #### Methods * `getUnredacted()` object * Returns a TailRequest object with unredacted properties Some of the properties of `TailRequest` are redacted by default to make it harder to accidentally record sensitive information, like user credentials or API tokens. The redactions use heuristic rules, so they are subject to false positives and negatives. Clients can call `getUnredacted()` to bypass redaction, but they should always be careful about what information is retained, whether using the redaction or not. * Header redaction: The header value will be the string `“REDACTED”` when the (case-insensitive) header name is `cookie`/`set-cookie` or contains a substring `"auth”`, `“key”`, `“secret”`, `“token”`, or `"jwt"`. * URL redaction: For each greedily matched substring of ID characters (a-z, A-Z, 0-9, '+', '-', '\_') in the URL, if it meets the following criteria for a hex or base-64 ID, the substring will be replaced with the string `“REDACTED”`. * Hex ID: Contains 32 or more hex digits, and contains only hex digits and separators ('+', '-', '\_') * Base-64 ID: Contains 21 or more characters, and contains at least two uppercase, two lowercase, and two digits. ### `TailResponse` #### Properties * `status` number * The HTTP status code. ### `TailLog` Records information sent to console functions. #### Properties * `timestamp` number * Measured in epoch time. * `level` string * A string indicating the console function that was called. One of: `debug`, `info`, `log`, `warn`, `error`. * `message` object * The array of parameters passed to the console function. ### `TailException` Records an unhandled exception that occurred during the Worker invocation. #### Properties * `timestamp` number * Measured in epoch time. * `name` string * The error type (For example,`Error`, `TypeError`, etc.). * `message` object * The error description (For example, `"x" is not a function`). ## Related resources * [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/) \- Configure a Tail Worker to receive information about the execution of other Workers. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/#page","headline":"Tail Handler · Cloudflare Workers docs","description":"Process real-time logs from producer Workers using the tail() handler in Tail Workers.","url":"https://developers.cloudflare.com/workers/runtime-apis/handlers/tail/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-15","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Access HTTP request and response headers. title: Headers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Headers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/headers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background All HTTP request and response headers are available through the [Headers API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Headers). When a header name possesses multiple values, those values will be concatenated as a single, comma-delimited string value. This means that `Headers.get` will always return a string or a `null` value. This applies to all header names except for `Set-Cookie`, which requires `Headers.getAll`. This is documented below in [Differences](#differences). ```js let headers = new Headers(); headers.get('x-foo'); //=> null headers.set('x-foo', '123'); headers.get('x-foo'); //=> "123" headers.set('x-foo', 'hello'); headers.get('x-foo'); //=> "hello" headers.append('x-foo', 'world'); headers.get('x-foo'); //=> "hello, world" ``` ## Differences The Workers implementation of the `Headers` API differs from the web standard in several ways. These differences are intentional, and reflect the server-side nature of the Workers runtime. TypeScript users Workers type definitions (from `@cloudflare/workers-types` or generated via [wrangler types](https://developers.cloudflare.com/workers/wrangler/commands/general/#types)) define a `Headers` type that includes Workers-specific methods like `getAll()`. This type is not directly compatible with the standard `Headers` type from `lib.dom.d.ts`. If you are working with code that uses both Workers types and standard web types, you may need to use type assertions. ### `getAll()` method Despite the fact that the `Headers.getAll` method has been made obsolete in web browsers, Workers still provides this method for use with the `Set-Cookie` header. This is because cookies often contain date strings, which include commas. This can make parsing multiple values in a `Set-Cookie` header difficult. Any attempts to use `Headers.getAll` with other header names will throw an error. A brief history of `Headers.getAll` is available in this [GitHub issue ↗](https://github.com/whatwg/fetch/issues/973). ### `Set-Cookie` handling Due to [RFC 6265 ↗](https://www.rfc-editor.org/rfc/rfc6265) prohibiting folding multiple `Set-Cookie` headers into a single header, the `Headers.append` method allows you to set multiple `Set-Cookie` response headers instead of appending the value onto the existing header. ```js const headers = new Headers(); headers.append("Set-Cookie", "cookie1=value_for_cookie_1; Path=/; HttpOnly;"); headers.append("Set-Cookie", "cookie2=value_for_cookie_2; Path=/; HttpOnly;"); console.log(headers.getAll("Set-Cookie")); // Array(2) [ cookie1=value_for_cookie_1; Path=/; HttpOnly;, cookie2=value_for_cookie_2; Path=/; HttpOnly; ] ``` ### `USVString` return type In Cloudflare Workers, the `Headers.get` method returns a [USVString ↗](https://mdn2.netlify.app/en-us/docs/web/api/usvstring/) instead of a [ByteString ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/String), which is specified by the web standard. For most scenarios, this should have no noticeable effect. To compare the differences between these two string classes, refer to this [Playground example ↗](https://workers.cloudflare.com/playground#LYVwNgLglgDghgJwgegGYHsHALQBM4RwDcABAEbogB2+CAngLzbMutvvsCMALAJx-cAzAHZeANkG8AHAAZOU7t2EBWAEy9eqsXNWdOALg5HjbHv34jxk2fMUr1m7Z12cAsACgAwuioQApr7YACJQAM4w6KFQ0D76JBhYeATEJFRwwH4MAERQNH4AHgB0AFahWaSoUGAB6Zk5eUWlWR7evgEQ2AAqdDB+cXAwMGBQAMYEUD7IxXAAbnChIwiwEADUwOi44H4eHgURSCS4fqhw4BAkAN7uAJDzdFQj8X4QIwAWABQIfgCOIH6hEAAlJcbtdqucxucGCQsoBeDcAXHtZUHgkggCCoKSeAgkaFUPwAdxInQKEAAog8Nn4EO9AYUAiNKe9IYDkc8SPTKbgsVCSABlCBLKgAc0KqAQ6GAnleiG8R3ehQVaIx3JZoIZVFC6GqhTA6CF7yynVeYRIJrgJAAqryAGr8wVCkj46KvEjmyH6LIAGhIzLVPk12t1+sNxtCprD5oAQnR-Hbcg6nRAXW7sT5LZ0AGLYKQe70co5cgiq67XZDIEgACT8cCOCAjXxIoRAg0iflwJAg6EdmAA1iQfGA6I7nSRo7GBfHQt6yGj+yAEKCy6bgEM-BlfOM0yBQv9LTa48LQoUiaHUiSSMM8cOwGASDBBec4Ivy-jEFR466KLOk2FCqzzq81a1mGuIEpWQFUqE7wXDC+ZttgkJZHEcGFucAC+xbXF8EDzlQZ6EgASv8EQan4BpSn4Ix9pQ5xJn4JAAAatAGfgMa6NAdoBJBEeE-r0YBNaQR2XY7vRdFzhAMCzgyK6IGE-qFF6lwkAJwEkBhNxoe4aEeCYelGGYAiWBI0hyAoShqBoWg6HoLQ+P4gQhLxUQxFQcQJDg+CEKQaQZNkGSEF5cDlPEVQ1H5WRkLqZDNF49ntF0PR9K6gzDJCExUFMmpUDs7gXFkwBwLkAD66ybNUSH1EcjRlDp7j6Q1rCGRYogmTY5n2FZTguMwHhAA). ## Cloudflare headers Cloudflare sets a number of its own custom headers on incoming requests and outgoing responses. While some may be used for its own tracking and bookkeeping, many of these can be useful to your own applications – or Workers – too. For a list of documented Cloudflare request headers, refer to [Cloudflare HTTP headers](https://developers.cloudflare.com/fundamentals/reference/http-headers/). ## Related resources * [Logging headers to console](https://developers.cloudflare.com/workers/examples/logging-headers/) \- Review how to log headers in the console. * [Cloudflare HTTP headers](https://developers.cloudflare.com/fundamentals/reference/http-headers/) \- Contains a list of specific headers that Cloudflare adds. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/headers/#page","headline":"Headers · Cloudflare Workers docs","description":"Access HTTP request and response headers.","url":"https://developers.cloudflare.com/workers/runtime-apis/headers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Build comprehensive and expressive HTML parsers inside of a Worker application. title: HTMLRewriter image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # HTMLRewriter Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The `HTMLRewriter` class allows developers to build comprehensive and expressive HTML parsers inside of a Cloudflare Workers application. It can be thought of as a jQuery-like experience directly inside of your Workers application. Leaning on a powerful JavaScript API to parse and transform HTML, `HTMLRewriter` allows developers to build deeply functional applications. The `HTMLRewriter` class should be instantiated once in your Workers script, with a number of handlers attached using the `on` and `onDocument` functions. --- ## Constructor ```js new HTMLRewriter() .on("*", new ElementHandler()) .onDocument(new DocumentHandler()); ``` --- ## Global types Throughout the `HTMLRewriter` API, there are a few consistent types that many properties and methods use: * `Content` string | Response | ReadableStream * Content inserted in the output stream should be a string, [Response](https://developers.cloudflare.com/workers/runtime-apis/response/), or [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/). * `ContentOptions` Object * `{ html: Boolean }` Controls the way the HTMLRewriter treats inserted content. If the `html` boolean is set to true, content is treated as raw HTML. If the `html` boolean is set to false or not provided, content will be treated as text and proper HTML escaping will be applied to it. --- ## Handlers There are two handler types that can be used with `HTMLRewriter`: element handlers and document handlers. ### Element Handlers An element handler responds to any incoming element, when attached using the `.on` function of an `HTMLRewriter` instance. The element handler should respond to `element`, `comments`, and `text`. The example processes `div` elements with an `ElementHandler` class. ```js class ElementHandler { element(element) { // An incoming element, such as `div` console.log(`Incoming element: ${element.tagName}`); } comments(comment) { // An incoming comment } text(text) { // An incoming piece of text } } async function handleRequest(req) { const res = await fetch(req); return new HTMLRewriter().on("div", new ElementHandler()).transform(res); } ``` ### Document Handlers A document handler represents the incoming HTML document. A number of functions can be defined on a document handler to query and manipulate a document’s `doctype`, `comments`, `text`, and `end`. Unlike an element handler, a document handler’s `doctype`, `comments`, `text`, and `end` functions are not scoped by a particular selector. A document handler's functions are called for all the content on the page including the content outside of the top-level HTML tag: ```js class DocumentHandler { doctype(doctype) { // An incoming doctype, such as } comments(comment) { // An incoming comment } text(text) { // An incoming piece of text } end(end) { // The end of the document } } ``` #### Async Handlers All functions defined on both element and document handlers can return either `void` or a `Promise`. Making your handler function `async` allows you to access external resources such as an API via fetch, Workers KV, Durable Objects, or the cache. ```js class UserElementHandler { async element(element) { let response = await fetch(new Request("/user")); // fill in user info using response } } async function handleRequest(req) { const res = await fetch(req); // run the user element handler via HTMLRewriter on a div with ID `user_info` return new HTMLRewriter() .on("div#user_info", new UserElementHandler()) .transform(res); } ``` ### Element The `element` argument, used only in element handlers, is a representation of a DOM element. A number of methods exist on an element to query and manipulate it: #### Properties * `tagName` string * The name of the tag, such as `"h1"` or `"div"`. This property can be assigned different values, to modify an element’s tag. * `attributes` Iterator read-only * A `[name, value]` pair of the tag’s attributes. * `removed` boolean * Indicates whether the element has been removed or replaced by one of the previous handlers. * `namespaceURI` string * Represents the [namespace URI ↗](https://infra.spec.whatwg.org/#namespaces) of an element. #### Methods * `` getAttribute(name `string`) `` : `string | null` * Returns the value for a given attribute name on the element, or `null` if it is not found. * `` hasAttribute(name `string`) `` : `boolean` * Returns a boolean indicating whether an attribute exists on the element. * `` setAttribute(name `string`, value `string`) `` : `Element` * Sets an attribute to a provided value, creating the attribute if it does not exist. * `` removeAttribute(name `string`) `` : `Element` * Removes the attribute. * `` before(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content before the element. Content and ContentOptions Refer to [Global types](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#global-types) for more information on `Content` and `ContentOptions`. * `` after(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content right after the element. * `` prepend(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content right after the start tag of the element. * `` append(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content right before the end tag of the element. * `` replace(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Removes the element and inserts content in place of it. * `` setInnerContent(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Replaces content of the element. * `remove()` : `Element` * Removes the element with all its content. * `removeAndKeepContent()` : `Element` * Removes the start tag and end tag of the element but keeps its inner content intact. * `` onEndTag(handler `Function`) `` : `void` * Registers a handler that is invoked when the end tag of the element is reached. ### EndTag The `endTag` argument, used only in handlers registered with `element.onEndTag`, is a limited representation of a DOM element. #### Properties * `name` string * The name of the tag, such as `"h1"` or `"div"`. This property can be assigned different values, to modify an element's tag. #### Methods * `` before(content `Content`, contentOptions `ContentOptions`optional) `` : `EndTag` * Inserts content right before the end tag. * `` after(content `Content`, contentOptions `ContentOptions`optional) `` : `EndTag` * Inserts content right after the end tag. Content and ContentOptions Refer to [Global types](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#global-types) for more information on `Content` and `ContentOptions`. * `remove()` : `EndTag` * Removes the element with all its content. ### Text chunks Since Cloudflare performs zero-copy streaming parsing, text chunks are not the same thing as text nodes in the lexical tree. A lexical tree text node can be represented by multiple chunks, as they arrive over the wire from the origin. Consider the following markup: `
Hey. How are you?
`. It is possible that the Workers script will not receive the entire text node from the origin at once; instead, the `text` element handler will be invoked for each received part of the text node. For example, the handler might be invoked with `"Hey. How "`, then `"are you?"`. When the last chunk arrives, the text's `lastInTextNode` property will be set to `true`. Developers should make sure to concatenate these chunks together. #### Properties * `removed` boolean * Indicates whether the element has been removed or replaced by one of the previous handlers. * `text` string read-only * The text content of the chunk. Could be empty if the chunk is the last chunk of the text node. * `lastInTextNode` boolean read-only * Specifies whether the chunk is the last chunk of the text node. #### Methods * `` before(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content before the element. Content and ContentOptions Refer to [Global types](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#global-types) for more information on `Content` and `ContentOptions`. * `` after(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content right after the element. * `` replace(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Removes the element and inserts content in place of it. * `remove()` : `Element` * Removes the element with all its content. ### Comments The `comments` function on an element handler allows developers to query and manipulate HTML comment tags. ```js class ElementHandler { comments(comment) { // An incoming comment element, such as } } ``` #### Properties * `comment.removed` boolean * Indicates whether the element has been removed or replaced by one of the previous handlers. * `comment.text` string * The text of the comment. This property can be assigned different values, to modify comment's text. #### Methods * `` before(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content before the element. Content and ContentOptions Refer to [Global types](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#global-types) for more information on `Content` and `ContentOptions`. * `` after(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Inserts content right after the element. * `` replace(content `Content`, contentOptions `ContentOptions`optional) `` : `Element` * Removes the element and inserts content in place of it. * `remove()` : `Element` * Removes the element with all its content. ### Doctype The `doctype` function on a document handler allows developers to query a document's [doctype ↗](https://developer.mozilla.org/en-US/docs/Glossary/Doctype). ```js class DocumentHandler { doctype(doctype) { // An incoming doctype element, such as // } } ``` #### Properties * `doctype.name` string | null read-only * The doctype name. * `doctype.publicId` string | null read-only * The quoted string in the doctype after the PUBLIC atom. * `doctype.systemId` string | null read-only * The quoted string in the doctype after the SYSTEM atom or immediately after the `publicId`. ### End The `end` function on a document handler allows developers to append content to the end of a document. ```js class DocumentHandler { end(end) { // The end of the document } } ``` #### Methods * `` append(content `Content`, contentOptions `ContentOptions`optional) `` : `DocumentEnd` * Inserts content after the end of the document. Content and ContentOptions Refer to [Global types](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#global-types) for more information on `Content` and `ContentOptions`. --- ## Selectors This is what selectors are and what they are used for. * `*` * Any element. * `E` * Any element of type E. * `E:nth-child(n)` * An E element, the n-th child of its parent. * `E:first-child` * An E element, first child of its parent. * `E:nth-of-type(n)` * An E element, the n-th sibling of its type. * `E:first-of-type` * An E element, first sibling of its type. * `E:not(s)` * An E element that does not match either compound selectors. * `E.warning` * An E element belonging to the class warning. * `E#myid` * An E element with ID equal to myid. * `E[foo]` * An E element with a foo attribute. * `E[foo="bar"]` * An E element whose foo attribute value is exactly equal to bar. * `E[foo="bar" i]` * An E element whose foo attribute value is exactly equal to any (ASCII-range) case-permutation of bar. * `E[foo="bar" s]` * An E element whose foo attribute value is exactly and case-sensitively equal to bar. * `E[foo~="bar"]` * An E element whose foo attribute value is a list of whitespace-separated values, one of which is exactly equal to bar. * `E[foo^="bar"]` * An E element whose foo attribute value begins exactly with the string bar. * `E[foo$="bar"]` * An E element whose foo attribute value ends exactly with the string bar. * `E[foo*="bar"]` * An E element whose foo attribute value contains the substring bar. * `E[foo|="en"]` * An E element whose foo attribute value is a hyphen-separated list of values beginning with en. * `E F` * An F element descendant of an E element. * `E > F` * An F element child of an E element. --- ## Errors If a handler throws an exception, parsing is immediately halted, the transformed response body is errored with the thrown exception, and the untransformed response body is canceled (closed). If the transformed response body was already partially streamed back to the client, the client will see a truncated response. ```js async function handle(request) { let oldResponse = await fetch(request); let newResponse = new HTMLRewriter() .on("*", { element(element) { throw new Error("A really bad error."); }, }) .transform(oldResponse); // At this point, an expression like `await newResponse.text()` // will throw `new Error("A really bad error.")`. // Thereafter, any use of `newResponse.body` will throw the same error, // and `oldResponse.body` will be closed. // Alternatively, this will produce a truncated response to the client: return newResponse; } ``` --- ## Related resources * [Introducing HTMLRewriter ↗](https://blog.cloudflare.com/introducing-htmlrewriter/) * [Tutorial: Localize a Website](https://developers.cloudflare.com/pages/tutorials/localize-a-website/) * [Example: rewrite links](https://developers.cloudflare.com/workers/examples/rewrite-links/) * [Example: Inject Turnstile](https://developers.cloudflare.com/workers/examples/turnstile-html-rewriter/) * [Example: SPA shell with bootstrap data](https://developers.cloudflare.com/workers/examples/spa-shell/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#page","headline":"HTMLRewriter · Cloudflare Workers docs","description":"Build comprehensive and expressive HTML parsers inside of a Worker application.","url":"https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Channel messaging with MessageChannel and MessagePort title: MessageChannel image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # MessageChannel Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/messagechannel/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The [MessageChannel API ↗](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel) provides a way to create a communication channel between different parts of your application. The Workers runtime provides a minimal implementation of the `MessageChannel` API that is currently limited to uses with a single Worker instance. This means that you can use `MessageChannel` to send messages between different parts of your Worker, but not across different Workers. ```js const { port1, port2 } = new MessageChannel(); port2.onmessage = (event) => { console.log('Received message:', event.data); }; port2.postMessage('Hello from port2!'); ``` Any value that can be used with the `structuredClone(...)` API can be sent over the port. ## Differences There are a number of key limitations to the `MessageChannel` API in Workers: * Transfer lists are currently not supported. This means that you will not be able to transfer ownership of objects like `ArrayBuffer` or `MessagePort` between ports. * The `MessagePort` is not yet serializable. This means that you cannot send a `MessagePort` object through the `postMessage` method or via JSRPC calls. * The `'messageerror'` event is only partially supported. If the `'onmessage'` handler throws an error, the `'messageerror'` event will be triggered, however, it will not be triggered when there are errors serializing or deserializing the message data. Instead, the error will be thrown when the `postMessage` method is called on the sending port. * The `'close'` event will be emitted on both ports when one of the ports is closed, however it will not be emitted when the Worker is terminated or when one of the ports is garbage collected. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/messagechannel/#page","headline":"MessageChannel · Cloudflare Workers docs","description":"Channel messaging with MessageChannel and MessagePort","url":"https://developers.cloudflare.com/workers/runtime-apis/messagechannel/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Node.js APIs available in Cloudflare Workers title: Node.js compatibility image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Node.js compatibility Last updated Aug 12, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) When you write a Worker, you may need to import packages from [npm ↗](https://www.npmjs.com/). Many npm packages rely on APIs from the [Node.js runtime ↗](https://nodejs.org/en/about), and will not work unless these Node.js APIs are available. Cloudflare Workers provides a subset of Node.js APIs in two forms: 1. As built-in APIs provided by the Workers Runtime. Most of these APIs are full implementations of the corresponding Node.js APIs, while a few are partially supported. 2. As polyfill shim implementations that [Wrangler](https://developers.cloudflare.com/workers/wrangler/) adds to your Worker's code, allowing it to import the module, but calling API methods will throw errors. ## Get Started For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. Built-in Node.js APIs and polyfills are available without additional configuration. For these compatibility dates, `nodejs_compat` and `nodejs_compat_v2` are not used because the compatibility date enables the same behavior. Existing projects do not need to remove these flags when updating their compatibility date. Omit them from new configurations. For compatibility dates from `2024-09-23` through `2026-08-03`, add the `nodejs_compat` compatibility flag to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in: ```jsonc { "compatibility_date": "2026-08-03", "compatibility_flags": ["nodejs_compat"] } ``` ```toml compatibility_date = "2026-08-03" compatibility_flags = [ "nodejs_compat" ] ``` To turn off Node.js compatibility completely with a compatibility date of `2026-08-04` or later, remove the positive flags if present. Then add both `no_nodejs_compat` and `no_nodejs_compat_v2`. For configuration examples, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## Supported Node.js APIs The runtime APIs from Node.js listed in this section with the status "🟢 supported" are currently natively supported in the Workers Runtime. Items listed as "🟡 partially supported" include usable APIs, but do not implement the complete Node.js API surface. [Deprecated or experimental APIs from Node.js ↗](https://nodejs.org/docs/latest/api/documentation.html#stability-index), and APIs that do not fit in a serverless context, are not included in the supported API list in this section. Some import-only stubs for these APIs are listed separately in [Non-functional stub modules](#non-functional-stub-modules). | API Name | Natively supported by the Workers Runtime | | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | [Assertion testing](https://developers.cloudflare.com/workers/runtime-apis/nodejs/assert/) | 🟢 supported | | [Asynchronous context tracking](https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/) | 🟢 supported | | [Buffer](https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/) | 🟢 supported | | [Console ↗](https://nodejs.org/docs/latest/api/console.html) | 🟡 partially supported | | [Crypto](https://developers.cloudflare.com/workers/runtime-apis/nodejs/crypto/) | 🟢 supported | | [Debugger](https://developers.cloudflare.com/workers/observability/dev-tools/) | 🟢 supported via [Chrome DevTools integration](https://developers.cloudflare.com/workers/observability/dev-tools/) | | [Diagnostics Channel](https://developers.cloudflare.com/workers/runtime-apis/nodejs/diagnostics-channel/) | 🟢 supported | | [DNS](https://developers.cloudflare.com/workers/runtime-apis/nodejs/dns/) | 🟡 partially supported | | Errors | 🟢 supported | | [Events](https://developers.cloudflare.com/workers/runtime-apis/nodejs/eventemitter/) | 🟢 supported | | [File system](https://developers.cloudflare.com/workers/runtime-apis/nodejs/fs/) | 🟢 supported | | Globals | 🟢 supported | | [HTTP](https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/) | 🟢 supported | | [HTTPS](https://developers.cloudflare.com/workers/runtime-apis/nodejs/https/) | 🟢 supported | | [Module ↗](https://nodejs.org/docs/latest/api/module.html) | 🟡 partially supported | | [Net](https://developers.cloudflare.com/workers/runtime-apis/nodejs/net/) | 🟢 supported | | [OS ↗](https://nodejs.org/docs/latest/api/os.html) | 🟡 partially supported | | [Path](https://developers.cloudflare.com/workers/runtime-apis/nodejs/path/) | 🟢 supported | | [Performance hooks ↗](https://nodejs.org/docs/latest/api/perf%5Fhooks.html) | 🟡 partially supported | | [Process](https://developers.cloudflare.com/workers/runtime-apis/nodejs/process/) | 🟢 supported | | [Punycode ↗](https://nodejs.org/docs/latest/api/punycode.html) (deprecated) | 🟢 supported | | [Query strings ↗](https://nodejs.org/docs/latest/api/querystring.html) | 🟢 supported | | [Stream](https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/) | 🟢 supported | | [String decoder](https://developers.cloudflare.com/workers/runtime-apis/nodejs/string-decoder/) | 🟢 supported | | [Test runner](https://developers.cloudflare.com/workers/runtime-apis/nodejs/test/) | 🟡 partially supported | | [Timers](https://developers.cloudflare.com/workers/runtime-apis/nodejs/timers/) | 🟢 supported | | [TLS/SSL](https://developers.cloudflare.com/workers/runtime-apis/nodejs/tls/) | 🟡 partially supported | | [URL](https://developers.cloudflare.com/workers/runtime-apis/nodejs/url/) | 🟢 supported | | [Utilities](https://developers.cloudflare.com/workers/runtime-apis/nodejs/util/) | 🟢 supported | | [Web Crypto API](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) | 🟢 supported | | [Web Streams API](https://developers.cloudflare.com/workers/runtime-apis/streams/) | 🟢 supported | | [Zlib](https://developers.cloudflare.com/workers/runtime-apis/nodejs/zlib/) | 🟢 supported | Unless otherwise specified, native implementations of Node.js APIs in Workers are intended to match the implementation in the [Current release of Node.js ↗](https://github.com/nodejs/release#release-schedule). If an API you wish to use is missing and you want to suggest that Workers support it, please add a post or comment in the [Node.js APIs discussions category ↗](https://github.com/cloudflare/workerd/discussions/categories/node-js-apis) on GitHub. ### Non-functional stub modules Some Node.js modules are available as non-functional stubs. A stub can be imported or required, but does not provide a working implementation of the underlying Node.js API. These stubs exist so packages that check for the presence of a module can load in Workers, but they are not suitable for direct use in application code. The following stubs are enabled automatically only when the `nodejs_compat` compatibility flag is enabled and your Worker's compatibility date is on or after the date shown. To enable one earlier, add the corresponding enable flag. To keep one unavailable after that date, add the corresponding disable flag. | Stub module | Enabled with nodejs\_compat on or after | Enable flag | Disable flag | | ---------------------------------------------------------------------------------- | --------------------------------------- | --------------------------------------- | ---------------------------------------- | | [node:http2 ↗](https://nodejs.org/docs/latest/api/http2.html) | 2025-09-01 | enable\_nodejs\_http2\_module | disable\_nodejs\_http2\_module | | [node:vm ↗](https://nodejs.org/docs/latest/api/vm.html) | 2025-10-01 | enable\_nodejs\_vm\_module | disable\_nodejs\_vm\_module | | [node:cluster ↗](https://nodejs.org/docs/latest/api/cluster.html) | 2025-12-04 | enable\_nodejs\_cluster\_module | disable\_nodejs\_cluster\_module | | [node:domain ↗](https://nodejs.org/docs/latest/api/domain.html) | 2025-12-04 | enable\_nodejs\_domain\_module | disable\_nodejs\_domain\_module | | [node:trace\_events ↗](https://nodejs.org/docs/latest/api/tracing.html) | 2025-12-04 | enable\_nodejs\_trace\_events\_module | disable\_nodejs\_trace\_events\_module | | [node:wasi ↗](https://nodejs.org/docs/latest/api/wasi.html) | 2025-12-04 | enable\_nodejs\_wasi\_module | disable\_nodejs\_wasi\_module | | node:\_stream\_wrap | 2026-01-29 | enable\_nodejs\_stream\_wrap\_module | disable\_nodejs\_stream\_wrap\_module | | [node:dgram ↗](https://nodejs.org/docs/latest/api/dgram.html) | 2026-01-29 | enable\_nodejs\_dgram\_module | disable\_nodejs\_dgram\_module | | [node:inspector ↗](https://nodejs.org/docs/latest/api/inspector.html) | 2026-01-29 | enable\_nodejs\_inspector\_module | disable\_nodejs\_inspector\_module | | [node:sqlite ↗](https://nodejs.org/docs/latest/api/sqlite.html) | 2026-01-29 | enable\_nodejs\_sqlite\_module | disable\_nodejs\_sqlite\_module | | [node:child\_process ↗](https://nodejs.org/docs/latest/api/child%5Fprocess.html) | 2026-03-17 | enable\_nodejs\_child\_process\_module | disable\_nodejs\_child\_process\_module | | [node:readline ↗](https://nodejs.org/docs/latest/api/readline.html) | 2026-03-17 | enable\_nodejs\_readline\_module | disable\_nodejs\_readline\_module | | [node:repl ↗](https://nodejs.org/docs/latest/api/repl.html) | 2026-03-17 | enable\_nodejs\_repl\_module | disable\_nodejs\_repl\_module | | [node:tty ↗](https://nodejs.org/docs/latest/api/tty.html) | 2026-03-17 | enable\_nodejs\_tty\_module | disable\_nodejs\_tty\_module | | [node:v8 ↗](https://nodejs.org/docs/latest/api/v8.html) | 2026-03-17 | enable\_nodejs\_v8\_module | disable\_nodejs\_v8\_module | | [node:worker\_threads ↗](https://nodejs.org/docs/latest/api/worker%5Fthreads.html) | 2026-03-17 | enable\_nodejs\_worker\_threads\_module | disable\_nodejs\_worker\_threads\_module | ### Node.js API Polyfills Node.js APIs that are not yet supported in the Workers runtime are polyfilled via [Wrangler](https://developers.cloudflare.com/workers/wrangler/), which uses [unenv ↗](https://github.com/unjs/unenv). If the `nodejs_compat` [compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/) is enabled, and your Worker's [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) is 2024-09-23 or later, Wrangler will automatically inject polyfills into your Worker's code. Adding polyfills maximizes compatibility with existing npm packages by providing modules with mocked methods. Calling these mocked methods will either noop or will throw an error with a message like: ```plaintext [unenv] is not implemented yet! ``` This allows you to import packages that use these Node.js modules, even if certain methods are not supported. ## Enable only AsyncLocalStorage If you need to enable only the Node.js `AsyncLocalStorage` API, you can enable the `nodejs_als` compatibility flag: ```jsonc { "compatibility_flags": ["nodejs_als"], } ``` ```toml compatibility_flags = [ "nodejs_als" ] ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/#page","headline":"Node.js compatibility · Cloudflare Workers docs","description":"Node.js APIs available in Cloudflare Workers","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-12","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js assert module in Cloudflare Workers for testing assertions and value comparisons. title: assert image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # assert Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/assert/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [node:assert ↗](https://nodejs.org/docs/latest/api/assert.html) module in Node.js provides a number of useful assertions that are useful when building tests. ```js import { strictEqual, deepStrictEqual, ok, doesNotReject } from "node:assert"; strictEqual(1, 1); // ok! strictEqual(1, "1"); // fails! throws AssertionError deepStrictEqual({ a: { b: 1 } }, { a: { b: 1 } }); // ok! deepStrictEqual({ a: { b: 1 } }, { a: { b: 2 } }); // fails! throws AssertionError ok(true); // ok! ok(false); // fails! throws AssertionError await doesNotReject(async () => {}); // ok! await doesNotReject(async () => { throw new Error("boom"); }); // fails! throws AssertionError ``` Note In the Workers implementation of `assert`, all assertions run in, what Node.js calls, the strict assertion mode. In strict assertion mode, non-strict methods behave like their corresponding strict methods. For example, `deepEqual()` will behave like `deepStrictEqual()`. Refer to the [Node.js documentation for assert ↗](https://nodejs.org/dist/latest-v19.x/docs/api/assert.html) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/assert/#page","headline":"assert · Cloudflare Workers docs","description":"Use the Node.js assert module in Cloudflare Workers for testing assertions and value comparisons.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/assert/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js AsyncLocalStorage API in Cloudflare Workers to maintain context across asynchronous operations. title: AsyncLocalStorage image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # AsyncLocalStorage Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Cloudflare Workers provides an implementation of a subset of the Node.js [AsyncLocalStorage ↗](https://nodejs.org/dist/latest-v18.x/docs/api/async%5Fcontext.html#class-asynclocalstorage) API for creating in-memory stores that remain coherent through asynchronous operations. ## Constructor ```js import { AsyncLocalStorage } from "node:async_hooks"; const asyncLocalStorage = new AsyncLocalStorage(); ``` * `new AsyncLocalStorage()` : AsyncLocalStorage * Returns a new `AsyncLocalStorage` instance. ## Methods * `getStore()` : any * Returns the current store. If called outside of an asynchronous context initialized by calling `asyncLocalStorage.run()`, it returns `undefined`. * `run(storeany, callbackfunction, ...argsarguments)` : any * Runs a function synchronously within a context and returns its return value. The store is not accessible outside of the callback function. The store is accessible to any asynchronous operations created within the callback. The optional `args` are passed to the callback function. If the callback function throws an error, the error is thrown by `run()` also. * `exit(callbackfunction, ...argsarguments)` : any * Runs a function synchronously outside of a context and returns its return value. This method is equivalent to calling `run()` with the `store` value set to `undefined`. ## Static Methods * `AsyncLocalStorage.bind(fn)` : function * Captures the asynchronous context that is current when `bind()` is called and returns a function that enters that context before calling the passed in function. * `AsyncLocalStorage.snapshot()` : function * Captures the asynchronous context that is current when `snapshot()` is called and returns a function that enters that context before calling a given function. ## Examples ### Fetch Listener ```js import { AsyncLocalStorage } from 'node:async_hooks'; const asyncLocalStorage = new AsyncLocalStorage(); let idSeq = 0; export default { async fetch(req) { return asyncLocalStorage.run(idSeq++, () => { // Simulate some async activity... await scheduler.wait(1000); return new Response(asyncLocalStorage.getStore()); }); } }; ``` ### Multiple stores The API supports multiple `AsyncLocalStorage` instances to be used concurrently. ```js import { AsyncLocalStorage } from 'node:async_hooks'; const als1 = new AsyncLocalStorage(); const als2 = new AsyncLocalStorage(); export default { async fetch(req) { return als1.run(123, () => { return als2.run(321, () => { // Simulate some async activity... await scheduler.wait(1000); return new Response(`${als1.getStore()}-${als2.getStore()}`); }); }); } }; ``` ### Unhandled Rejections When a `Promise` rejects and the rejection is unhandled, the async context propagates to the `'unhandledrejection'` event handler: ```js import { AsyncLocalStorage } from "node:async_hooks"; const asyncLocalStorage = new AsyncLocalStorage(); let idSeq = 0; addEventListener("unhandledrejection", (event) => { console.log(asyncLocalStorage.getStore(), "unhandled rejection!"); }); export default { async fetch(req) { return asyncLocalStorage.run(idSeq++, () => { // Cause an unhandled rejection! throw new Error("boom"); }); }, }; ``` ### `AsyncLocalStorage.bind()` and `AsyncLocalStorage.snapshot()` ```js import { AsyncLocalStorage } from "node:async_hooks"; const als = new AsyncLocalStorage(); function foo() { console.log(als.getStore()); } function bar() { console.log(als.getStore()); } const oneFoo = als.run(123, () => AsyncLocalStorage.bind(foo)); oneFoo(); // prints 123 const snapshot = als.run("abc", () => AsyncLocalStorage.snapshot()); snapshot(foo); // prints 'abc' snapshot(bar); // prints 'abc' ``` ```js import { AsyncLocalStorage } from "node:async_hooks"; const als = new AsyncLocalStorage(); class MyResource { #runInAsyncScope = AsyncLocalStorage.snapshot(); doSomething() { this.#runInAsyncScope(() => { return als.getStore(); }); } } const myResource = als.run(123, () => new MyResource()); console.log(myResource.doSomething()); // prints 123 ``` ## `AsyncResource` The [AsyncResource ↗](https://nodejs.org/dist/latest-v18.x/docs/api/async%5Fcontext.html#class-asyncresource) class is a component of Node.js' async context tracking API that allows users to create their own async contexts. Objects that extend from `AsyncResource` are capable of propagating the async context in much the same way as promises. Note that `AsyncLocalStorage.snapshot()` and `AsyncLocalStorage.bind()` provide a better approach. `AsyncResource` is provided solely for backwards compatibility with Node.js. ### Constructor ```js import { AsyncResource, AsyncLocalStorage } from "node:async_hooks"; const als = new AsyncLocalStorage(); class MyResource extends AsyncResource { constructor() { // The type string is required by Node.js but unused in Workers. super("MyResource"); } doSomething() { this.runInAsyncScope(() => { return als.getStore(); }); } } const myResource = als.run(123, () => new MyResource()); console.log(myResource.doSomething()); // prints 123 ``` * `new AsyncResource(typestring, optionsAsyncResourceOptions)` : AsyncResource * Returns a new `AsyncResource`. Importantly, while the constructor arguments are required in Node.js' implementation of `AsyncResource`, they are not used in Workers. * `AsyncResource.bind(fnfunction, typestring, thisArgany)` * Binds the given function to the current async context. ### Methods * `asyncResource.bind(fnfunction, thisArgany)` * Binds the given function to the async context associated with this `AsyncResource`. * `asyncResource.runInAsyncScope(fnfunction, thisArgany, ...argsarguments)` * Call the provided function with the given arguments in the async context associated with this `AsyncResource`. ## Caveats * The `AsyncLocalStorage` implementation provided by Workers intentionally omits support for the [asyncLocalStorage.enterWith() ↗](https://nodejs.org/dist/latest-v18.x/docs/api/async%5Fcontext.html#asynclocalstorageenterwithstore) and [asyncLocalStorage.disable() ↗](https://nodejs.org/dist/latest-v18.x/docs/api/async%5Fcontext.html#asynclocalstoragedisable) methods. * Workers does not implement the full [async\_hooks ↗](https://nodejs.org/dist/latest-v18.x/docs/api/async%5Fhooks.html) API upon which Node.js' implementation of `AsyncLocalStorage` is built. * Workers does not implement the ability to create an `AsyncResource` with an explicitly identified trigger context as allowed by Node.js. This means that a new `AsyncResource` will always be bound to the async context in which it was created. * Thenables (non-Promise objects that expose a `then()` method) are not fully supported when using `AsyncLocalStorage`. When working with thenables, instead use [AsyncLocalStorage.snapshot() ↗](https://nodejs.org/api/async%5Fcontext.html#static-method-asynclocalstoragesnapshot) to capture a snapshot of the current context. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/#page","headline":"AsyncLocalStorage · Cloudflare Workers docs","description":"Use the Node.js AsyncLocalStorage API in Cloudflare Workers to maintain context across asynchronous operations.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/asynclocalstorage/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js Buffer API in Cloudflare Workers to manipulate binary data with encoding and decoding support. title: Buffer image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Buffer Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [Buffer ↗](https://nodejs.org/docs/latest/api/buffer.html) API in Node.js is one of the most commonly used Node.js APIs for manipulating binary data. Every `Buffer` instance extends from the standard [Uint8Array ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Uint8Array) class, but adds a range of unique capabilities such as built-in base64 and hex encoding/decoding, byte-order manipulation, and encoding-aware substring searching. ```js import { Buffer } from "node:buffer"; const buf = Buffer.from("hello world", "utf8"); console.log(buf.toString("hex")); // Prints: 68656c6c6f20776f726c64 console.log(buf.toString("base64")); // Prints: aGVsbG8gd29ybGQ= ``` A Buffer extends from `Uint8Array`. Therefore, it can be used in any Workers API that currently accepts `Uint8Array`, such as creating a new Response: ```js const response = new Response(Buffer.from("hello world")); ``` You can also use the `Buffer` API when interacting with streams: ```js const writable = getWritableStreamSomehow(); const writer = writable.getWriter(); writer.write(Buffer.from("hello world")); ``` One key difference between the Workers implementation of `Buffer` and the Node.js implementation is that some methods of creating a `Buffer` in Node.js will allocate those from a global memory pool as a performance optimization. The Workers implementation does not use a memory pool and all `Buffer` instances are allocated independently. Further, in Node.js it is possible to allocate a `Buffer` with uninitialized memory using the `Buffer.allocUnsafe()` method. This is not supported in Workers and `Buffer`instances are always initialized so that the `Buffer` is always filled with null bytes (`0x00`) when allocated. Refer to the [Node.js documentation for Buffer ↗](https://nodejs.org/dist/latest-v19.x/docs/api/buffer.html) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/#page","headline":"Buffer · Cloudflare Workers docs","description":"Use the Node.js Buffer API in Cloudflare Workers to manipulate binary data with encoding and decoding support.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js crypto module in Cloudflare Workers for hashing, encryption, signing, and verification. title: crypto image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # crypto Last updated Aug 12, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/crypto/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [node:crypto ↗](https://nodejs.org/docs/latest/api/crypto.html) module provides cryptographic functionality that includes a set of wrappers for OpenSSL's hash, HMAC, cipher, decipher, sign, and verify functions. All `node:crypto` APIs are fully supported in Workers with the following exceptions: * The functions [generateKeyPair ↗](https://nodejs.org/api/crypto.html#cryptogeneratekeypairtype-options-callback) and [generateKeyPairSync ↗](https://nodejs.org/api/crypto.html#cryptogeneratekeypairsynctype-options)do not support DSA or DH key pairs. * `argon2` and `argon2Sync` are not supported. * `ed448` and `x448` curves are not supported. * It is not possible to manually enable or disable [FIPS mode ↗](https://nodejs.org/docs/latest/api/crypto.html#fips-mode). The full `node:crypto` API is documented in the [Node.js documentation for node:crypto ↗](https://nodejs.org/api/crypto.html). The [WebCrypto API](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) is also available within Cloudflare Workers. This does not require the `nodejs_compat` compatibility flag. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/crypto/#page","headline":"crypto · Cloudflare Workers docs","description":"Use the Node.js crypto module in Cloudflare Workers for hashing, encryption, signing, and verification.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/crypto/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-12","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js diagnostics_channel API in Cloudflare Workers for low-overhead diagnostic event reporting. title: Diagnostics Channel image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Diagnostics Channel Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/diagnostics-channel/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [diagnostics\_channel ↗](https://nodejs.org/dist/latest-v20.x/docs/api/diagnostics%5Fchannel.html) module provides an API to create named channels to report arbitrary message data for diagnostics purposes. The API is essentially a simple event pub/sub model that is specifically designed to support low-overhead diagnostics reporting. ```js import { channel, hasSubscribers, subscribe, unsubscribe, tracingChannel, } from "node:diagnostics_channel"; // For publishing messages to a channel, acquire a channel object: const myChannel = channel("my-channel"); // Any JS value can be published to a channel. myChannel.publish({ foo: "bar" }); // For receiving messages on a channel, use subscribe: subscribe("my-channel", (message) => { console.log(message); }); ``` All `Channel` instances are singletons per each Isolate/context (for example, the same entry point). Subscribers are always invoked synchronously and in the order they were registered, much like an `EventTarget` or Node.js `EventEmitter` class. ## Integration with Tail Workers When using [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/), all messages published to any channel will be forwarded also to the [Tail Worker](https://developers.cloudflare.com/workers/observability/logs/tail-workers/). Within the Tail Worker, the diagnostic channel messages can be accessed via the `diagnosticsChannelEvents` property: ```js export default { async tail(events) { for (const event of events) { for (const messageData of event.diagnosticsChannelEvents) { console.log( messageData.timestamp, messageData.channel, messageData.message, ); } } }, }; ``` Note that message published to the tail worker is passed through the [structured clone algorithm ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm) (same mechanism as the [structuredClone() ↗](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) API) so only values that can be successfully cloned are supported. ## `TracingChannel` Per the Node.js documentation, "[TracingChannel ↗](https://nodejs.org/api/diagnostics%5Fchannel.html#class-tracingchannel) is a collection of \[Channels\] which together express a single traceable action. `TracingChannel` is used to formalize and simplify the process of producing events for tracing application flow." ```js import { tracingChannel } from "node:diagnostics_channel"; import { AsyncLocalStorage } from "node:async_hooks"; const channels = tracingChannel("my-channel"); const requestId = new AsyncLocalStorage(); channels.start.bindStore(requestId); channels.subscribe({ start(message) { console.log(requestId.getStore()); // { requestId: '123' } // Handle start message }, end(message) { console.log(requestId.getStore()); // { requestId: '123' } // Handle end message }, asyncStart(message) { console.log(requestId.getStore()); // { requestId: '123' } // Handle asyncStart message }, asyncEnd(message) { console.log(requestId.getStore()); // { requestId: '123' } // Handle asyncEnd message }, error(message) { console.log(requestId.getStore()); // { requestId: '123' } // Handle error message }, }); // The subscriber handlers will be invoked while tracing the execution of the async // function passed into `channel.tracePromise`... channel.tracePromise( async () => { // Perform some asynchronous work... }, { requestId: "123" }, ); ``` Refer to the [Node.js documentation for diagnostics\_channel ↗](https://nodejs.org/dist/latest-v20.x/docs/api/diagnostics%5Fchannel.html) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/diagnostics-channel/#page","headline":"Diagnostics Channel · Cloudflare Workers docs","description":"Use the Node.js diagnostics\\_channel API in Cloudflare Workers for low-overhead diagnostic event reporting.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/diagnostics-channel/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js dns module in Cloudflare Workers for DNS name resolution via DNS over HTTPS. title: dns image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # dns Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/dns/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). You can use [node:dns ↗](https://nodejs.org/api/dns.html) for name resolution via [DNS over HTTPS](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/) using [Cloudflare DNS ↗](https://www.cloudflare.com/application-services/products/dns/) at 1.1.1.1. ```js import dns from "node:dns"; let response = await dns.promises.resolve4("cloudflare.com", "NS"); ``` ```ts import dns from 'node:dns'; let response = await dns.promises.resolve4('cloudflare.com', 'NS'); ``` All `node:dns` functions are available, except `lookup`, `lookupService`, and `resolve` which throw "Not implemented" errors when called. Note DNS requests will execute a subrequest, counts for your [Worker's subrequest limit](https://developers.cloudflare.com/workers/platform/limits/#subrequests). The full `node:dns` API is documented in the [Node.js documentation for node:dns ↗](https://nodejs.org/api/dns.html). ```plaintext ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/dns/#page","headline":"dns · Cloudflare Workers docs","description":"Use the Node.js dns module in Cloudflare Workers for DNS name resolution via DNS over HTTPS.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/dns/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js EventEmitter API in Cloudflare Workers to emit and listen for named events. title: EventEmitter image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # EventEmitter Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/eventemitter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). An [EventEmitter ↗](https://nodejs.org/docs/latest/api/events.html#class-eventemitter)is an object that emits named events that cause listeners to be called. ```js import { EventEmitter } from "node:events"; const emitter = new EventEmitter(); emitter.on("hello", (...args) => { console.log(...args); // 1 2 3 }); emitter.emit("hello", 1, 2, 3); ``` The implementation in the Workers runtime supports the entire Node.js `EventEmitter` API. This includes the [captureRejections ↗](https://nodejs.org/docs/latest/api/events.html#capture-rejections-of-promises)option that allows improved handling of async functions as event handlers: ```js const emitter = new EventEmitter({ captureRejections: true }); emitter.on("hello", async (...args) => { throw new Error("boom"); }); emitter.on("error", (err) => { // the async promise rejection is emitted here! }); ``` Like Node.js, when an `'error'` event is emitted on an `EventEmitter` and there is no listener for it, the error will be immediately thrown. However, in Node.js it is possible to add a handler on the `process` object for the `'uncaughtException'` event to catch globally uncaught exceptions. The `'uncaughtException'` event, however, is currently not implemented in the Workers runtime. It is strongly recommended to always add an `'error'` listener to any `EventEmitter` instance. Refer to the [Node.js documentation for EventEmitter ↗](https://nodejs.org/api/events.html#class-eventemitter) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/eventemitter/#page","headline":"EventEmitter · Cloudflare Workers docs","description":"Use the Node.js EventEmitter API in Cloudflare Workers to emit and listen for named events.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/eventemitter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js fs module in Cloudflare Workers to access a virtual file system for reading and writing files. title: fs image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # fs Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/fs/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). You can use [node:fs ↗](https://nodejs.org/api/fs.html) to access a virtual file system in Workers. The `node:fs` module is available in Workers runtimes that support Node.js compatibility using the `nodejs_compat` compatibility flag. Any Worker running with `nodejs_compat` enabled and with a compatibility date of `2025-09-01` or later will have access to `node:fs` by default. It is also possible to enable `node:fs` on Workers with an earlier compatibility date using a combination of the `nodejs_compat` and `enable_nodejs_fs_module`flags. To disable `node:fs` you can set the `disable_nodejs_fs_module` flag. ```js import { readFileSync, writeFileSync } from "node:fs"; const config = readFileSync("/bundle/config.txt", "utf8"); writeFileSync("/tmp/abc.txt", "Hello, world!"); ``` The Workers Virtual File System (VFS) is a memory-based file system that allows you to read modules included in your Worker bundle as read-only files, access a directory for writing temporary files, or access common [character devices ↗](https://linux-kernel-labs.github.io/refs/heads/master/labs/device%5Fdrivers.html) like `/dev/null`, `/dev/random`, `/dev/full`, and `/dev/zero`. The directory structure initially looks like: ```plaintext /bundle └── (one file for each module in your Worker bundle) /tmp └── (empty, but you can write files, create directories, symlinks, etc) /dev ├── null ├── random ├── full └── zero ``` The `/bundle` directory contains the files for all modules included in your Worker bundle, which you can read using APIs like `readFileSync` or `read(...)`, etc. These are always read-only. Reading from the bundle can be useful when you need to read a config file or a template. ```js import { readFileSync } from "node:fs"; // The config.txt file would be included in your Worker bundle. // Refer to the Wrangler documentation for details on how to // include additional files. const config = readFileSync("/bundle/config.txt", "utf8"); export default { async fetch(request) { return new Response(`Config contents: ${config}`); }, }; ``` The `/tmp` directory is writable, and you can use it to create temporary files or directories. You can also create symlinks in this directory. However, the contents of `/tmp` are not persistent and are unique to each request. This means that files created in `/tmp` within the context of one request will not be available in other concurrent or subsequent requests. ```js import { writeFileSync, readFileSync } from "node:fs"; export default { fetch(request) { // The file `/tmp/hello.txt` will only exist for the duration // of this request. writeFileSync("/tmp/hello.txt", "Hello, world!"); const contents = readFileSync("/tmp/hello.txt", "utf8"); return new Response(`File contents: ${contents}`); }, }; ``` The `/dev` directory contains common character devices: * `/dev/null`: A null device that discards all data written to it and returns EOF on read. * `/dev/random`: A device that provides random bytes on reads and discards all data written to it. Reading from `/dev/random` is only permitted when within the context of a request. * `/dev/full`: A device that always returns EOF on reads and discards all data written to it. * `/dev/zero`: A device that provides an infinite stream of zero bytes on reads and discards all data written to it. All operations on the VFS are synchronous. You can use the synchronous, asynchronous callback, or promise-based APIs provided by the `node:fs` module but all operations will be performed synchronously. Timestamps for files in the VFS are currently always set to the Unix epoch (`1970-01-01T00:00:00Z`). This means that operations that rely on timestamps, like `fs.stat`, will always return the same timestamp for all files in the VFS. This is a temporary limitation that will be addressed in a future release. Since all temporary files are held in memory, the total size of all temporary files and directories created count towards your Worker’s memory limit. If you exceed this limit, the Worker instance will be terminated and restarted. The file system implementation has the following limits: * The maximum total length of a file path is 4096 characters, including path separators. Because paths are handled as file URLs internally, the limit accounts for percent-encoding of special characters, decoding characters that do not need encoding before the limit is checked. For example, the path `/tmp/abcde%66/ghi%zz' is 18 characters long because the `%66`does not need to be percent-encoded and is therefore counted as one character, while the`%zz\` is an invalid percent-encoding that is counted as 3 characters. * The maximum number of path segments is 48\. For example, the path `/a/b/c` is 3 segments. * The maximum size of an individual file is 128 MB total. The following `node:fs` APIs are not supported in Workers, or are only partially supported: * `fs.watch` and `fs.watchFile` operations for watching for file changes. * The `fs.globSync()` and other glob APIs have not yet been implemented. * The `force` option in the `fs.rm` API has not yet been implemented. * Timestamps for files are always set to the Unix epoch (`1970-01-01T00:00:00Z`). * File permissions and ownership are not supported. The full `node:fs` API is documented in the [Node.js documentation for node:fs ↗](https://nodejs.org/api/fs.html). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/fs/#page","headline":"fs · Cloudflare Workers docs","description":"Use the Node.js fs module in Cloudflare Workers to access a virtual file system for reading and writing files.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/fs/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js http module in Cloudflare Workers for client and server-side HTTP functionality. title: http image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # http Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## Compatibility flags ### Client-side methods To use the HTTP client-side methods (`http.get`, `http.request`, etc.), you must enable the [enable\_nodejs\_http\_modules](https://developers.cloudflare.com/workers/configuration/compatibility-flags/) compatibility flag in addition to the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) flag. This flag is automatically enabled for Workers using a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) of `2025-08-15` or later when `nodejs_compat` is enabled. For Workers using an earlier compatibility date, you can manually enable it by adding the flag to your Wrangler configuration file: ```jsonc { "compatibility_flags": [ "nodejs_compat", "enable_nodejs_http_modules" ] } ``` ```toml compatibility_flags = [ "nodejs_compat", "enable_nodejs_http_modules" ] ``` ### Server-side methods To use the HTTP server-side methods (`http.createServer`, `http.Server`, `http.ServerResponse`), you must enable the `enable_nodejs_http_server_modules` compatibility flag in addition to the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) flag. This flag is automatically enabled for Workers using a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) of `2025-09-01` or later when `nodejs_compat` is enabled. For Workers using an earlier compatibility date, you can manually enable it by adding the flag to your Wrangler configuration file: ```jsonc { "compatibility_flags": [ "nodejs_compat", "enable_nodejs_http_server_modules" ] } ``` ```toml compatibility_flags = [ "nodejs_compat", "enable_nodejs_http_server_modules" ] ``` To use both client-side and server-side methods, enable both flags: ```jsonc { "compatibility_flags": [ "nodejs_compat", "enable_nodejs_http_modules", "enable_nodejs_http_server_modules" ] } ``` ```toml compatibility_flags = [ "nodejs_compat", "enable_nodejs_http_modules", "enable_nodejs_http_server_modules" ] ``` ## get An implementation of the Node.js [http.get ↗](https://nodejs.org/docs/latest/api/http.html#httpgetoptions-callback) method. The `get` method performs a GET request to the specified URL and invokes the callback with the response. It's a convenience method that simplifies making HTTP GET requests without manually configuring request options. Because `get` is a wrapper around `fetch(...)`, it may be used only within an exported fetch or similar handler. Outside of such a handler, attempts to use `get` will throw an error. ```js import { get } from "node:http"; export default { async fetch() { const { promise, resolve, reject } = Promise.withResolvers(); get("http://example.org", (res) => { let data = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { resolve(new Response(data)); }); res.on("error", reject); }).on("error", reject); return promise; }, }; ``` The implementation of `get` in Workers is a wrapper around the global [fetch API ↗](https://developers.cloudflare.com/workers/runtime-apis/fetch/)and is therefore subject to the same [limits ↗](https://developers.cloudflare.com/workers/platform/limits/). As shown in the example above, it is necessary to arrange for requests to be correctly awaited in the `fetch` handler using a promise or the fetch may be canceled prematurely when the handler returns. ## request An implementation of the Node.js [\`http.request' ↗](https://nodejs.org/docs/latest/api/http.html#httprequesturl-options-callback) method. The `request` method creates an HTTP request with customizable options like method, headers, and body. It provides full control over the request configuration and returns a Node.js [stream.Writable ↗](https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/) for sending request data. Because `request` is a wrapper around `fetch(...)`, it may be used only within an exported fetch or similar handler. Outside of such a handler, attempts to use `request` will throw an error. ```js import { get } from "node:http"; export default { async fetch() { const { promise, resolve, reject } = Promise.withResolvers(); get( { method: "GET", protocol: "http:", hostname: "example.org", path: "/", }, (res) => { let data = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { resolve(new Response(data)); }); res.on("error", reject); }, ) .on("error", reject) .end(); return promise; }, }; ``` The following options passed to the `request` (and `get`) method are not supported due to the differences required by Cloudflare Workers implementation of `node:http` as a wrapper around the global `fetch` API: * `maxHeaderSize` * `insecureHTTPParser` * `createConnection` * `lookup` * `socketPath` ## OutgoingMessage The [OutgoingMessage ↗](https://nodejs.org/docs/latest/api/http.html#class-httpoutgoingmessage) class represents an HTTP response that is sent to the client. It provides methods for writing response headers and body, as well as for ending the response. `OutgoingMessage` extends from the Node.js [stream.Writable stream class ↗](https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/). The `OutgoingMessage` class is a base class for outgoing HTTP messages (both requests and responses). It provides methods for writing headers and body data, as well as for ending the message. `OutgoingMessage` extends from the [Writable stream class ↗](https://nodejs.org/docs/latest/api/stream.html#class-streamwritable). Both `ClientRequest` and `ServerResponse` both extend from and inherit from `OutgoingMessage`. ## IncomingMessage The `IncomingMessage` class represents an HTTP request that is received from the client. It provides methods for reading request headers and body, as well as for ending the request. `IncomingMessage` extends from the `Readable` stream class. The `IncomingMessage` class represents an HTTP message (request or response). It provides methods for reading headers and body data. `IncomingMessage` extends from the `Readable` stream class. ```js import { get, IncomingMessage } from "node:http"; import { ok, strictEqual } from "node:assert"; export default { async fetch() { // ... get("http://example.org", (res) => { ok(res instanceof IncomingMessage); }); // ... }, }; ``` The Workers implementation includes a `cloudflare` property on `IncomingMessage` objects: ```js import { createServer } from "node:http"; import { httpServerHandler } from "cloudflare:node"; const server = createServer((req, res) => { console.log(req.cloudflare.cf.country); console.log(req.cloudflare.cf.ray); res.write("Hello, World!"); res.end(); }); server.listen(8080); export default httpServerHandler({ port: 8080 }); ``` The `cloudflare.cf` property contains [Cloudflare-specific request properties](https://developers.cloudflare.com/workers/runtime-apis/request/#incomingrequestcfproperties). The following differences exist between the Workers implementation and Node.js: * Trailer headers are not supported * The `socket` attribute **does not extend from `net.Socket`** and only contains the following properties: `encrypted`, `remoteFamily`, `remoteAddress`, `remotePort`, `localAddress`, `localPort`, and `destroy()` method. * The following `socket` attributes behave differently than their Node.js counterparts: * `remoteAddress` will return `127.0.0.1` when ran locally * `remotePort` will return a random port number between 2^15 and 2^16 * `localAddress` will return the value of request's `host` header if exists. Otherwise, it will return `127.0.0.1` * `localPort` will return the port number assigned to the server instance * `req.socket.destroy()` falls through to `req.destroy()` ## Agent A partial implementation of the Node.js [\`http.Agent' ↗](https://nodejs.org/docs/latest/api/http.html#class-httpagent) class. An `Agent` manages HTTP connection reuse by maintaining request queues per host/port. In the workers environment, however, such low-level management of the network connection, ports, etc, is not relevant because it is handled by the Cloudflare infrastructure instead. Accordingly, the implementation of `Agent` in Workers is a stub implementation that does not support connection pooling or keep-alive. ```js import { Agent } from "node:http"; import { strictEqual } from "node:assert"; const agent = new Agent(); strictEqual(agent.protocol, "http:"); ``` ## createServer An implementation of the Node.js [http.createServer ↗](https://nodejs.org/docs/latest/api/http.html#httpcreateserveroptions-requestlistener) method. The `createServer` method creates an HTTP server instance that can handle incoming requests. ```js import { createServer } from "node:http"; import { httpServerHandler } from "cloudflare:node"; const server = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello from Node.js HTTP server!"); }); server.listen(8080); export default httpServerHandler({ port: 8080 }); ``` ## Node.js integration ### httpServerHandler The `httpServerHandler` function integrates Node.js HTTP servers with the Cloudflare Workers request model. It supports two API patterns: ```js import http from "node:http"; import { httpServerHandler } from "cloudflare:node"; const server = http.createServer((req, res) => { res.end("hello world"); }); // Pass server directly (simplified) - automatically calls listen() if needed export default httpServerHandler(server); // Or use port-based routing for multiple servers server.listen(8080); export default httpServerHandler({ port: 8080 }); ``` The handler automatically routes incoming Worker requests to your Node.js server. When using port-based routing, the port number acts as a routing key to determine which server handles requests, allowing multiple servers to coexist in the same Worker. ### handleAsNodeRequest For more direct control over request routing, you can use the `handleAsNodeRequest` function from `cloudflare:node`. This function directly routes a Worker request to a Node.js server running on a specific port: ```js import { createServer } from "node:http"; import { handleAsNodeRequest } from "cloudflare:node"; const server = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello from Node.js HTTP server!"); }); server.listen(8080); export default { fetch(request) { return handleAsNodeRequest(8080, request); }, }; ``` This approach gives you full control over the fetch handler while still leveraging Node.js HTTP servers for request processing. Note Failing to call `close()` on an HTTP server may result in the server persisting until the worker is destroyed. In most cases, this is not an issue since servers typically live for the lifetime of the worker. However, if you need to create multiple servers during a worker's lifetime or want explicit lifecycle control (such as in test scenarios), call `close()` when you're done with the server, or use [explicit resource management ↗](https://v8.dev/features/explicit-resource-management). ## Server An implementation of the Node.js [http.Server ↗](https://nodejs.org/docs/latest/api/http.html#class-httpserver) class. The `Server` class represents an HTTP server and provides methods for handling incoming requests. It extends the Node.js `EventEmitter` class and can be used to create custom server implementations. When using `httpServerHandler`, the port number specified in `server.listen()` acts as a routing key rather than an actual network port. The handler uses this port to determine which HTTP server instance should handle incoming requests, allowing multiple servers to coexist within the same Worker by using different port numbers for identification. Using a port value of `0` (or `null` or `undefined`) will result in a random port number being assigned. ```js import { Server } from "node:http"; import { httpServerHandler } from "cloudflare:node"; const server = new Server((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello from HTTP Server!" })); }); server.listen(8080); export default httpServerHandler({ port: 8080 }); ``` The following differences exist between the Workers implementation and Node.js: * Connection management methods such as `closeAllConnections()` and `closeIdleConnections()` are not implemented * Only `listen()` variants with a port number or no parameters are supported: `listen()`, `listen(0, callback)`, `listen(callback)`, etc. For reference, see the [Node.js documentation ↗](https://nodejs.org/docs/latest/api/net.html#serverlisten). * The following server options are not supported: `maxHeaderSize`, `insecureHTTPParser`, `keepAliveTimeout`, `connectionsCheckingInterval` ## ServerResponse An implementation of the Node.js [http.ServerResponse ↗](https://nodejs.org/docs/latest/api/http.html#class-httpserverresponse) class. The `ServerResponse` class represents the server-side response object that is passed to request handlers. It provides methods for writing response headers and body data, and extends the Node.js `Writable` stream class. ```js import { createServer, ServerResponse } from "node:http"; import { httpServerHandler } from "cloudflare:node"; import { ok } from "node:assert"; const server = createServer((req, res) => { ok(res instanceof ServerResponse); // Set multiple headers at once res.writeHead(200, { "Content-Type": "application/json", "X-Custom-Header": "Workers-HTTP", }); // Stream response data res.write('{"data": ['); res.write('{"id": 1, "name": "Item 1"},'); res.write('{"id": 2, "name": "Item 2"}'); res.write("]}"); // End the response res.end(); }); export default httpServerHandler(server); ``` The following methods and features are not supported in the Workers implementation: * `assignSocket()` and `detachSocket()` methods are not available * Trailer headers are not supported * `writeContinue()` and `writeEarlyHints()` methods are not available * 1xx responses in general are not supported ## Other differences between Node.js and Workers implementation of `node:http` Because the Workers implementation of `node:http` is a wrapper around the global `fetch` API, there are some differences in behavior and limitations compared to a standard Node.js environment: * `Connection` headers are not used. Workers will manage connections automatically. * `Content-Length` headers will be handled the same way as in the `fetch` API. If a body is provided, the header will be set automatically and manually set values will be ignored. * `Expect: 100-continue` headers are not supported. * Trailing headers are not supported. * The `'continue'` event is not supported. * The `'information'` event is not supported. * The `'socket'` event is not supported. * The `'upgrade'` event is not supported. * Gaining direct access to the underlying `socket` is not supported. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/#page","headline":"http · Cloudflare Workers docs","description":"Use the Node.js http module in Cloudflare Workers for client and server-side HTTP functionality.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js https module in Cloudflare Workers for TLS-encrypted HTTP client and server functionality. title: https image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # https Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/https/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## Compatibility flags ### Client-side methods To use the HTTPS client-side methods (`https.get`, `https.request`, etc.), you must enable the [enable\_nodejs\_http\_modules](https://developers.cloudflare.com/workers/configuration/compatibility-flags/) compatibility flag in addition to the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) flag. This flag is automatically enabled for Workers using a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) of `2025-08-15` or later when `nodejs_compat` is enabled. For Workers using an earlier compatibility date, you can manually enable it by adding the flag to your `wrangler.toml`: ```toml compatibility_flags = ["nodejs_compat", "enable_nodejs_http_modules"] ``` ### Server-side methods To use the HTTPS server-side methods (`https.createServer`, `https.Server`, `https.ServerResponse`), you must enable the `enable_nodejs_http_server_modules` compatibility flag in addition to the [nodejs\_compat](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) flag. This flag is automatically enabled for Workers using a [compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) of `2025-09-01` or later when `nodejs_compat` is enabled. For Workers using an earlier compatibility date, you can manually enable it by adding the flag to your `wrangler.toml`: ```toml compatibility_flags = ["nodejs_compat", "enable_nodejs_http_server_modules"] ``` To use both client-side and server-side methods, enable both flags: ```toml compatibility_flags = ["nodejs_compat", "enable_nodejs_http_modules", "enable_nodejs_http_server_modules"] ``` ## get An implementation of the Node.js [\`https.get' ↗](https://nodejs.org/docs/latest/api/https.html#httpsgetoptions-callback) method. The `get` method performs a GET request to the specified URL and invokes the callback with the response. This is a convenience method that simplifies making HTTPS GET requests without manually configuring request options. Because `get` is a wrapper around `fetch(...)`, it may be used only within an exported fetch or similar handler. Outside of such a handler, attempts to use `get` will throw an error. ```js import { get } from "node:https"; export default { async fetch() { const { promise, resolve, reject } = Promise.withResolvers(); get("https://example.com", (res) => { let data = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { resolve(new Response(data)); }); res.on("error", reject); }).on("error", reject); return promise; }, }; ``` The implementation of `get` in Workers is a wrapper around the global [fetch API ↗](https://developers.cloudflare.com/workers/runtime-apis/fetch/)and is therefore subject to the same [limits ↗](https://developers.cloudflare.com/workers/platform/limits/). As shown in the example above, it is necessary to arrange for requests to be correctly awaited in the `fetch` handler using a promise or the fetch may be canceled prematurely when the handler returns. ## request An implementation of the Node.js [\`https.request' ↗](https://nodejs.org/docs/latest/api/https.html#httpsrequestoptions-callback) method. The `request` method creates an HTTPS request with customizable options like method, headers, and body. It provides full control over the request configuration and returns a Node.js [stream.Writable ↗](https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/) for sending request data. Because `get` is a wrapper around `fetch(...)`, it may be used only within an exported fetch or similar handler. Outside of such a handler, attempts to use `get` will throw an error. The request method accepts all options from [http.request](https://developers.cloudflare.com/workers/runtime-apis/nodejs/http#request) with some differences in default values: * `protocol`: default `https:` * `port`: default `443` * `agent`: default `https.globalAgent` ```js import { request } from "node:https"; import { strictEqual, ok } from "node:assert"; export default { async fetch() { const { promise, resolve, reject } = Promise.withResolvers(); const req = request( "https://developers.cloudflare.com/robots.txt", { method: "GET", }, (res) => { strictEqual(res.statusCode, 200); let data = ""; res.setEncoding("utf8"); res.on("data", (chunk) => { data += chunk; }); res.once("error", reject); res.on("end", () => { ok(data.includes("User-agent")); resolve(new Response(data)); }); }, ); req.end(); return promise; }, }; ``` The following additional options are not supported: `ca`, `cert`, `ciphers`, `clientCertEngine` (deprecated), `crl`, `dhparam`, `ecdhCurve`, `honorCipherOrder`, `key`, `passphrase`, `pfx`, `rejectUnauthorized`, `secureOptions`, `secureProtocol`, `servername`, `sessionIdContext`, `highWaterMark`. ## createServer An implementation of the Node.js [https.createServer ↗](https://nodejs.org/docs/latest/api/https.html#httpscreateserveroptions-requestlistener) method. The `createServer` method creates an HTTPS server instance that can handle incoming secure requests. It's a convenience function that creates a new `Server` instance and optionally sets up a request listener callback. ```js import { createServer } from "node:https"; import { httpServerHandler } from "cloudflare:node"; const server = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("Hello from Node.js HTTPS server!"); }); server.listen(8080); export default httpServerHandler({ port: 8080 }); ``` The `httpServerHandler` function integrates Node.js HTTPS servers with the Cloudflare Workers request model. When a request arrives at your Worker, the handler automatically routes it to your Node.js server running on the specified port. This bridge allows you to use familiar Node.js server patterns while benefiting from the Workers runtime environment, including automatic scaling, edge deployment, and integration with other Cloudflare services. Note Failing to call `close()` on an HTTPS server may result in the server being leaked. To prevent this, call `close()` when you're done with the server, or use explicit resource management: ```js import { createServer } from "node:https"; await using server = createServer((req, res) => { res.end("Hello World"); }); // Server will be automatically closed when it goes out of scope ``` ## Agent An implementation of the Node.js [https.Agent ↗](https://nodejs.org/docs/latest/api/https.html#class-httpsagent) class. An [Agent ↗](https://nodejs.org/docs/latest/api/https.html#class-httpsagent) manages HTTPS connection reuse by maintaining request queues per host/port. In the Workers environment, however, such low-level management of the network connection, ports, etc, is not relevant because it is handled by the Cloudflare infrastructure instead. Accordingly, the implementation of `Agent` in Workers is a stub implementation that does not support connection pooling or keep-alive. ## Server An implementation of the Node.js [https.Server ↗](https://nodejs.org/docs/latest/api/https.html#class-httpsserver) class. In Node.js, the `https.Server` class represents an HTTPS server and provides methods for handling incoming secure requests. In Workers, handling of secure requests is provided by the Cloudflare infrastructure so there really is not much difference between using `https.Server` or `http.Server`. The workers runtime provides an implementation for completeness but most workers should probably just use [http.Server](https://developers.cloudflare.com/workers/runtime-apis/nodejs/http#server). ```js import { Server } from "node:https"; import { httpServerHandler } from "cloudflare:node"; const server = new Server((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ message: "Hello from HTTPS Server!" })); }); server.listen(8080); export default httpServerHandler({ port: 8080 }); ``` The following differences exist between the Workers implementation and Node.js: * Connection management methods such as `closeAllConnections()` and `closeIdleConnections()` are not implemented due to the nature of the Workers environment. * Only `listen()` variants with a port number or no parameters are supported: `listen()`, `listen(0, callback)`, `listen(callback)`, etc. * The following server options are not supported: `maxHeaderSize`, `insecureHTTPParser`, `keepAliveTimeout`, `connectionsCheckingInterval` * TLS/SSL-specific options such as `ca`, `cert`, `key`, `pfx`, `rejectUnauthorized`, `secureProtocol` are not supported in the Workers environment. If you need to use mTLS, use the [mTLS binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/mtls/). ## Other differences between Node.js and Workers implementation of `node:https` Because the Workers implementation of `node:https` is a wrapper around the global `fetch` API, there are some differences in behavior compared to Node.js: * `Connection` headers are not used. Workers will manage connections automatically. * `Content-Length` headers will be handled the same way as in the `fetch` API. If a body is provided, the header will be set automatically and manually set values will be ignored. * `Expect: 100-continue` headers are not supported. * Trailing headers are not supported. * The `'continue'` event is not supported. * The `'information'` event is not supported. * The `'socket'` event is not supported. * The `'upgrade'` event is not supported. * Gaining direct access to the underlying `socket` is not supported. * Configuring TLS-specific options like `ca`, `cert`, `key`, `rejectUnauthorized`, etc, is not supported. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/https/#page","headline":"https · Cloudflare Workers docs","description":"Use the Node.js https module in Cloudflare Workers for TLS-encrypted HTTP client and server functionality.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/https/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js net module in Cloudflare Workers to create TCP socket connections to external servers. title: net image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # net Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/net/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). You can use [node:net ↗](https://nodejs.org/api/net.html) to create a direct connection to servers via a TCP sockets with [net.Socket ↗](https://nodejs.org/api/net.html#class-netsocket). These functions use [connect](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#connect) functionality from the built-in `cloudflare:sockets` module. ```js import net from "node:net"; const exampleIP = "127.0.0.1"; export default { async fetch(req) { const socket = new net.Socket(); socket.connect(4000, exampleIP, function () { console.log("Connected"); }); socket.write("Hello, Server!"); socket.end(); return new Response("Wrote to server", { status: 200 }); }, }; ``` ```ts import net from "node:net"; const exampleIP = "127.0.0.1"; export default { async fetch(req): Promise { const socket = new net.Socket(); socket.connect(4000, exampleIP, function () { console.log("Connected"); }); socket.write("Hello, Server!"); socket.end(); return new Response("Wrote to server", { status: 200 }); }, } satisfies ExportedHandler; ``` Additionally, other APIs such as [net.BlockList ↗](https://nodejs.org/api/net.html#class-netblocklist)and [net.SocketAddress ↗](https://nodejs.org/api/net.html#class-netsocketaddress) are available. Note that the [net.Server ↗](https://nodejs.org/api/net.html#class-netserver) class is not supported by Workers. The full `node:net` API is documented in the [Node.js documentation for node:net ↗](https://nodejs.org/api/net.html). ```plaintext ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/net/#page","headline":"net · Cloudflare Workers docs","description":"Use the Node.js net module in Cloudflare Workers to create TCP socket connections to external servers.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/net/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js path module in Cloudflare Workers for file and directory path manipulation utilities. title: path image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # path Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/path/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [node:path ↗](https://nodejs.org/api/path.html) module provides utilities for working with file and directory paths. The `node:path` module can be accessed using: ```js import path from "node:path"; path.join("/foo", "bar", "baz/asdf", "quux", ".."); // Returns: '/foo/bar/baz/asdf' ``` Refer to the [Node.js documentation for path ↗](https://nodejs.org/api/path.html) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/path/#page","headline":"path · Cloudflare Workers docs","description":"Use the Node.js path module in Cloudflare Workers for file and directory path manipulation utilities.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/path/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js process module in Cloudflare Workers for environment variables, event handling, and runtime information. title: process image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # process Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/process/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [process ↗](https://nodejs.org/docs/latest/api/process.html) module in Node.js provides a number of useful APIs related to the current process. Initially Workers only supported `nextTick`, `env`, `exit`, `getBuiltinModule`, `platform` and `features` on process, which was then updated with the [enable\_nodejs\_process\_v2](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-process-v2-implementation) flag to include most Node.js process features. Refer to the [Node.js documentation for process ↗](https://nodejs.org/docs/latest/api/process.html) for more information. Workers-specific implementation details apply when adapting Node.js process support for a serverless environment, which are described in more detail below. ## `process.env` In the Node.js implementation of `process.env`, the `env` object is a copy of the environment variables at the time the process was started. In the Workers implementation, there is no process-level environment, so by default `env` is an empty object. You can still set and get values from `env`, and those will be globally persistent for all Workers running in the same isolate and context (for example, the same Workers entry point). Note If you use [Wrangler](https://developers.cloudflare.com/workers/wrangler/) or the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/), `process.env.NODE_ENV` is statically replaced at build time and is not a runtime value. Refer to [Bundling](https://developers.cloudflare.com/workers/wrangler/bundling/#node%5Fenv) for more information. When [Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/) is enabled and the [nodejs\_compat\_populate\_process\_env](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-auto-populating-processenv) compatibility flag is set (enabled by default for compatibility dates on or after 2025-04-01), `process.env` will contain any [environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables/), [secrets](https://developers.cloudflare.com/workers/configuration/secrets/), or [version metadata](https://developers.cloudflare.com/workers/runtime-apis/bindings/version-metadata/) metadata that has been configured on your Worker. Setting any value on `process.env` will coerce that value into a string. ### Alternative: Import `env` from `cloudflare:workers` Instead of using `process.env`, you can [import env from cloudflare:workers](https://developers.cloudflare.com/workers/runtime-apis/bindings/#importing-env-as-a-global) to access environment variables and all other bindings from anywhere in your code. ```js import * as process from "node:process"; export default { fetch(req, env) { // Set process.env.FOO to the value of env.FOO if process.env.FOO is not already set // and env.FOO is a string. process.env.FOO ??= (() => { if (typeof env.FOO === "string") { return env.FOO; } })(); }, }; ``` It is strongly recommended that you _do not_ replace the entire `process.env` object with the cloudflare `env` object. Doing so will cause you to lose any environment variables that were set previously and will cause unexpected behavior for other Workers running in the same isolate. Specifically, it would cause inconsistency with the `process.env` object when accessed via named imports. ```js import * as process from "node:process"; import { env } from "node:process"; process.env === env; // true! they are the same object process.env = {}; // replace the object! Do not do this! process.env === env; // false! they are no longer the same object // From this point forward, any changes to process.env will not be reflected in env, // and vice versa! ``` ## `process.nextTick()` The Workers implementation of `process.nextTick()` is a wrapper for the standard Web Platform API [queueMicrotask() ↗](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/queueMicrotask). ```js import { env, nextTick } from "node:process"; env["FOO"] = "bar"; console.log(env["FOO"]); // Prints: bar nextTick(() => { console.log("next tick"); }); ``` ## Stdio [process.stdout ↗](https://nodejs.org/docs/latest/api/process.html#processstdout), [process.stderr ↗](https://nodejs.org/docs/latest/api/process.html#processstderr) and [process.stdin ↗](https://nodejs.org/docs/latest/api/process.html#processstdin) are supported as streams. `stdin` is treated as an empty readable stream. `stdout` and `stderr` are non-TTY writable streams, which output to normal logging output only with `stdout: ` and `stderr: ` prefixing. The line buffer works by storing writes to stdout or stderr until either a newline character `\n` is encountered or until the next microtask, when the log is then flushed to the output. This ensures compatibility with inspector and structured logging outputs. ## Current Working Directory [process.cwd() ↗](https://nodejs.org/docs/latest/api/process.html#processcwd) is the _current working directory_, used as the default path for all filesystem operations, and is initialized to `/bundle`. [process.chdir() ↗](https://nodejs.org/docs/latest/api/process.html#processchdirdirectory) allows modifying the `cwd` and is respected by FS operations when using `enable_nodejs_fs_module`. ## Hrtime While [process.hrtime ↗](https://nodejs.org/docs/latest/api/process.html#processhrtimetime) high-resolution timer is available, it provides an inaccurate timer for compatibility only. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/process/#page","headline":"process · Cloudflare Workers docs","description":"Use the Node.js process module in Cloudflare Workers for environment variables, event handling, and runtime information.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/process/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js streams API in Cloudflare Workers for readable, writable, and transform stream operations. title: Streams image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Streams Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [Node.js streams API ↗](https://nodejs.org/api/stream.html) is the original API for working with streaming data in JavaScript, predating the [WHATWG ReadableStream standard ↗](https://streams.spec.whatwg.org/). A stream is an abstract interface for working with streaming data in Node.js. Streams can be readable, writable, or both. All streams are instances of [EventEmitter](https://developers.cloudflare.com/workers/runtime-apis/nodejs/eventemitter/). Where possible, you should use the [WHATWG standard "Web Streams" API ↗](https://streams.spec.whatwg.org/), which is [supported in Workers ↗](https://streams.spec.whatwg.org/). ```js import { Readable, Transform } from "node:stream"; import { text } from "node:stream/consumers"; import { pipeline } from "node:stream/promises"; // A Node.js-style Transform that converts data to uppercase // and appends a newline to the end of the output. class MyTransform extends Transform { constructor() { super({ encoding: "utf8" }); } _transform(chunk, _, cb) { this.push(chunk.toString().toUpperCase()); cb(); } _flush(cb) { this.push("\n"); cb(); } } export default { async fetch() { const chunks = [ "hello ", "from ", "the ", "wonderful ", "world ", "of ", "node.js ", "streams!", ]; function nextChunk(readable) { readable.push(chunks.shift()); if (chunks.length === 0) readable.push(null); else queueMicrotask(() => nextChunk(readable)); } // A Node.js-style Readable that emits chunks from the // array... const readable = new Readable({ encoding: "utf8", read() { nextChunk(readable); }, }); const transform = new MyTransform(); await pipeline(readable, transform); return new Response(await text(transform)); }, }; ``` Refer to the [Node.js documentation for stream ↗](https://nodejs.org/api/stream.html) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/#page","headline":"Streams - Node.js APIs · Cloudflare Workers docs","description":"Use the Node.js streams API in Cloudflare Workers for readable, writable, and transform stream operations.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/streams/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js string_decoder module in Cloudflare Workers for decoding buffer objects into strings. title: StringDecoder image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # StringDecoder Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/string-decoder/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The [node:string\_decoder ↗](https://nodejs.org/api/string%5Fdecoder.html) is a legacy utility module that predates the WHATWG standard [TextEncoder](https://developers.cloudflare.com/workers/runtime-apis/encoding/#textencoder) and [TextDecoder](https://developers.cloudflare.com/workers/runtime-apis/encoding/#textdecoder) API. In most cases, you should use `TextEncoder` and `TextDecoder` instead. `StringDecoder` is available in the Workers runtime primarily for compatibility with existing npm packages that rely on it. `StringDecoder` can be accessed using: ```js const { StringDecoder } = require("node:string_decoder"); const decoder = new StringDecoder("utf8"); const cent = Buffer.from([0xc2, 0xa2]); console.log(decoder.write(cent)); const euro = Buffer.from([0xe2, 0x82, 0xac]); console.log(decoder.write(euro)); ``` Refer to the [Node.js documentation for string\_decoder ↗](https://nodejs.org/dist/latest-v20.x/docs/api/string%5Fdecoder.html) for more information. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/string-decoder/#page","headline":"StringDecoder · Cloudflare Workers docs","description":"Use the Node.js string\\_decoder module in Cloudflare Workers for decoding buffer objects into strings.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/string-decoder/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js test module MockTracker API in Cloudflare Workers for tracking and managing mock objects. title: test image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # test Last updated Jun 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/test/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## `MockTracker` The `MockTracker` API in Node.js provides a means of tracking and managing mock objects in a test environment. ```js import { mock } from 'node:test'; const fn = mock.fn(); fn(1,2,3); // does nothing... but console.log(fn.mock.callCount()); // Records how many times it was called console.log(fn.mock.calls[0].arguments); // Records the arguments that were passed each call ``` The full `MockTracker` API is documented in the [Node.js documentation for MockTracker ↗](https://nodejs.org/docs/latest/api/test.html#class-mocktracker). The Workers implementation of `MockTracker` currently does not include an implementation of the [Node.js mock timers API ↗](https://nodejs.org/docs/latest/api/test.html#class-mocktimers). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/test/#page","headline":"test · Cloudflare Workers docs","description":"Use the Node.js test module MockTracker API in Cloudflare Workers for tracking and managing mock objects.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/test/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js timers API in Cloudflare Workers to schedule functions with setTimeout, setInterval, and setImmediate. title: timers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # timers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/timers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Use [node:timers ↗](https://nodejs.org/api/timers.html) APIs to schedule functions to be executed later. This includes [setTimeout ↗](https://nodejs.org/api/timers.html#settimeoutcallback-delay-args) for calling a function after a delay, [setInterval ↗](https://nodejs.org/api/timers.html#clearintervaltimeout) for calling a function repeatedly, and [setImmediate ↗](https://nodejs.org/api/timers.html#setimmediatecallback-args) for calling a function in the next iteration of the event loop. ```js import timers from "node:timers"; export default { async fetch() { console.log("first"); const { promise: promise1, resolve: resolve1 } = Promise.withResolvers(); const { promise: promise2, resolve: resolve2 } = Promise.withResolvers(); timers.setTimeout(() => { console.log("last"); resolve1(); }, 10); timers.setTimeout(() => { console.log("next"); resolve2(); }); await Promise.all([promise1, promise2]); return new Response("ok"); }, }; ``` ```ts import timers from "node:timers"; export default { async fetch(): Promise { console.log("first"); const { promise: promise1, resolve: resolve1 } = Promise.withResolvers(); const { promise: promise2, resolve: resolve2 } = Promise.withResolvers(); timers.setTimeout(() => { console.log("last"); resolve1(); }, 10); timers.setTimeout(() => { console.log("next"); resolve2(); }); await Promise.all([promise1, promise2]); return new Response("ok"); } } satisfies ExportedHandler; ``` Note Due to [security-based restrictions on timers](https://developers.cloudflare.com/workers/reference/security-model/#step-1-disallow-timers-and-multi-threading) in Workers, timers are limited to returning the time of the last I/O. This means that while setTimeout, setInterval, and setImmediate will defer your function execution until after other events have run, they will not delay them for the full time specified. Note When called from a global level (on [globalThis ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/globalThis)), functions such as `clearTimeout` and `setTimeout` will respect web standards rather than Node.js-specific functionality. For complete Node.js compatibility, you must call functions from the `node:timers` module. The full `node:timers` API is documented in the [Node.js documentation for node:timers ↗](https://nodejs.org/api/timers.html). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/timers/#page","headline":"timers · Cloudflare Workers docs","description":"Use the Node.js timers API in Cloudflare Workers to schedule functions with setTimeout, setInterval, and setImmediate.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/timers/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js tls module in Cloudflare Workers to create secure TLS connections to external services. title: tls image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # tls Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/tls/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). You can use [node:tls ↗](https://nodejs.org/api/tls.html) to create secure connections to external services using [TLS ↗](https://developer.mozilla.org/en-US/docs/Web/Security/Transport%5FLayer%5FSecurity) (Transport Layer Security). ```js import { connect } from "node:tls"; // ... in a request handler ... const connectionOptions = { key: env.KEY, cert: env.CERT }; const socket = connect(url, connectionOptions, () => { if (socket.authorized) { console.log("Connection authorized"); } }); socket.on("data", (data) => { console.log(data); }); socket.on("end", () => { console.log("server ends connection"); }); ``` The following APIs are available: * [connect ↗](https://nodejs.org/api/tls.html#tlsconnectoptions-callback) * [TLSSocket ↗](https://nodejs.org/api/tls.html#class-tlstlssocket) * [checkServerIdentity ↗](https://nodejs.org/api/tls.html#tlscheckserveridentityhostname-cert) * [createSecureContext ↗](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions) All other APIs, including [tls.Server ↗](https://nodejs.org/api/tls.html#class-tlsserver) and [tls.createServer ↗](https://nodejs.org/api/tls.html#tlscreateserveroptions-secureconnectionlistener), are not supported and will throw a `Not implemented` error when called. The full `node:tls` API is documented in the [Node.js documentation for node:tls ↗](https://nodejs.org/api/tls.html). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/tls/#page","headline":"tls · Cloudflare Workers docs","description":"Use the Node.js tls module in Cloudflare Workers to create secure TLS connections to external services.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/tls/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js url module in Workers for domain-to-ASCII and domain-to-Unicode conversions. title: url image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # url Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/url/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## domainToASCII Returns the Punycode ASCII serialization of the domain. If domain is an invalid domain, the empty string is returned. ```js import { domainToASCII } from "node:url"; console.log(domainToASCII("español.com")); // Prints xn--espaol-zwa.com console.log(domainToASCII("中文.com")); // Prints xn--fiq228c.com console.log(domainToASCII("xn--iñvalid.com")); // Prints an empty string ``` ## domainToUnicode Returns the Unicode serialization of the domain. If domain is an invalid domain, the empty string is returned. It performs the inverse operation to `domainToASCII()`. ```js import { domainToUnicode } from "node:url"; console.log(domainToUnicode("xn--espaol-zwa.com")); // Prints español.com console.log(domainToUnicode("xn--fiq228c.com")); // Prints 中文.com console.log(domainToUnicode("xn--iñvalid.com")); // Prints an empty string ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/url/#page","headline":"url · Cloudflare Workers docs","description":"Use the Node.js url module in Workers for domain-to-ASCII and domain-to-Unicode conversions.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/url/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js util module in Workers for promisify, callbackify, types, and MIMEType support. title: util image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # util Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/util/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). ## promisify/callbackify The `promisify` and `callbackify` APIs in Node.js provide a means of bridging between a Promise-based programming model and a callback-based model. The `promisify` method allows taking a Node.js-style callback function and converting it into a Promise-returning async function: ```js import { promisify } from "node:util"; function foo(args, callback) { try { callback(null, 1); } catch (err) { // Errors are emitted to the callback via the first argument. callback(err); } } const promisifiedFoo = promisify(foo); await promisifiedFoo(args); ``` Similarly to `promisify`, `callbackify` converts a Promise-returning async function into a Node.js-style callback function: ```js import { callbackify } from 'node:util'; async function foo(args) { throw new Error('boom'); } const callbackifiedFoo = callbackify(foo); callbackifiedFoo(args, (err, value) => { if (err) throw err; }); ``` `callbackify` and `promisify` make it easy to handle all of the challenges that come with bridging between callbacks and promises. Refer to the [Node.js documentation for callbackify ↗](https://nodejs.org/dist/latest-v19.x/docs/api/util.html#utilcallbackifyoriginal) and [Node.js documentation for promisify ↗](https://nodejs.org/dist/latest-v19.x/docs/api/util.html#utilpromisifyoriginal) for more information. ## util.types The `util.types` API provides a reliable and efficient way of checking that values are instances of various built-in types. ```js import { types } from "node:util"; types.isAnyArrayBuffer(new ArrayBuffer()); // Returns true types.isAnyArrayBuffer(new SharedArrayBuffer()); // Returns true types.isArrayBufferView(new Int8Array()); // true types.isArrayBufferView(Buffer.from("hello world")); // true types.isArrayBufferView(new DataView(new ArrayBuffer(16))); // true types.isArrayBufferView(new ArrayBuffer()); // false function foo() { types.isArgumentsObject(arguments); // Returns true } types.isAsyncFunction(function foo() {}); // Returns false types.isAsyncFunction(async function foo() {}); // Returns true // .. and so on ``` Caution The Workers implementation currently does not provide implementations of the `util.types.isExternal()`, `util.types.isProxy()`, `util.types.isKeyObject()`, or `util.type.isWebAssemblyCompiledModule()` APIs. For more about `util.types`, refer to the [Node.js documentation for util.types ↗](https://nodejs.org/dist/latest-v19.x/docs/api/util.html#utiltypes). ## util.MIMEType `util.MIMEType` provides convenience methods that allow you to more easily work with and manipulate [MIME types ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics%5Fof%5FHTTP/MIME%5Ftypes). For example: ```js import { MIMEType } from "node:util"; const myMIME = new MIMEType("text/javascript;key=value"); console.log(myMIME.type); // Prints: text console.log(myMIME.essence); // Prints: text/javascript console.log(myMIME.subtype); // Prints: javascript console.log(String(myMIME)); // Prints: application/javascript;key=value ``` For more about `util.MIMEType`, refer to the [Node.js documentation for util.MIMEType ↗](https://nodejs.org/api/util.html#class-utilmimetype). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/util/#page","headline":"util · Cloudflare Workers docs","description":"Use the Node.js util module in Workers for promisify, callbackify, types, and MIMEType support.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/util/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the Node.js zlib module in Workers for Gzip, Deflate, and Brotli compression. title: zlib image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # zlib Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/nodejs/zlib/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note For compatibility dates of `2026-08-04` or later, Workers enables both `nodejs_compat` and `nodejs_compat_v2` by default. These flags are not used for these compatibility dates. Existing projects do not need to remove them when updating their compatibility date. For earlier dates, add `nodejs_compat` to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/) to opt in. For instructions to turn off Node.js compatibility, refer to the [Node.js compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). The node:zlib module provides compression functionality implemented using Gzip, Deflate/Inflate, and Brotli. To access it: ```js import zlib from "node:zlib"; ``` The full `node:zlib` API is documented in the [Node.js documentation for node:zlib ↗](https://nodejs.org/api/zlib.html). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/zlib/#page","headline":"zlib · Cloudflare Workers docs","description":"Use the Node.js zlib module in Workers for Gzip, Deflate, and Brotli compression.","url":"https://developers.cloudflare.com/workers/runtime-apis/nodejs/zlib/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Measure timing, performance, and timing of subrequests and other operations. title: Performance and timers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Performance and timers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/performance/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The Workers runtime supports a subset of the [Performance API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Performance), used to measure timing and performance, as well as timing of subrequests and other operations. ### `performance.now()` The [performance.now() method ↗](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now) returns timestamp in milliseconds, representing the time elapsed since `performance.timeOrigin`. When Workers are deployed to Cloudflare, as a security measure to [mitigate against Spectre attacks](https://developers.cloudflare.com/workers/reference/security-model/#step-1-disallow-timers-and-multi-threading), APIs that return timers, including [performance.now() ↗](https://developer.mozilla.org/en-US/docs/Web/API/Performance/now) and [Date.now() ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Date/now), only advance or increment after I/O occurs. Consider the following examples: ```typescript const start = performance.now(); for (let i = 0; i < 1e6; i++) { // do expensive work } const end = performance.now(); const timing = end - start; // 0 ``` ```typescript const start = performance.now(); const response = await fetch("https://developers.cloudflare.com/"); const end = performance.now(); const timing = end - start; // duration of the subrequest to developers.cloudflare.com ``` By wrapping a subrequest in calls to `performance.now()` or `Date.now()` APIs, you can measure the timing of a subrequest, fetching a key from KV, an object from R2, or any other form of I/O in your Worker. In local development, however, timers will increment regardless of whether I/O happens or not. This means that if you need to measure timing of a piece of code that is CPU intensive, that does not involve I/O, you can run your Worker locally, via [Wrangler](https://developers.cloudflare.com/workers/wrangler/), which uses the open-source Workers runtime, [workerd ↗](https://github.com/cloudflare/workerd) — the same runtime that your Worker runs in when deployed to Cloudflare. ### `performance.timeOrigin` The [performance.timeOrigin ↗](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin) API is a read-only property that returns a baseline timestamp to base other measurements off of. In the Workers runtime, the `timeOrigin` property returns 0. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/performance/#page","headline":"Performance and timers · Cloudflare Workers docs","description":"Measure timing, performance, and timing of subrequests and other operations.","url":"https://developers.cloudflare.com/workers/runtime-apis/performance/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Interface that represents an HTTP request. title: Request image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Request Last updated Jul 2, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/request/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The [Request ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) interface represents an HTTP request and is part of the [Fetch API](https://developers.cloudflare.com/workers/runtime-apis/fetch/). ## Background The most common way you will encounter a `Request` object is as a property of an incoming request: ```js export default { async fetch(request, env, ctx) { return new Response('Hello World!'); }, }; ``` You may also want to construct a `Request` yourself when you need to modify a request object, because the incoming `request` parameter that you receive from the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) is immutable. ```js export default { async fetch(request, env, ctx) { const url = "https://example.com"; const modifiedRequest = new Request(url, request); // ... }, }; ``` The [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) invokes the `Request` constructor. The [RequestInit](#options) and [RequestInitCfProperties](#the-cf-property-requestinitcfproperties) types defined below also describe the valid parameters that can be passed to the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). --- ## Constructor ```js let request = new Request(input, options) ``` ### Parameters * `input` string | Request * Either a string that contains a URL, or an existing `Request` object. * `options` options optional * Optional options object that contains settings to apply to the `Request`. #### `options` An object containing properties that you want to apply to the request. * `cache` `undefined | 'no-store' | 'no-cache'` optional * Standard HTTP `cache` header. Only `cache: 'no-store'` and `cache: 'no-cache'` are supported. Any other cache header will result in a `TypeError` with the message `Unsupported cache mode: `. * `cf` RequestInitCfProperties optional * Cloudflare-specific properties that can be set on the `Request` that control how Cloudflare’s global network handles the request. * `method` `string`optional * The HTTP request method. The default is `GET`. In Workers, all [HTTP request methods ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods) are supported, except for [CONNECT ↗](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/CONNECT). * `headers` Headers optional * A [Headers object ↗](https://developer.mozilla.org/en-US/docs/Web/API/Headers). * `body` string | ReadableStream | FormData | URLSearchParams optional * The request body, if any. * Note that a request using the GET or HEAD method cannot have a body. * `redirect` `string`optional * The redirect mode to use: `follow`, `error`, or `manual`. The default for a new `Request` object is `follow`. Note, however, that the incoming `Request` property of a `FetchEvent` will have redirect mode `manual`. * `signal` AbortSignal optional * If provided, the request can be canceled by triggering an abort on the corresponding `AbortController`. #### The `cf` property (`RequestInitCfProperties`) An object containing Cloudflare-specific properties that can be set on the `Request` object. For example: ```js // Disable ScrapeShield for this request. fetch(event.request, { cf: { scrapeShield: false } }) ``` Invalid or incorrectly-named keys in the `cf` object will be silently ignored. Consider using TypeScript and generating types by running [wrangler types](https://developers.cloudflare.com/workers/languages/typescript/#generate-types) to ensure proper use of the `cf` object. * `apps` `boolean`optional * Whether [Cloudflare Apps ↗](https://www.cloudflare.com/apps/) should be enabled for this request. Defaults to `true`. * `cacheEverything` `boolean`optional * Treats all content as static and caches all [file types](https://developers.cloudflare.com/cache/concepts/default-cache-behavior#default-cached-file-extensions) beyond the Cloudflare default cached content. Respects cache headers from the origin web server. This is equivalent to setting the Page Rule [**Cache Level** (to **Cache Everything**)](https://developers.cloudflare.com/rules/page-rules/reference/settings/). Defaults to `false`. This option applies to `GET` and `HEAD` request methods only. * `cacheKey` `string`optional * A request’s cache key is what determines if two requests are the same for caching purposes. If a request has the same cache key as some previous request, then Cloudflare can serve the same cached response for both. * `cacheTags` Array optional * This option appends additional [**Cache-Tag**](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/) headers to the response from the origin server. This allows for purges of cached content based on tags provided by the Worker, without modifications to the origin server. This is performed using the [**Purge by Tag**](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#purge-using-cache-tags) feature. * `cacheTtl` `number`optional * This option forces Cloudflare to cache the response for this request, regardless of what headers are seen on the response. This is equivalent to setting two Page Rules: [**Edge Cache TTL**](https://developers.cloudflare.com/cache/how-to/edge-browser-cache-ttl/) and [**Cache Level** (to **Cache Everything**)](https://developers.cloudflare.com/rules/page-rules/reference/settings/). The value must be zero or a positive number. A value of `0` indicates that the cache asset expires immediately. This option applies to `GET` and `HEAD` request methods only. * `cacheTtlByStatus` `{ [key: string]: number }` optional * This option is a version of the `cacheTtl` feature which chooses a TTL based on the response’s status code. If the response to this request has a status code that matches, Cloudflare will cache for the instructed time and override cache instructives sent by the origin. For example: `{ "200-299": 86400, "404": 1, "500-599": 0 }`. The value can be any integer, including zero and negative integers. A value of `0` indicates that the cache asset expires immediately. Any negative value instructs Cloudflare not to cache at all. This option applies to `GET` and `HEAD` request methods only. * `vary` `RequestInitCfPropertiesVary`optional * Controls how Cloudflare caches origin responses with a `Vary` header for a single `fetch()` request. If both `cf.vary` and [Cache Rules Vary](https://developers.cloudflare.com/cache/how-to/cache-rules/settings/#vary) apply, `cf.vary` takes precedence for this subrequest. * `image` Object | null optional * Enables [Image Resizing](https://developers.cloudflare.com/images/optimization/transformations/overview/) for this request. The possible values are described in [Transform images via Workers](https://developers.cloudflare.com/images/optimization/transformations/transform-via-workers/) documentation. * `polish` `string`optional * Sets [Polish ↗](https://blog.cloudflare.com/introducing-polish-automatic-image-optimizati/) mode. The possible values are `lossy`, `lossless` or `off`. * `resolveOverride` `string`optional * Directs the request to an alternate origin server by overriding the DNS lookup. The value of `resolveOverride` specifies an alternate hostname which will be used when determining the origin IP address, instead of using the hostname specified in the URL. The `Host` header of the request will still match what is in the URL. Thus, `resolveOverride` allows a request to be sent to a different server than the URL / `Host` header specifies. However, `resolveOverride` will only take effect if both the URL host and the host specified by `resolveOverride` are within your zone. If either specifies a host from a different zone / domain, then the option will be ignored for security reasons. If you need to direct a request to a host outside your zone (while keeping the `Host` header pointing within your zone), first create a CNAME record within your zone pointing to the outside host, and then set `resolveOverride` to point at the CNAME record. Note that, for security reasons, it is not possible to set the `Host` header to specify a host outside of your zone unless the request is actually being sent to that host. * `scrapeShield` `boolean`optional * Whether [ScrapeShield ↗](https://blog.cloudflare.com/introducing-scrapeshield-discover-defend-dete/) should be enabled for this request, if otherwise configured for this zone. Defaults to `true`. * `webp` `boolean`optional * Enables or disables [WebP ↗](https://blog.cloudflare.com/a-very-webp-new-year-from-cloudflare/) image format in [Polish](https://developers.cloudflare.com/images/polish/). #### The `cf.vary` property The `cf.vary` object controls how Cloudflare handles request headers named by the origin `Vary` response header for a single `fetch()` request. It uses the same `default` and `headers` shape as [Cache Rules Vary](https://developers.cloudflare.com/cache/how-to/cache-rules/settings/#vary), and the same actions and normalization behavior as [Vary](https://developers.cloudflare.com/cache/concepts/vary/). If you omit `cf.vary`, Cloudflare uses other Vary behavior for the zone, including Cache Rules Vary if configured. The origin response must include a `Vary` header for this setting to affect the cache key. A response containing `Vary: *` always bypasses cache. The `cf.vary` object supports these keys: | Key | Required | Description | | ------- | -------- | ---------------------------------------------------------------------------------------------- | | default | Yes | Configuration for any header name in the origin Vary response that is not included in headers. | | headers | No | A map of lowercase request header names to configuration objects. | If the `vary` object is present, `default` is required. An empty `vary` object is invalid. Invalid `cf.vary` configurations are ignored for that request. Each header configuration object, and the `default` object, must include an `action` key set to one of `normalize`, `passthrough`, or `bypass`. For guidance, refer to [Actions](https://developers.cloudflare.com/cache/concepts/vary/#actions). Additional parameters can be specified for certain header names: | Header | Additional key | Description | | --------------- | -------------- | ----------------------------------------------------------------------------------------------------------- | | accept | media\_types | MIME types to keep when normalizing the Accept header. Maximum 10 items and 255 characters per item. | | accept-language | languages | Languages to keep when normalizing the Accept-Language header. Maximum 20 items and 64 characters per item. | The `default` object and `headers` entries other than `accept` and `accept-language` support only `action`. For most deployments, set `default.action` to `bypass`, add `headers` entries for expected origin `Vary` headers, and use `normalize` for `accept` and `accept-language` unless your origin requires raw header values. The following limits and validation rules apply: * Header names in `headers` must be lowercase. * Header names can contain lowercase letters, numbers, underscores, and hyphens. * Header names cannot exceed 128 characters. * Header names beginning with `cf-` or `cf_` are not allowed. * Certain hop-by-hop, cache-control, or proxy-control headers are not allowed. Examples include `connection`, `content-length`, `cache-control`, `host`, `range`, `origin`, and `x-forwarded-for`. * `headers` can contain up to 50 entries. * `accept.media_types` can contain up to 10 entries. * `accept-language.languages` can contain up to 20 entries. * Values in `media_types` and `languages` must be non-empty printable ASCII strings. The following request init fragment normalizes `Accept` and `Accept-Language`, and bypasses cache for any other header in the origin `Vary` response: ```json { "cf": { "vary": { "default": { "action": "bypass" }, "headers": { "accept": { "action": "normalize", "media_types": ["text/html", "application/json"] }, "accept-language": { "action": "normalize", "languages": ["en", "fr", "de"] } } } } } ``` --- ## Properties All properties of an incoming `Request` object (the request you receive from the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/)) are read-only. To modify the properties of an incoming request, create a new `Request` object and pass the options to modify to its [constructor](#constructor). * `body` ReadableStream read-only * Stream of the body contents. * `bodyUsed` Boolean read-only * Declares whether the body has been used in a response yet. * `cf` IncomingRequestCfProperties read-only * An object containing properties about the incoming request provided by Cloudflare’s global network. * This property is read-only (unless created from an existing `Request`). To modify its values, pass in the new values on the [cf key of the init options argument](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties) when creating a new `Request` object. * `headers` Headers read-only * A [Headers object ↗](https://developer.mozilla.org/en-US/docs/Web/API/Headers). * Compared to browsers, Cloudflare Workers imposes very few restrictions on what headers you are allowed to send. For example, a browser will not allow you to set the `Cookie` header, since the browser is responsible for handling cookies itself. Workers, however, has no special understanding of cookies, and treats the `Cookie` header like any other header. Caution If the response is a redirect and the redirect mode is set to `follow` (see below), then all headers will be forwarded to the redirect destination, even if the destination is a different hostname or domain. This includes sensitive headers like `Cookie`, `Authorization`, or any application-specific headers. If this is not the behavior you want, you should set redirect mode to `manual` and implement your own redirect policy. Note that redirect mode defaults to `manual` for requests that originated from the Worker's client, so this warning only applies to `fetch()`es made by a Worker that are not proxying the original request. * `method` string read-only * Contains the request’s method, for example, `GET`, `POST`, etc. * `redirect` string read-only * The redirect mode to use: `follow`, `error`, or `manual`. The `fetch` method will automatically follow redirects if the redirect mode is set to `follow`. If set to `manual`, the `3xx` redirect response will be returned to the caller as-is. The default for a new `Request` object is `follow`. Note, however, that the incoming `Request` property of a `FetchEvent` will have redirect mode `manual`. * `signal` AbortSignal read-only * The `AbortSignal` corresponding to this request. If you use the [enable\_request\_signal](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#enable-requestsignal-for-incoming-requests) compatibility flag, you can attach an event listener to the signal. This allows you to perform cleanup tasks or write to logs before your Worker's invocation ends. For example, if you run the Worker below, and then abort the request from the client, a log will be written: ```js export default { async fetch(request, env, ctx) { // This sets up an event listener that will be called if the client disconnects from your // worker. request.signal.addEventListener("abort", () => { console.log("The request was aborted!"); }); const { readable, writable } = new IdentityTransformStream(); sendPing(writable); return new Response(readable, { headers: { "Content-Type": "text/plain" }, }); }, }; async function sendPing(writable) { const writer = writable.getWriter(); const enc = new TextEncoder(); for (;;) { // Send 'ping' every second to keep the connection alive await writer.write(enc.encode("ping\r\n")); await scheduler.wait(1000); } } ``` ```ts export default { async fetch(request, env, ctx): Promise { // This sets up an event listener that will be called if the client disconnects from your // worker. request.signal.addEventListener('abort', () => { console.log('The request was aborted!'); }); const { readable, writable } = new IdentityTransformStream(); sendPing(writable); return new Response(readable, { headers: { 'Content-Type': 'text/plain' } }); }, } satisfies ExportedHandler; async function sendPing(writable: WritableStream): Promise { const writer = writable.getWriter(); const enc = new TextEncoder(); for (;;) { // Send 'ping' every second to keep the connection alive await writer.write(enc.encode('ping\r\n')); await scheduler.wait(1000); } } ``` * `url` string read-only * Contains the URL of the request. ### `IncomingRequestCfProperties` In addition to the properties on the standard [Request ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request) object, the `request.cf` object on an inbound `Request` contains information about the request provided by Cloudflare’s global network. All plans have access to: * `asn` Number * ASN of the incoming request, for example, `395747`. * `asOrganization` string * The organization which owns the ASN of the incoming request, for example, `Google Cloud`. * `botManagement` Object | null * Only set when using Cloudflare Bot Management. Object with the following properties: `score`, `verifiedBot`, `signedAgent`, `staticResource`, `ja3Hash`, `ja4`, and `detectionIds`. Refer to [Bot Management Variables](https://developers.cloudflare.com/bots/reference/bot-management-variables/) for more details. * `clientAcceptEncoding` string | null * If Cloudflare replaces the value of the `Accept-Encoding` header, the original value is stored in the `clientAcceptEncoding` property, for example, `"gzip, deflate, br"`. * `clientQuicRtt` number | undefined * The smoothed round-trip time (RTT) between Cloudflare and the client for QUIC connections, in milliseconds. Only present when the client connected over QUIC (HTTP/3). For example, `42`. * `clientTcpRtt` number | undefined * The smoothed round-trip time (RTT) between the client and Cloudflare for TCP connections, in milliseconds. Only present when the client connected over TCP (HTTP/1 and HTTP/2). For example, `22`. * `colo` string * The three-letter [IATA ↗](https://en.wikipedia.org/wiki/IATA%5Fairport%5Fcode) airport code of the data center that the request hit, for example, `"DFW"`. * `country` string | null * Country of the incoming request. The two-letter country code in the request. This is the same value as that provided in the `CF-IPCountry` header, for example, `"US"`. * `edgeL4` Object | undefined * Layer 4 transport statistics for the connection between the client and Cloudflare. Contains the following property: * `deliveryRate` number - The most recent data delivery rate estimate for the connection, in bytes per second. For example, `123456`. * `isEUCountry` string | null * If the country of the incoming request is in the EU, this will return `"1"`. Otherwise, this property is either omitted or `false`. * `httpProtocol` string * HTTP Protocol, for example, `"HTTP/2"`. * `hostMetadata` Object | undefined * Only populated when the incoming request is from a zone with custom hostname metadata. Refer to the Cloudflare for Platforms documentation for more about what you can add as [custom hostname metadata](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata/), and how it is exposed on the `hostMetadata` field. * `requestPriority` string | null * The browser-requested prioritization information in the request object, for example, `"weight=192;exclusive=0;group=3;group-weight=127"`. * `tlsCipher` string * The cipher for the connection to Cloudflare, for example, `"AEAD-AES128-GCM-SHA256"`. * `tlsClientAuth` Object | null * Various details about the client certificate (for mTLS connections). Refer to [Client certificate variables](https://developers.cloudflare.com/ssl/client-certificates/client-certificate-variables/) for more details. * `tlsClientCiphersSha1` string * The SHA-1 hash (Base64-encoded) of the cipher suite sent by the client during the TLS handshake, encoded in big-endian format. For example, `"GXSPDLP4G3X+prK73a4wBuOaHRc="`. * `tlsClientExtensionsSha1` string * The SHA-1 hash (Base64-encoded) of the TLS client extensions sent during the handshake, encoded in big-endian format. For example, `"OWFiM2I5ZDc0YWI0YWYzZmFkMGU0ZjhlYjhiYmVkMjgxNTU5YTU2Mg=="`. * `tlsClientExtensionsSha1Le` string * The SHA-1 hash (Base64-encoded) of the TLS client extensions sent during the handshake, encoded in little-endian format. For example, `"7zIpdDU5pvFPPBI2/PCzqbaXnRA="`. * `tlsClientHelloLength` string * The length of the client hello message sent in a [TLS handshake ↗](https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/). For example, `"508"`. Specifically, the length of the bytestring of the client hello. * `tlsClientRandom` string * The value of the 32-byte random value provided by the client in a [TLS handshake ↗](https://www.cloudflare.com/learning/ssl/what-happens-in-a-tls-handshake/). Refer to [RFC 8446 ↗](https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2) for more details. * `tlsVersion` string * The TLS version of the connection to Cloudflare, for example, `TLSv1.3`. * `city` string | null * City of the incoming request, for example, `"Austin"`. * `continent` string | null * Continent of the incoming request, for example, `"NA"`. * `latitude` string | null * Latitude of the incoming request, for example, `"30.27130"`. * `longitude` string | null * Longitude of the incoming request, for example, `"-97.74260"`. * `postalCode` string | null * Postal code of the incoming request, for example, `"78701"`. * `metroCode` string | null * Metro code (DMA) of the incoming request, for example, `"635"`. * `region` string | null * If known, the [ISO 3166-2 ↗](https://en.wikipedia.org/wiki/ISO%5F3166-2) name for the first level region associated with the IP address of the incoming request, for example, `"Texas"`. * `regionCode` string | null * If known, the [ISO 3166-2 ↗](https://en.wikipedia.org/wiki/ISO%5F3166-2) code for the first-level region associated with the IP address of the incoming request, for example, `"TX"`. * `timezone` string * Timezone of the incoming request, for example, `"America/Chicago"`. Caution The `request.cf` object is not available in the Cloudflare Workers dashboard or Playground preview editor. --- ## Methods ### Instance methods These methods are only available on an instance of a `Request` object or through its prototype. * `clone()` : Request * Creates a copy of the `Request` object. * `arrayBuffer()` : Promise * Returns a promise that resolves with an [ArrayBuffer ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/ArrayBuffer) representation of the request body. * `formData()` : Promise * Returns a promise that resolves with a [FormData ↗](https://developer.mozilla.org/en-US/docs/Web/API/FormData) representation of the request body. * `json()` : Promise * Returns a promise that resolves with a JSON representation of the request body. * `text()` : Promise * Returns a promise that resolves with a string (text) representation of the request body. --- ## The `Request` context Each time a Worker is invoked by an incoming HTTP request, the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch) is called on your Worker. The `Request` context starts when the `fetch()` handler is called, and asynchronous tasks (such as making a subrequest using the [fetch() API](https://developers.cloudflare.com/workers/runtime-apis/fetch/)) can only be run inside the `Request` context: ```js export default { async fetch(request, env, ctx) { // Request context starts here return new Response('Hello World!'); }, }; ``` ### When passing a promise to fetch event `.respondWith()` If you pass a Response promise to the fetch event `.respondWith()` method, the request context is active during any asynchronous tasks which run before the Response promise has settled. You can pass the event to an async handler, for example: ```js addEventListener("fetch", event => { event.respondWith(eventHandler(event)) }) // No request context available here async function eventHandler(event){ // Request context available here return new Response("Hello, Workers!") } ``` ### Errors when attempting to access an inactive `Request` context Any attempt to use APIs such as `fetch()` or access the `Request` context during script startup will throw an exception: ```js const promise = fetch("https://example.com/") // Error async function eventHandler(event){..} ``` This code snippet will throw during script startup, and the `"fetch"` event listener will never be registered. --- ### Set the `Content-Length` header The `Content-Length` header will be automatically set by the runtime based on whatever the data source for the `Request` is. Any value manually set by user code in the `Headers` will be ignored. To have a `Content-Length` header with a specific value specified, the `body` of the `Request` must be either a `FixedLengthStream` or a fixed-length value just as a string or `TypedArray`. A `FixedLengthStream` is an identity `TransformStream` that permits only a fixed number of bytes to be written to it. ```js const { writable, readable } = new FixedLengthStream(11); const enc = new TextEncoder(); const writer = writable.getWriter(); writer.write(enc.encode("hello world")); writer.end(); const req = new Request('https://example.org', { method: 'POST', body: readable }); ``` Using any other type of `ReadableStream` as the body of a request will result in Chunked-Encoding being used. --- ## Differences The Workers implementation of the `Request` interface includes several extensions to the web standard `Request` API. These differences are intentional and provide additional functionality specific to the Workers runtime. TypeScript users Workers type definitions (from `@cloudflare/workers-types` or generated via [wrangler types](https://developers.cloudflare.com/workers/wrangler/commands/general/#types)) define a `Request` type that includes Workers-specific properties like `cf`. This type is not directly compatible with the standard `Request` type from `lib.dom.d.ts`. If you are working with code that uses both Workers types and standard web types, you may need to use type assertions or create a new `Request` object. ### The `cf` property Workers adds a `cf` property to the `Request` object that contains Cloudflare-specific metadata about the incoming request. This property is not part of the web standard, and is only available in the Workers runtime. Refer to [IncomingRequestCfProperties](#incomingrequestcfproperties) for details. ### The `headers` property The `headers` property returns a Workers-specific [Headers](https://developers.cloudflare.com/workers/runtime-apis/headers/) object that includes additional methods like `getAll()` for `Set-Cookie` headers. Refer to the [Headers documentation](https://developers.cloudflare.com/workers/runtime-apis/headers/#differences) for details on how the Workers `Headers` implementation differs from the web standard. ### Immutability Incoming `Request` objects passed to the [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/) are immutable. To modify properties of an incoming request, you must create a new `Request` object. --- ## Related resources * [Examples: Modify request property](https://developers.cloudflare.com/workers/examples/modify-request-property/) * [Examples: Accessing the cf object](https://developers.cloudflare.com/workers/examples/accessing-the-cloudflare-object/) * [Reference: Response](https://developers.cloudflare.com/workers/runtime-apis/response/) * Write your Worker code in [ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) for an optimized experience. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/request/#page","headline":"Request · Cloudflare Workers docs","description":"Interface that represents an HTTP request.","url":"https://developers.cloudflare.com/workers/runtime-apis/request/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-02","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Interface that represents an HTTP response. title: Response image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Response Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/response/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The `Response` interface represents an HTTP response and is part of the Fetch API. --- ## Constructor ```js let response = new Response(body, init); ``` ### Parameters * `body` optional * An object that defines the body text for the response. Can be `null` or any one of the following types: * BufferSource * FormData * ReadableStream * URLSearchParams * USVString * `init` optional * An `options` object that contains custom settings to apply to the response. Valid options for the `options` object include: * `cf` any | null * An object that contains Cloudflare-specific information. This object is not part of the Fetch API standard and is only available in Cloudflare Workers. This field is only used by consumers of the Response for informational purposes and does not have any impact on Workers behavior. * `encodeBody` string * Workers have to compress data according to the `content-encoding` header when transmitting, to serve data that is already compressed, this property has to be set to `"manual"`, otherwise the default is `"automatic"`. * `headers` Headers | ByteString * Any headers to add to your response that are contained within a [Headers](https://developers.cloudflare.com/workers/runtime-apis/request/#parameters) object or object literal of [ByteString ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/String) key-value pairs. * `status` int * The status code for the response, such as `200`. * `statusText` string * The status message associated with the status code, such as, `OK`. * `webSocket` WebSocket | null * This is present in successful WebSocket handshake responses. For example, if a client sends a WebSocket upgrade request to an origin and a Worker intercepts the request and then forwards it to the origin and the origin replies with a successful WebSocket upgrade response, the Worker sees `response.webSocket`. This establishes a WebSocket connection proxied through a Worker. Note that you cannot intercept data flowing over a WebSocket connection. ## Properties * `response.body` Readable Stream * A getter to get the body contents. * `response.bodyUsed` boolean * A boolean indicating if the body was used in the response. * `response.headers` Headers * The headers for the response. * `response.ok` boolean * A boolean indicating if the response was successful (status in the range `200`\-`299`). * `response.redirected` boolean * A boolean indicating if the response is the result of a redirect. If so, its URL list has more than one entry. * `response.status` int * The status code of the response (for example, `200` to indicate success). * `response.statusText` string * The status message corresponding to the status code (for example, `OK` for `200`). * `response.url` string * The URL of the response. The value is the final URL obtained after any redirects. * `response.webSocket` WebSocket? * This is present in successful WebSocket handshake responses. For example, if a client sends a WebSocket upgrade request to an origin and a Worker intercepts the request and then forwards it to the origin and the origin replies with a successful WebSocket upgrade response, the Worker sees `response.webSocket`. This establishes a WebSocket connection proxied through a Worker. Note that you cannot intercept data flowing over a WebSocket connection. ## Methods ### Instance methods * `clone()` : Response * Creates a clone of a [Response](#response) object. * `json()` : Response * Creates a new response with a JSON-serialized payload. * `redirect()` : Response * Creates a new response with a different URL. ### Additional instance methods `Response` implements the [Body ↗](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI/Using%5FFetch#body) mixin of the [Fetch API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Fetch%5FAPI), and therefore `Response` instances additionally have the following methods available: * `arrayBuffer()` : Promise * Takes a [Response](#response) stream, reads it to completion, and returns a promise that resolves with an [ArrayBuffer ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/ArrayBuffer). * `formData()` : Promise * Takes a [Response](#response) stream, reads it to completion, and returns a promise that resolves with a [FormData ↗](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. * `json()` : Promise * Takes a [Response](#response) stream, reads it to completion, and returns a promise that resolves with the result of parsing the body text as [JSON ↗](https://developer.mozilla.org/en-US/docs/Web/). * `text()` : Promise * Takes a [Response](#response) stream, reads it to completion, and returns a promise that resolves with a [USVString ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/String) (text). ### Set the `Content-Length` header The `Content-Length` header will be automatically set by the runtime based on whatever the data source for the `Response` is. Any value manually set by user code in the `Headers` will be ignored. To have a `Content-Length` header with a specific value specified, the `body` of the `Response` must be either a `FixedLengthStream` or a fixed-length value just as a string or `TypedArray`. A `FixedLengthStream` is an identity `TransformStream` that permits only a fixed number of bytes to be written to it. ```js const { writable, readable } = new FixedLengthStream(11); const enc = new TextEncoder(); const writer = writable.getWriter(); writer.write(enc.encode("hello world")); writer.end(); return new Response(readable); ``` Using any other type of `ReadableStream` as the body of a response will result in chunked encoding being used. --- ## Differences The Workers implementation of the `Response` interface includes several extensions to the web standard `Response` API. These differences are intentional and provide additional functionality specific to the Workers runtime. TypeScript users Workers type definitions (from `@cloudflare/workers-types` or generated via [wrangler types](https://developers.cloudflare.com/workers/wrangler/commands/general/#types)) define a `Response` type that includes Workers-specific properties like `cf` and `webSocket`. This type is not directly compatible with the standard `Response` type from `lib.dom.d.ts`. If you are working with code that uses both Workers types and standard web types, you may need to use type assertions. ### The `cf` property Workers adds an optional `cf` property to the `Response` object. This property can be set in the `ResponseInit` options and is used for informational purposes by consumers of the Response. It does not affect Workers behavior. ### The `webSocket` property Workers adds a `webSocket` property to the `Response` object to support WebSocket connections. This property is present in successful WebSocket handshake responses. Refer to [WebSockets](https://developers.cloudflare.com/workers/runtime-apis/websockets/) for more information. ### The `encodeBody` option Workers adds an `encodeBody` option in `ResponseInit` that controls how the response body is compressed. Set this to `"manual"` when serving pre-compressed data to prevent automatic compression. ### The `headers` property The `headers` property returns a Workers-specific [Headers](https://developers.cloudflare.com/workers/runtime-apis/headers/) object that includes additional methods like `getAll()` for `Set-Cookie` headers. Refer to the [Headers documentation](https://developers.cloudflare.com/workers/runtime-apis/headers/#differences) for details on how the Workers `Headers` implementation differs from the web standard. --- ## Related resources * [Examples: Modify response](https://developers.cloudflare.com/workers/examples/modify-response/) * [Examples: Conditional response](https://developers.cloudflare.com/workers/examples/conditional-response/) * [Reference: Request](https://developers.cloudflare.com/workers/runtime-apis/request/) * Write your Worker code in [ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) for an optimized experience. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/response/#page","headline":"Response · Cloudflare Workers docs","description":"Interface that represents an HTTP response.","url":"https://developers.cloudflare.com/workers/runtime-apis/response/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: The built-in, JavaScript-native RPC system built into Workers and Durable Objects. title: Remote-procedure call (RPC) image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Remote-procedure call (RPC) Last updated Aug 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/rpc/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note To use RPC, [define a compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates) of `2024-04-03` or higher, or include `rpc` in your [compatibility flags](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag). Workers provide a built-in, JavaScript-native [RPC (Remote Procedure Call) ↗](https://en.wikipedia.org/wiki/Remote%5Fprocedure%5Fcall) system, allowing you to: * Define public methods on your Worker that can be called by other Workers on the same Cloudflare account, via [Service Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc) * Define public methods on [Durable Objects](https://developers.cloudflare.com/durable-objects) that can be called by other workers on the same Cloudflare account that declare a binding to it. The RPC system is designed to feel as similar as possible to calling a JavaScript function in the same Worker. In most cases, you should be able to write code in the same way you would if everything was in a single Worker. ## Example For example, if Worker B implements the public method `add(a, b)`: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_b", "main": "./src/workerB.js" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_b" main = "./src/workerB.js" ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async fetch() { return new Response("Hello from Worker B"); } add(a, b) { return a + b; } } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async fetch() { return new Response("Hello from Worker B"); } add(a: number, b: number) { return a + b; } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response("Hello from Worker B") def add(self, a: int, b: int) -> int: return a + b ``` Worker A can declare a [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) to Worker B: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "worker_a", "main": "./src/workerA.js", "services": [ { "binding": "WORKER_B", "service": "worker_b" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "worker_a" main = "./src/workerA.js" [[services]] binding = "WORKER_B" service = "worker_b" ``` Making it possible for Worker A to call the `add()` method from Worker B: ```js export default { async fetch(request, env) { const result = await env.WORKER_B.add(1, 2); return new Response(result); }, }; ``` ```ts export default { async fetch(request, env) { const result = await env.WORKER_B.add(1, 2); return new Response(result); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): result = await self.env.WORKER_B.add(1, 2) return Response(f"Result: {result}") ``` The client, in this case Worker A, calls Worker B and tells it to execute a specific procedure using specific arguments that the client provides. This is accomplished with standard JavaScript classes. ## All calls are asynchronous Whether or not the method you are calling was declared asynchronous on the server side, it will behave as such on the client side. You must `await` the result. Note that RPC calls do not actually return `Promise`s, but they return a type that behaves like a `Promise`. The type is a "custom thenable", in that it implements the method `then()`. JavaScript supports awaiting any "thenable" type, so, for the most part, you can treat the return value like a Promise. (We'll see why the type is not actually a Promise a bit later.) ## Structured clonable types, and more Nearly all types that are [Structured Cloneable ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI/Structured%5Fclone%5Falgorithm#supported%5Ftypes) can be used as a parameter or return value of an RPC method. This includes, most basic "value" types in JavaScript, including objects, arrays, strings and numbers. As an exception to Structured Clone, application-defined classes (or objects with custom prototypes) cannot be passed over RPC, except as described below. The RPC system also supports a number of types that are not Structured Cloneable, including: * Functions, which are replaced by stubs that call back to the sender. * Application-defined classes that extend `RpcTarget`, which are similarly replaced by stubs. * [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/) and [WriteableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/), with automatic streaming flow control. * [Request](https://developers.cloudflare.com/workers/runtime-apis/request/) and [Response](https://developers.cloudflare.com/workers/runtime-apis/response/), for conveniently representing HTTP messages. * RPC stubs themselves, even if the stub was received from a third Worker. ## Functions You can send a function over RPC. When you do so, the function is replaced by a "stub". The recipient can call the stub like a function, but doing so makes a new RPC back to the place where the function originated. ### Return functions from RPC methods Consider the following two Workers, connected via a [Service Binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc). The counter service provides the RPC method `newCounter()`, which returns a function: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "counter-service", "main": "./src/counterService.js" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "counter-service" main = "./src/counterService.js" ``` ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async fetch() { return new Response("Hello from counter-service"); } async newCounter() { let value = 0; return (increment = 0) => { value += increment; return value; }; } } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { async fetch() { return new Response("Hello from counter-service"); } async newCounter() { let value = 0; return (increment = 0) => { value += increment; return value; }; } } ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): return Response("Hello from counter-service") async def new_counter(self): value = 0 def increment(amount=0): nonlocal value value += amount return value return increment ``` This function can then be called by the client Worker: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "client_worker", "main": "./src/clientWorker.js", "services": [ { "binding": "COUNTER_SERVICE", "service": "counter-service" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "client_worker" main = "./src/clientWorker.js" [[services]] binding = "COUNTER_SERVICE" service = "counter-service" ``` ```js export default { async fetch(request, env) { using f = await env.COUNTER_SERVICE.newCounter(); await f(2); // returns 2 await f(1); // returns 3 const count = await f(-5); // returns -2 return new Response(count); }, }; ``` ```ts export default { async fetch(request: Request, env: Env) { using f = await env.COUNTER_SERVICE.newCounter(); await f(2); // returns 2 await f(1); // returns 3 const count = await f(-5); // returns -2 return new Response(count); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): counter = await self.env.COUNTER_SERVICE.new_counter() await counter(2) # returns 2 await counter(1) # returns 3 count = await counter(-5) # returns -2 return Response(str(count)) ``` Note Refer to [Explicit Resource Management](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle) to learn more about the `using` declaration shown in the example above. How is this possible? The system is not serializing the function itself. When the function returned by `CounterService` is called, it runs within `CounterService` — even if it is called by another Worker. Under the hood, the caller is not really calling the function itself directly, but calling what is called a "stub". A "stub" is a [Proxy ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy) object that allows the client to call the remote service as if it were local, running in the same Worker. Behind the scenes, it calls back to the Worker that implements `CounterService` and asks it to execute the function closure that had been returned earlier. ### Send functions as parameters of RPC methods You can also send a function in the parameters of an RPC. This enables the "server" to call back to the "client", reversing the direction of the relationship. Because of this, the words "client" and "server" can be ambiguous when talking about RPC. The "server" is a Durable Object or WorkerEntrypoint, and the "client" is the Worker that invoked the server via a binding. But, RPCs can flow both ways between the two. When talking about an individual RPC, we recommend instead using the words "caller" and "callee". ## Class Instances To use an instance of a class that you define as a parameter or return value of an RPC method, you must extend the built-in `RpcTarget` class. Consider the following example: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "counter", "main": "./src/counter.js" } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "counter" main = "./src/counter.js" ``` ```js import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers"; class Counter extends RpcTarget { #value = 0; increment(amount) { this.#value += amount; return this.#value; } get value() { return this.#value; } } export class CounterService extends WorkerEntrypoint { async newCounter() { return new Counter(); } } export default { fetch() { return new Response("ok"); }, }; ``` ```ts import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers"; class Counter extends RpcTarget { #value = 0; increment(amount: number) { this.#value += amount; return this.#value; } get value() { return this.#value; } } export class CounterService extends WorkerEntrypoint { async newCounter() { return new Counter(); } } export default { fetch() { return new Response("ok"); }, }; ``` The method `increment` can be called directly by the client, as can the public property `value`: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "client-worker", "main": "./src/clientWorker.js", "services": [ { "binding": "COUNTER_SERVICE", "service": "counter", "entrypoint": "CounterService" } ] } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "client-worker" main = "./src/clientWorker.js" [[services]] binding = "COUNTER_SERVICE" service = "counter" entrypoint = "CounterService" ``` ```js export default { async fetch(request, env) { using counter = await env.COUNTER_SERVICE.newCounter(); await counter.increment(2); // returns 2 await counter.increment(1); // returns 3 await counter.increment(-5); // returns -2 const count = await counter.value; // returns -2 return new Response(count); }, }; ``` ```ts export default { async fetch(request: Request, env: Env) { using counter = await env.COUNTER_SERVICE.newCounter(); await counter.increment(2); // returns 2 await counter.increment(1); // returns 3 await counter.increment(-5); // returns -2 const count = await counter.value; // returns -2 return new Response(count); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): counter = await self.env.COUNTER_SERVICE.new_counter() await counter.increment(2) # returns 2 await counter.increment(1) # returns 3 await counter.increment(-5) # returns -2 count = await counter.value # returns -2 return Response(str(count)) ``` Note Refer to [Explicit Resource Management](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle) to learn more about the `using` declaration shown in the JavaScript/TypeScript example above. Classes that extend `RpcTarget` work a lot like functions: the object itself is not serialized, but is instead replaced by a stub. In this case, the stub itself is not callable, but its methods are. Calling any method on the stub actually makes an RPC back to the original object, where it was created. As shown above, you can also access properties of classes. Properties behave like RPC methods that don't take any arguments — you await the property to asynchronously fetch its current value. Note that the act of awaiting the property (which, behind the scenes, calls `.then()` on it) is what causes the property to be fetched. If you do not use `await` when accessing the property, it will not be fetched. Note While it's possible to define a similar interface to the caller using an object that contains many functions, this is less efficient. If you return an object that contains five functions, then you are creating five stubs. If you return a class instance, where the class declares five methods, you are only returning a single stub. Returning a single stub is often more efficient and easier to reason about. Moreover, when returning a plain object (not a class), non-function properties of the object will be transmitted at the time the object itself is transmitted; they cannot be fetched asynchronously on-demand. Note Classes which do not inherit `RpcTarget` cannot be sent over RPC at all. This differs from Structured Clone, which defines application-defined classes as clonable. Why the difference? By default, the Structured Clone algorithm simply ignores an object's class entirely. So, the recipient receives a plain object, containing the original object's instance properties but entirely missing its original type. This behavior is rarely useful in practice, and could be confusing if the developer had intended the class to be treated as an `RpcTarget`. So, Workers RPC has chosen to disallow classes that are not `RpcTarget`s, to avoid any confusion. ### Promise pipelining When you call an RPC method and get back an object, it's common to immediately call a method on the object: ```js // Two round trips. using counter = await env.COUNTER_SERVICE.getCounter(); await counter.increment(); ``` ```ts // Two round trips. using counter = await env.COUNTER_SERVICE.getCounter(); await counter.increment(); ``` ```python # Two round trips. counter = await self.env.COUNTER_SERVICE.get_counter() await counter.increment() ``` But consider the case where the Worker service that you are calling may be far away across the network, as in the case of [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) or [Durable Objects](https://developers.cloudflare.com/durable-objects). The code above makes two round trips, once when calling `getCounter()`, and again when calling `.increment()`. We'd like to avoid this. With most RPC systems, the only way to avoid the problem would be to combine the two calls into a single "batch" call, perhaps called `getCounterAndIncrement()`. However, this makes the interface worse. You wouldn't design a local interface this way. Workers RPC allows a different approach: You can simply omit the first `await`: ```js // Only one round trip! Note the missing `await`. using promiseForCounter = env.COUNTER_SERVICE.getCounter(); await promiseForCounter.increment(); ``` ```ts // Only one round trip! Note the missing `await`. using promiseForCounter = env.COUNTER_SERVICE.getCounter(); await promiseForCounter.increment(); ``` ```python # Only one round trip! Note the missing await. promise_for_counter = self.env.COUNTER_SERVICE.get_counter() await promise_for_counter.increment() ``` In this code, `getCounter()` returns a promise for a counter. Normally, the only thing you would do with a promise is `await` it. However, Workers RPC promises are special: they also allow you to initiate speculative calls on the future result of the promise. These calls are sent to the server immediately, without waiting for the initial call to complete. Thus, multiple chained calls can be completed in a single round trip. How does this work? The promise returned by an RPC is not a real JavaScript `Promise`. Instead, it is a custom ["Thenable" ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Promise#thenables). It has a `.then()` method like `Promise`, which allows it to be used in all the places where you'd use a normal `Promise`. For instance, you can `await` it. But, in addition to that, an RPC promise also acts like a stub. Calling any method name on the promise forms a speculative call on the promise's eventual result. This is known as "promise pipelining". This works when calling properties of objects returned by RPC methods as well. For example: ```js import { WorkerEntrypoint } from "cloudflare:workers"; export class MyService extends WorkerEntrypoint { async foo() { return { bar: { baz: () => "qux", }, }; } } ``` ```ts import { WorkerEntrypoint } from "cloudflare:workers"; export class MyService extends WorkerEntrypoint { async foo() { return { bar: { baz: () => "qux", }, }; } } ``` ```python from workers import WorkerEntrypoint class MyService(WorkerEntrypoint): async def foo(self): return { "bar": { "baz": lambda: "qux", }, } ``` ```js export default { async fetch(request, env) { using foo = env.MY_SERVICE.foo(); let baz = await foo.bar.baz(); return new Response(baz); }, }; ``` ```ts export default { async fetch(request, env) { using foo = env.MY_SERVICE.foo(); let baz = await foo.bar.baz(); return new Response(baz); }, }; ``` ```python from workers import WorkerEntrypoint, Response class Default(WorkerEntrypoint): async def fetch(self, request): foo = self.env.MY_SERVICE.foo() baz = await foo.bar.baz() return Response(baz) ``` If the initial RPC ends up throwing an exception, then any pipelined calls will also fail with the same exception ## ReadableStream, WriteableStream, Request and Response You can send and receive [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/), [WriteableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/), [Request](https://developers.cloudflare.com/workers/runtime-apis/request/), and [Response](https://developers.cloudflare.com/workers/runtime-apis/response/) using RPC methods. When doing so, bytes in the body are automatically streamed with appropriate flow control. This allows you to send messages over RPC which are larger than [the typical 32 MiB limit](#limitations). Only [byte-oriented streams ↗](https://developer.mozilla.org/en-US/docs/Web/API/Streams%5FAPI/Using%5Freadable%5Fbyte%5Fstreams) (streams with an underlying byte source of `type: "bytes"`) are supported. In all cases, ownership of the stream is transferred to the recipient. The sender can no longer read/write the stream after sending it. If the sender wishes to keep its own copy, it can use the [tee() method of ReadableStream ↗](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/tee) or the [clone() method of Request or Response ↗](https://developer.mozilla.org/en-US/docs/Web/API/Response/clone). Keep in mind that doing this may force the system to buffer bytes and lose the benefits of flow control. ## Forwarding RPC stubs A stub received over RPC from one Worker can be forwarded over RPC to another Worker. ```js using counter = env.COUNTER_SERVICE.getCounter(); await env.ANOTHER_SERVICE.useCounter(counter); ``` ```ts using counter = env.COUNTER_SERVICE.getCounter(); await env.ANOTHER_SERVICE.useCounter(counter); ``` ```python counter = self.env.COUNTER_SERVICE.get_counter() await self.env.ANOTHER_SERVICE.use_counter(counter) ``` Here, three different workers are involved: 1. The calling Worker (we'll call this the "introducer") 2. `COUNTER_SERVICE` 3. `ANOTHER_SERVICE` When `ANOTHER_SERVICE` calls a method on the `counter` that is passed to it, this call will automatically be proxied through the introducer and on to the [RpcTarget](https://developers.cloudflare.com/workers/runtime-apis/rpc/) class implemented by `COUNTER_SERVICE`. In this way, the introducer Worker can connect two Workers that did not otherwise have any ability to form direct connections to each other. Currently, this proxying only lasts until the end of the Workers' execution contexts. A proxy connection cannot be persisted for later use. ## Video Tutorial In this video, we explore how Cloudflare Workers support Remote Procedure Calls (RPC) to simplify communication between Workers. Learn how to implement RPC in your JavaScript applications and build serverless solutions with ease. Whether you're managing microservices or optimizing web architecture, this tutorial will show you how to quickly set up and use Cloudflare Workers for RPC calls. By the end of this video, you'll understand how to call functions between Workers, pass functions as arguments, and implement user authentication with Cloudflare Workers. ## More Details * [Lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/) * [Reserved Methods](https://developers.cloudflare.com/workers/runtime-apis/rpc/reserved-methods/) * [Visibility and Security Model](https://developers.cloudflare.com/workers/runtime-apis/rpc/visibility/) * [TypeScript](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/) * [Error handling](https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/) ## Limitations * [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) is currently ignored when making RPC calls. If Smart Placement is enabled for Worker A, and Worker B declares a [Service Binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) to it, when Worker B calls Worker A via RPC, Worker A will run locally, on the same machine. * The maximum serialized RPC limit is 32 MiB. Consider using [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/) when returning more data. ```js export class MyService extends WorkerEntrypoint { async foo() { // Although this works, it puts a lot of memory pressure on the isolate. // If possible, streaming the data from its original source is much preferred and would yield better performance. // If you must buffer the data into memory, consider chunking it into smaller pieces if possible. const sizeInBytes = 33 * 1024 * 1024; // 33 MiB const arr = new Uint8Array(sizeInBytes); return new ReadableStream({ start(controller) { controller.enqueue(arr); controller.close(); }, }); } } ``` ```ts export class MyService extends WorkerEntrypoint { async foo() { // Although this works, it puts a lot of memory pressure on the isolate. // If possible, streaming the data from its original source is much preferred and would yield better performance. // If you must buffer the data into memory, consider chunking it into smaller pieces if possible. const sizeInBytes = 33 * 1024 * 1024; // 33 MiB const arr = new Uint8Array(sizeInBytes); return new ReadableStream({ start(controller) { controller.enqueue(arr); controller.close(); }, }); } } ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/rpc/#page","headline":"Remote-procedure call (RPC) · Cloudflare Workers docs","description":"The built-in, JavaScript-native RPC system built into Workers and Durable Objects.","url":"https://developers.cloudflare.com/workers/runtime-apis/rpc/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["RPC"]} ``` --- --- description: How exceptions, stack traces, and logging works with the Workers RPC system. title: Error handling image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Error handling Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Exceptions An exception thrown by an RPC method implementation will propagate to the caller. If it is one of the standard JavaScript Error types, the `message` and prototype's `name` will be retained, though the stack trace is not. ### Unsupported error types * If an [AggregateError ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/AggregateError) is thrown by an RPC method, it is not propagated back to the caller. * The [SuppressedError ↗](https://github.com/tc39/proposal-explicit-resource-management?tab=readme-ov-file#the-suppressederror-error) type from the Explicit Resource Management proposal is not currently implemented or supported in Workers. * Own properties of error objects, such as the [cause ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Error/cause) property, are not propagated back to the caller ## Additional properties For some remote exceptions, the runtime may set properties on the propagated exception to provide more information about the error; see [Durable Object error handling](https://developers.cloudflare.com/durable-objects/best-practices/error-handling) for more details. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/#page","headline":"Workers RPC — Error Handling · Cloudflare Workers docs","description":"How exceptions, stack traces, and logging works with the Workers RPC system.","url":"https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Memory management, resource management, and the lifecycle of RPC stubs. title: Lifecycle image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Lifecycle Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Lifetimes, Memory and Resource Management When you call another Worker over RPC using a Service binding, you are using memory in the Worker you are calling. Consider the following example: ```js let user = await env.USER_SERVICE.findUser(id); ``` Assume that `findUser()` on the server side returns an object extending `RpcTarget`, thus `user` on the client side ends up being a stub pointing to that remote object. As long as the stub still exists on the client, the corresponding object on the server cannot be garbage collected. But, each isolate has its own garbage collector which cannot see into other isolates. So, in order for the server's isolate to know that the object can be collected, the calling isolate must send it an explicit signal saying so, called "disposing" the stub. In many cases (described below), the system will automatically realize when a stub is no longer needed, and will dispose it automatically. However, for best performance, your code should dispose stubs explicitly when it is done with them. ## Explicit Resource Management To ensure resources are properly disposed of, you should use [Explicit Resource Management ↗](https://github.com/tc39/proposal-explicit-resource-management), a new JavaScript language feature that allows you to explicitly signal when resources can be disposed of. Explicit Resource Management is a Stage 3 TC39 proposal — it is [coming to V8 soon ↗](https://bugs.chromium.org/p/v8/issues/detail?id=13559). Explicit Resource Management adds the following language features: * The [using declaration ↗](https://github.com/tc39/proposal-explicit-resource-management?tab=readme-ov-file#using-declarations) * [Symbol.dispose and Symbol.asyncDispose ↗](https://github.com/tc39/proposal-explicit-resource-management?tab=readme-ov-file#additions-to-symbol) If a variable is declared with `using`, when the variable is no longer in scope, the variable's disposer will be invoked. For example: ```js function sendEmail(id, message) { using user = await env.USER_SERVICE.findUser(id); await user.sendEmail(message); // user[Symbol.dispose]() is implicitly called at the end of the scope. } ``` `using` declarations are useful to make sure you can't forget to dispose stubs — even if your code is interrupted by an exception. ### How to use the `using` declaration in your Worker [Wrangler](https://developers.cloudflare.com/workers/wrangler/) v4+ supports the `using` keyword natively. If you are using an earlier version of Wrangler, you will need to manually dispose of resources instead. The following code: ```js { using counter = await env.COUNTER_SERVICE.newCounter(); await counter.increment(2); await counter.increment(4); } ``` ...is equivalent to: ```js { const counter = await env.COUNTER_SERVICE.newCounter(); try { await counter.increment(2); await counter.increment(4); } finally { counter[Symbol.dispose](); } } ``` ## Automatic disposal and execution contexts The RPC system automatically disposes of stubs in the following cases: ### End of event handler / execution context When an event handler is "done", any stubs created as part of the event are automatically disposed. For example, consider a [fetch() handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch) which handles incoming HTTP events. The handler may make outgoing RPCs as part of handling the event, and those may return stubs. When the final HTTP response is sent, the handler is "done", and all stubs are immediately disposed. More precisely, the event has an "execution context", which begins when the handler is first invoked, and ends when the HTTP response is sent. The execution context may also end early if the client disconnects before receiving a response, or it can be extended past its normal end point by calling [ctx.waitUntil()](https://developers.cloudflare.com/workers/runtime-apis/context). For example, the Worker below does not make use of the `using` declaration, but stubs will be disposed of once the `fetch()` handler returns a response: ```js export default { async fetch(request, env, ctx) { let authResult = await env.AUTH_SERVICE.checkCookie( req.headers.get("Cookie"), ); if (!authResult.authorized) { return new Response("Not authorized", { status: 403 }); } let profile = await authResult.user.getProfile(); return new Response(`Hello, ${profile.name}!`); }, }; ``` A Worker invoked via RPC also has an execution context. The context begins when an RPC method on a `WorkerEntrypoint` is invoked. If no stubs are passed in the parameters or results of this RPC, the context ends (the event is "done") when the RPC returns. However, if any stubs are passed, then the execution context is implicitly extended until all such stubs are disposed (and all calls made through them have returned). As with HTTP, if the client disconnects, the server's execution context is canceled immediately, regardless of whether stubs still exist. A client that is itself another Worker is considered to have disconnected when its own execution context ends. Again, the context can be extended with [ctx.waitUntil()](https://developers.cloudflare.com/workers/runtime-apis/context). ### Stubs received as parameters in an RPC call When stubs are received in the parameters of an RPC, those stubs are automatically disposed when the call returns. If you wish to keep the stubs longer than that, you must call the `dup()` method on them. ### Disposing RPC objects disposes stubs that are part of that object When an RPC returns any kind of object, that object will have a disposer added by the system. Disposing it will dispose all stubs returned by the call. For instance, if an RPC returns an array of four stubs, the array itself will have a disposer that disposes all four stubs. The only time the value returned by an RPC does not have a disposer is when it is a primitive value, such as a number or string. These types cannot have disposers added to them, but because these types cannot themselves contain stubs, there is no need for a disposer in this case. This means you should almost always store the result of an RPC into a `using` declaration: ```js using result = stub.foo(); ``` This way, if the result contains any stubs, they will be disposed of. Even if you don't expect the RPC to return stubs, if it returns any kind of an object, it is a good idea to store it into a `using` declaration. This way, if the RPC is extended in the future to return stubs, your code is ready. If you decide you want to keep a returned stub beyond the scope of the `using` declaration, you can call `dup()` on the stub before the end of the scope. (Remember to explicitly dispose the duplicate later.) ## Disposers and `RpcTarget` classes A class that extends [RpcTarget](https://developers.cloudflare.com/workers/runtime-apis/rpc/) can optionally implement a disposer: ```js class Foo extends RpcTarget { [Symbol.dispose]() { // ... } } ``` The RpcTarget's disposer runs after the last stub is disposed. Note that the client-side call to the stub's disposer does not wait for the server-side disposer to be called; the server's disposer is called later on. Because of this, any exceptions thrown by the disposer do not propagate to the client; instead, they are reported as uncaught exceptions. Note that an `RpcTarget`'s disposer must be declared as `Symbol.dispose`. `Symbol.asyncDispose` is not supported. ## The `dup()` method Sometimes, you need to pass a stub to a function which will dispose the stub when it is done, but you also want to keep the stub for later use. To solve this problem, you can "dup" the stub: ```js let stub = await env.SOME_SERVICE.getThing(); // Create a duplicate. let stub2 = stub.dup(); // Call some function that will dispose the stub. await func(stub); // stub2 is still valid ``` You can think of `dup()` like the [Unix system call of the same name ↗](https://man7.org/linux/man-pages/man2/dup.2.html): it creates a new handle pointing at the same target, which must be independently closed (disposed). If the instance of the [RpcTarget class](https://developers.cloudflare.com/workers/runtime-apis/rpc/) that the stubs point to has a disposer, the disposer will only be invoked when all duplicates have been disposed. However, this only applies to duplicates that originate from the same stub. If the same instance of `RpcTarget` is passed over RPC multiple times, a new stub is created each time, and these are not considered duplicates of each other. Thus, the disposer will be invoked once for each time the `RpcTarget` was sent. In order to avoid this situation, you can manually create a stub locally, and then pass the stub across RPC multiple times. When passing a stub over RPC, ownership of the stub transfers to the recipient, so you must make a `dup()` for each time you send it: ```js import { RpcTarget, RpcStub } from "cloudflare:workers"; class Foo extends RpcTarget { // ... } let obj = new Foo(); let stub = new RpcStub(obj); await rpc1(stub.dup()); // sends a dup of `stub` await rpc2(stub.dup()); // sends another dup of `stub` stub[Symbol.dispose](); // disposes the original stub // obj's disposer will be called when the other two stubs // are disposed remotely. ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#page","headline":"Workers RPC — Lifecycle · Cloudflare Workers docs","description":"Memory management, resource management, and the lifecycle of RPC stubs.","url":"https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Reserved methods with special behavior that are treated differently. title: Reserved Methods image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Reserved Methods Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/rpc/reserved-methods/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Some method names are reserved or have special semantics. ## Special Methods For backwards compatibility, when extending `WorkerEntrypoint` or `DurableObject`, the following method names have special semantics. Note that this does _not_ apply to `RpcTarget`. On `RpcTarget`, these methods work like any other RPC method. ### `fetch()` The `fetch()` method is treated specially — it can only be used to handle an HTTP request — equivalent to the [fetch handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/). You may implement a `fetch()` method in your class that extends `WorkerEntrypoint` — but it must accept only one parameter of type [Request ↗](https://developer.mozilla.org/en-US/docs/Web/API/Request), and must return an instance of [Response ↗](https://developer.mozilla.org/en-US/docs/Web/API/Response), or a [Promise ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Promise) of one. On the client side, `fetch()` called on a service binding or Durable Object stub works like the standard global `fetch()`. That is, the caller may pass one or two parameters to `fetch()`. If the caller does not simply pass a single `Request` object, then a new `Request` is implicitly constructed, passing the parameters to its constructor, and that request is what is actually sent to the server. Some properties of `Request` control the behavior of `fetch()` on the client side and are not actually sent to the server. For example, the property `redirect: "auto"` (which is the default) instructs `fetch()` that if the server returns a redirect response, it should automatically be followed, resulting in an HTTP request to the public internet. Again, this behavior is according to the Fetch API standard. In short, `fetch()` doesn't have RPC semantics, it has Fetch API semantics. ### `connect()` The `connect()` method of the `WorkerEntrypoint` class is reserved for opening a socket-like connection to your Worker. This is currently not implemented or supported — though you can [open a TCP socket from a Worker](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/) or connect directly to databases over a TCP socket with [Hyperdrive](https://developers.cloudflare.com/hyperdrive/get-started/). ## Disallowed Method Names The following method (or property) names may not be used as RPC methods on any RPC type (including `WorkerEntrypoint`, `DurableObject`, and `RpcTarget`): * `dup`: This is reserved for duplicating a stub. Refer to the [RPC Lifecycle](https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle) docs to learn more about `dup()`. * `constructor`: This name has special meaning for JavaScript classes. It is not intended to be called as a method, so it is not allowed over RPC. The following methods are disallowed only on `WorkerEntrypoint` and `DurableObject`, but allowed on `RpcTarget`. These methods have historically had special meaning to Durable Objects, where they are used to handle certain system-generated events. * `alarm` * `webSocketMessage` * `webSocketClose` * `webSocketError` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/rpc/reserved-methods/#page","headline":"Workers RPC — Reserved Methods · Cloudflare Workers docs","description":"Reserved methods with special behavior that are treated differently.","url":"https://developers.cloudflare.com/workers/runtime-apis/rpc/reserved-methods/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: How TypeScript types for your Worker or Durable Object's RPC methods are generated and exposed to clients title: TypeScript image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # TypeScript Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Running [wrangler types](https://developers.cloudflare.com/workers/languages/typescript/#generate-types) generates runtime types including the `Service` and `DurableObjectNamespace` types, each of which accepts a single type parameter for the [WorkerEntrypoint](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc) or [DurableObject](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#call-rpc-methods) types. Using higher-order types, we automatically generate client-side stub types (e.g., forcing all methods to be async). [wrangler types](https://developers.cloudflare.com/workers/languages/typescript/#generate-types) also generates types for the `env` object. You can pass in the path to the config files of the Worker or Durable Object being called so that the generated types include the type parameters for the `Service` and `DurableObjectNamespace` types. For example, if your client Worker had bindings to a Worker in `../sum-worker/` and a Durable Object in `../counter/`, you should generate types for the client Worker's `env` by running: npmyarnpnpm ``` npx wrangler types -c ./client/wrangler.jsonc -c ../sum-worker/wrangler.jsonc -c ../counter/wrangler.jsonc ``` ``` yarn wrangler types -c ./client/wrangler.jsonc -c ../sum-worker/wrangler.jsonc -c ../counter/wrangler.jsonc ``` ``` pnpm wrangler types -c ./client/wrangler.jsonc -c ../sum-worker/wrangler.jsonc -c ../counter/wrangler.jsonc ``` This will produce a `worker-configuration.d.ts` file that includes: ```ts interface Env { SUM_SERVICE: Service; COUNTER_OBJECT: DurableObjectNamespace< import("../counter/src/index").Counter >; } ``` Now types for RPC method like the `env.SUM_SERVICE.sum` method will be exposed to the client Worker. ```ts export default { async fetch(req, env, ctx): Promise { const result = await env.SUM_SERVICE.sum(1, 2); return new Response(result.toString()); }, } satisfies ExportedHandler; ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/#page","headline":"Workers RPC — TypeScript · Cloudflare Workers docs","description":"How TypeScript types for your Worker or Durable Object's RPC methods are generated and exposed to clients","url":"https://developers.cloudflare.com/workers/runtime-apis/rpc/typescript/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Which properties are and are not exposed to clients that communicate with your Worker or Durable Object via RPC title: Visibility and Security Model image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Visibility and Security Model Last updated Jul 5, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/rpc/visibility/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Security Model The Workers RPC system is intended to allow safe communications between Workers that do not trust each other. The system does not allow either side of an RPC session to access arbitrary objects on the other side, much less invoke arbitrary code. Instead, each side can only invoke the objects and functions for which they have explicitly received stubs via previous calls. This security model is commonly known as Object Capabilities, or Capability-Based Security. Workers RPC is built on [Cap'n Proto RPC ↗](https://capnproto.org/rpc.html), which in turn is based on CapTP, the object transport protocol used by the [distributed programming language E ↗](https://www.crockford.com/ec/etut.html). ## Visibility of Methods and Properties ### Private properties [Private properties ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private%5Fproperties) of classes are not directly exposed over RPC. ### Arrow functions Arrow function expressions are not exposed over RPC because they are defined on class instances, not on the class prototype. They [should not be used as class methods ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow%5Ffunctions#cannot%5Fbe%5Fused%5Fas%5Fmethods) of `WorkerEntrypoint` classes. ```js import { WorkerEntrypoint } from "cloudflare:workers"; export default class extends WorkerEntrypoint { // add() is exposed over RPC. add(a, b) { return a + b; } // subtract() is NOT exposed over RPC. subtract = (a, b) => a - b; } ``` ### Class instance properties When you send an instance of an application-defined class, the recipient can only access methods and properties declared on the class, not properties of the instance. For example: ```js class Foo extends RpcTarget { constructor() { super(); // i CANNOT be accessed over RPC this.i = 0; // funcProp CANNOT be called over RPC this.funcProp = () => {} } // value CAN be accessed over RPC get value() { return this.i; } // method CAN be called over RPC method() {} } ``` This behavior is intentional — it is intended to protect you from accidentally exposing private class internals. Generally, instance properties should be declared private, [by prefixing them with # ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private%5Fproperties). However, private properties are a relatively new feature of JavaScript, and are not yet widely used in the ecosystem. Since the RPC interface between two of your Workers may be a security boundary, we need to be extra-careful, so instance properties are always private when communicating between Workers using RPC, whether or not they have the `#` prefix. You can always declare an explicit getter at the class level if you wish to expose the property, as shown above. These visibility rules apply only to objects that extend `RpcTarget`, `WorkerEntrypoint`, or `DurableObject`, and do not apply to plain objects. Plain objects are passed "by value", sending all of their "own" properties. ### "Own" properties of functions When you pass a function over RPC, the caller can access the "own" properties of the function object itself. ```js someRpcMethod() { let func = () => {}; func.prop = 123; // `prop` is visible over RPC return func; } ``` Such properties on a function are accessed asynchronously, like class properties of an RpcTarget. But, unlike the `RpcTarget` example above, the function's instance properties that are accessible to the caller. In practice, properties are rarely added to functions. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/rpc/visibility/#page","headline":"Workers RPC — Visibility and Security Model · Cloudflare Workers docs","description":"Which properties are and are not exposed to clients that communicate with your Worker or Durable Object via RPC","url":"https://developers.cloudflare.com/workers/runtime-apis/rpc/visibility/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-05","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the scheduler.wait() API to delay execution in Workers. title: Scheduler image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Scheduler Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/scheduler/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The `scheduler` global provides task scheduling APIs based on the [WICG Scheduling APIs proposal ↗](https://github.com/WICG/scheduling-apis). Workers currently implement the `scheduler.wait()` method. `scheduler.wait()` returns a Promise that resolves after a given number of milliseconds. It is an `await`\-able alternative to `setTimeout()` that does not require a callback. Like other [timers in Workers](https://developers.cloudflare.com/workers/runtime-apis/web-standards/#timers), `scheduler.wait()` does not advance during CPU execution when deployed to Cloudflare. This is a [security measure to mitigate against Spectre attacks](https://developers.cloudflare.com/workers/reference/security-model/#step-1-disallow-timers-and-multi-threading). In local development, timers advance regardless of whether I/O occurs. ## Syntax ```js await scheduler.wait(delay); await scheduler.wait(delay, options); ``` ## Parameters * `delay` number * The number of milliseconds to wait before the returned Promise resolves. * `options` object optional * Optional configuration for the wait operation. * `signal` AbortSignal optional * An [AbortSignal](https://developers.cloudflare.com/workers/runtime-apis/web-standards/#abortcontroller-and-abortsignal) that cancels the wait. When the signal is aborted, the returned Promise rejects with an `AbortError`. ## Return value A `Promise` that resolves after `delay` milliseconds. If an `AbortSignal` is provided and aborted before the delay elapses, the Promise rejects with an `AbortError`. ## Examples ### Basic delay Use `scheduler.wait()` to pause execution for a specified duration. ```js export default { async fetch(request) { // Wait for 1 second await scheduler.wait(1000); return new Response("Delayed response"); }, }; ``` ```ts export default { async fetch(request): Promise { // Wait for 1 second await scheduler.wait(1000); return new Response("Delayed response"); }, } satisfies ExportedHandler; ``` ### Retry with exponential backoff Use `scheduler.wait()` to implement a delay between retry attempts. This example uses exponential backoff with jitter. ```js async function fetchWithRetry(url, maxAttempts = 3) { const baseBackoffMs = 100; const maxBackoffMs = 10000; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fetch(url); } catch (err) { if (attempt + 1 >= maxAttempts) { throw err; } const backoffMs = Math.min( maxBackoffMs, baseBackoffMs * Math.random() * Math.pow(2, attempt), ); await scheduler.wait(backoffMs); } } throw new Error("unreachable"); } export default { async fetch(request) { const response = await fetchWithRetry("https://example.com/api"); return new Response(response.body, response); }, }; ``` ```ts async function fetchWithRetry(url: string, maxAttempts = 3): Promise { const baseBackoffMs = 100; const maxBackoffMs = 10000; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fetch(url); } catch (err) { if (attempt + 1 >= maxAttempts) { throw err; } const backoffMs = Math.min( maxBackoffMs, baseBackoffMs * Math.random() * Math.pow(2, attempt), ); await scheduler.wait(backoffMs); } } throw new Error("unreachable"); } export default { async fetch(request): Promise { const response = await fetchWithRetry("https://example.com/api"); return new Response(response.body, response); }, } satisfies ExportedHandler; ``` ### Cancel with AbortSignal Use an [AbortController](https://developers.cloudflare.com/workers/runtime-apis/web-standards/#abortcontroller-and-abortsignal) to cancel a pending wait. ```js export default { async fetch(request) { const controller = new AbortController(); // Cancel the wait after 500ms setTimeout(() => controller.abort(), 500); try { await scheduler.wait(5000, { signal: controller.signal }); return new Response("Wait completed"); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return new Response("Wait was cancelled", { status: 408 }); } throw err; } }, }; ``` ```ts export default { async fetch(request): Promise { const controller = new AbortController(); // Cancel the wait after 500ms setTimeout(() => controller.abort(), 500); try { await scheduler.wait(5000, { signal: controller.signal }); return new Response("Wait completed"); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return new Response("Wait was cancelled", { status: 408 }); } throw err; } }, } satisfies ExportedHandler; ``` ## Related resources * [Timers](https://developers.cloudflare.com/workers/runtime-apis/web-standards/#timers) — `setTimeout()` and `setInterval()` APIs * [Performance and timers](https://developers.cloudflare.com/workers/runtime-apis/performance/) — `performance.now()` and timer security behavior * [AbortController and AbortSignal](https://developers.cloudflare.com/workers/runtime-apis/web-standards/#abortcontroller-and-abortsignal) — cancel asynchronous operations * [WICG Scheduling APIs proposal ↗](https://github.com/WICG/scheduling-apis) — the specification this API is based on Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/scheduler/#page","headline":"Scheduler · Cloudflare Workers docs","description":"Use the scheduler.wait() API to delay execution in Workers.","url":"https://developers.cloudflare.com/workers/runtime-apis/scheduler/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: A web standard API that allows JavaScript to programmatically access and process streams of data. title: Streams image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Streams Last updated Jun 15, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The [Streams API ↗](https://developer.mozilla.org/en-US/docs/Web/API/Streams%5FAPI) is a web standard API that allows JavaScript to programmatically access and process streams of data. * [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/) * [ReadableStream BYOBReader](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/) * [ReadableStream DefaultReader](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreamdefaultreader/) * [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/) * [WritableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/) * [WritableStream DefaultWriter](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestreamdefaultwriter/) Use the Streams API to avoid buffering large requests or responses in memory. This enables you to parse extremely large request or response bodies within a Worker's 128 MB memory limit. This is faster than buffering the entire payload into memory, as your Worker can start processing data incrementally, and allows your Worker to handle multi-gigabyte payloads or files within its memory limits. Workers do not need to prepare an entire response body before returning a `Response`. You can use a [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/) to stream a response body after sending the response status line and headers. Note By default, Cloudflare Workers is capable of streaming responses using the [Streams APIs ↗](https://developer.mozilla.org/en-US/docs/Web/API/Streams%5FAPI). To maintain the streaming behavior, you should only modify the response body using the methods in the Streams APIs. If your Worker only forwards subrequest responses to the client verbatim without reading their body text, then its body handling is already optimal and you do not have to use these APIs. The worker can create a `Response` object using a `ReadableStream` as the body. Any data provided through the `ReadableStream` will be streamed to the client as it becomes available. ```js export default { async fetch(request, env, ctx) { // Fetch from origin server. const response = await fetch(request); // ... and deliver our Response while that’s running. return new Response(response.body, response); }, }; ``` Service Workers are deprecated Service Workers are deprecated, but still supported. We recommend using [Module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) instead. New features may not be supported for Service Workers. ```js addEventListener("fetch", (event) => { event.respondWith(fetchAndStream(event.request)); }); async function fetchAndStream(request) { // Fetch from origin server. const response = await fetch(request); // ... and deliver our Response while that’s running. return new Response(readable.body, response); } ``` ```python from workers import WorkerEntrypoint, Response, fetch class Default(WorkerEntrypoint): async def fetch(self, request): # Fetch from origin server. response = await fetch(request) # Stream the response body to the client. return Response(response.body, headers=response.headers) ``` A [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/) and the [ReadableStream.pipeTo()](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/#methods) method can be used to modify the response body as it is being streamed: ```js export default { async fetch(request, env, ctx) { // Fetch from origin server. const response = await fetch(request); const { readable, writable } = new TransformStream({ transform(chunk, controller) { controller.enqueue(modifyChunkSomehow(chunk)); }, }); // Start pumping the body. NOTE: No await! response.body.pipeTo(writable); // ... and deliver our Response while that’s running. return new Response(readable, response); }, }; ``` Service Workers are deprecated Service Workers are deprecated, but still supported. We recommend using [Module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) instead. New features may not be supported for Service Workers. ```js addEventListener("fetch", (event) => { event.respondWith(fetchAndStream(event.request)); }); async function fetchAndStream(request) { // Fetch from origin server. const response = await fetch(request); const { readable, writable } = new TransformStream({ transform(chunk, controller) { controller.enqueue(modifyChunkSomehow(chunk)); }, }); // Start pumping the body. NOTE: No await! response.body.pipeTo(writable); // ... and deliver our Response while that’s running. return new Response(readable, response); } ``` ```python from workers import WorkerEntrypoint, Response from js import ReadableStream, TextEncoder from pyodide.ffi import create_proxy, to_js import asyncio class Default(WorkerEntrypoint): async def fetch(self, request): enc = TextEncoder.new() async def start(controller): for i in range(5): controller.enqueue(enc.encode(f"chunk {i}\n")) await asyncio.sleep(0.1) controller.close() stream = ReadableStream.new( to_js({"start": create_proxy(start)}) ) return Response(stream, headers={"Content-Type": "text/plain"}) ``` This example calls `response.body.pipeTo(writable)` but does not `await` it. This is so it does not block the forward progress of the remainder of the `fetchAndStream()` function. It continues to run asynchronously until the response is complete or the client disconnects. The runtime can continue running a function (`response.body.pipeTo(writable)`) after a response is returned to the client. This example pumps the subrequest response body to the final response body. However, you can use more complicated logic, such as adding a prefix or a suffix to the body or to process it somehow. --- ## Common issues Warning The Streams API is only available inside of the [Request context](https://developers.cloudflare.com/workers/runtime-apis/request/), inside the `fetch` event listener callback. --- ## Related resources * [Stream large JSON](https://developers.cloudflare.com/workers/examples/streaming-json/) \- Parse and transform large JSON request and response bodies * [MDN's Streams API documentation ↗](https://developer.mozilla.org/en-US/docs/Web/API/Streams%5FAPI) * [Streams API spec ↗](https://streams.spec.whatwg.org/) * Write your Worker code in [ES modules syntax](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/) for an optimized experience. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/#page","headline":"Streams - Runtime APIs · Cloudflare Workers docs","description":"A web standard API that allows JavaScript to programmatically access and process streams of data.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-15","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn about the ReadableStream API for reading streamed data in Cloudflare Workers. title: ReadableStream image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # ReadableStream Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background A `ReadableStream` is returned by the `readable` property inside [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/). ## Properties * `locked` boolean * A Boolean value that indicates if the readable stream is locked to a reader. ## Methods * `pipeTo(destinationWritableStream, optionsPipeToOptions)` : Promise * Pipes the readable stream to a given writable stream `destination` and returns a promise that is fulfilled when the `write` operation succeeds or rejects it if the operation fails. * `getReader(optionsObject)` : ReadableStreamDefaultReader * Gets an instance of `ReadableStreamDefaultReader` and locks the `ReadableStream` to that reader instance. This method accepts an object argument indicating options. The only supported option is `mode`, which can be set to `byob` to create a [ReadableStreamBYOBReader](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/), as shown here: ```js let reader = readable.getReader({ mode: 'byob' }); ``` ### `PipeToOptions` * `preventClose` bool * When `true`, closure of the source `ReadableStream` will not cause the destination `WritableStream` to be closed. * `preventAbort` bool * When `true`, errors in the source `ReadableStream` will no longer abort the destination `WritableStream`. `pipeTo` will return a rejected promise with the error from the source or any error that occurred while aborting the destination. --- ## Related resources * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [Readable streams in the WHATWG Streams API specification ↗](https://streams.spec.whatwg.org/#rs-model) * [MDN’s ReadableStream documentation ↗](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/#page","headline":"ReadableStream · Cloudflare Workers docs","description":"Learn about the ReadableStream API for reading streamed data in Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use ReadableStreamBYOBReader in Workers to read streamed data into your own buffer. title: ReadableStream BYOBReader image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # ReadableStream BYOBReader Last updated May 1, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background `BYOB` is an abbreviation of bring your own buffer. A `ReadableStreamBYOBReader` allows reading into a developer-supplied buffer, thus minimizing copies. An instance of `ReadableStreamBYOBReader` is functionally identical to [ReadableStreamDefaultReader](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreamdefaultreader/) with the exception of the `read` method. A `ReadableStreamBYOBReader` is not instantiated via its constructor. Rather, it is retrieved from a [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/): ```js const { readable, writable } = new TransformStream(); const reader = readable.getReader({ mode: 'byob' }); ``` --- ## Methods * `read(bufferArrayBufferView)` : Promise * Returns a promise with the next available chunk of data read into a passed-in buffer. * `readAtLeast(minElements, bufferArrayBufferView)` : Promise * Returns a promise with the next available chunk of data read into a passed-in buffer. The promise will not resolve until at least `minElements` elements have been read. The element size is determined by `bufferArrayBufferView`, for example 4 bytes per element for a `Uint32Array`. However, fewer than `minElements` elements may be returned if the end of the stream is reached or the underlying stream is closed. Specifically: * If `minElements` or more elements are available, the promise resolves with `{ value: , done: false }`. * If the stream ends after some data has been read but fewer than `minElements` elements, the promise resolves with the partial data: `{ value: , done: false }`. The next call to `read` or `readAtLeast` will then return `{ value: undefined, done: true }`. * If the stream ends with zero bytes available (that is, the stream is already at EOF), the promise resolves with `{ value: , done: true }`. * If the stream errors, the promise rejects. * `minElements` must be at least 1, and `minElements * elementSize` must not exceed the byte length of `bufferArrayBufferView`, or the promise rejects with a `TypeError`. For a `Uint8Array`, element size is 1, so `minElements` is effectively a byte count. --- ## Common issues Warning `read` provides no control over the minimum number of bytes that should be read into the buffer. Even if you allocate a 1 MiB buffer, the kernel is perfectly within its rights to fulfill this read with a single byte, whether or not an EOF immediately follows. In practice, the Workers team has found that `read` typically fills only 1% of the provided buffer. `readAtLeast` is a non-standard extension to the Streams API which allows users to specify that at least `minElements` elements must be read into the buffer before resolving the read. For a `Uint8Array` (the most common case), each element is one byte, so `minElements` is effectively a byte count. If the stream ends before `minElements` elements are available, the partial data that was read is still returned rather than throwing an error — refer to the [readAtLeast method documentation above](#methods) for the full details. --- ## Related resources * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [Background about BYOB readers in the Streams API WHATWG specification ↗](https://streams.spec.whatwg.org/#byob-readers) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/#page","headline":"ReadableStreamBYOBReader · Cloudflare Workers docs","description":"Use ReadableStreamBYOBReader in Workers to read streamed data into your own buffer.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreambyobreader/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-05-01","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use ReadableStreamDefaultReader in Workers to read chunks from a ReadableStream. title: ReadableStream DefaultReader image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # ReadableStream DefaultReader Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreamdefaultreader/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background A reader is used when you want to read from a [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/), rather than piping its output to a [WritableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/). A `ReadableStreamDefaultReader` is not instantiated via its constructor. Rather, it is retrieved from a [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/): ```js const { readable, writable } = new TransformStream(); const reader = readable.getReader(); ``` --- ## Properties * `reader.closed` : Promise * A promise indicating if the reader is closed. The promise is fulfilled when the reader stream closes and is rejected if there is an error in the stream. ## Methods * `read()` : Promise * A promise that returns the next available chunk of data being passed through the reader queue. * `cancel(reasonstringoptional)` : void * Cancels the stream. `reason` is an optional human-readable string indicating the reason for cancellation. `reason` will be passed to the underlying source’s cancel algorithm -- if this readable stream is one side of a [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/), then its cancel algorithm causes the transform’s writable side to become errored with `reason`. Warning Any data not yet read is lost. * `releaseLock()` : void * Releases the lock on the readable stream. A lock cannot be released if the reader has pending read operations. A `TypeError` is thrown and the reader remains locked. --- ## Related resources * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [Readable streams in the WHATWG Streams API specification ↗](https://streams.spec.whatwg.org/#rs-model) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreamdefaultreader/#page","headline":"ReadableStreamDefaultReader · Cloudflare Workers docs","description":"Use ReadableStreamDefaultReader in Workers to read chunks from a ReadableStream.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/readablestreamdefaultreader/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the TransformStream API in Workers to pipe data between readable and writable streams. title: TransformStream image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # TransformStream Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background A transform stream consists of a pair of streams: a writable stream, known as its writable side, and a readable stream, known as its readable side. Writes to the writable side result in new data being made available for reading from the readable side. Workers currently only implements an identity transform stream, a type of transform stream which forwards all chunks written to its writable side to its readable side, without any changes. --- ## Constructor ```js let { readable, writable } = new TransformStream(); ``` * `TransformStream()` TransformStream * Returns a new identity transform stream. ## Properties * `readable` ReadableStream * An instance of a `ReadableStream`. * `writable` WritableStream * An instance of a `WritableStream`. --- ## `IdentityTransformStream` The current implementation of `TransformStream` in the Workers platform is not current compliant with the [Streams Standard ↗](https://streams.spec.whatwg.org/#transform-stream) and we will soon be making changes to the implementation to make it conform with the specification. In preparation for doing so, we have introduced the `IdentityTransformStream` class that implements behavior identical to the current `TransformStream` class. This type of stream forwards all chunks of byte data (in the form of `TypedArray`s) written to its writable side to its readable side, without any changes. The `IdentityTransformStream` readable side supports [bring your own buffer (BYOB) reads ↗](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader). ### Constructor ```js let { readable, writable } = new IdentityTransformStream(); ``` * `IdentityTransformStream()` IdentityTransformStream * Returns a new identity transform stream. ### Properties * `readable` ReadableStream * An instance of a `ReadableStream`. * `writable` WritableStream * An instance of a `WritableStream`. --- ## `FixedLengthStream` The `FixedLengthStream` is a specialization of `IdentityTransformStream` that limits the total number of bytes that the stream will passthrough. It is useful primarily because, when using `FixedLengthStream` to produce either a `Response` or `Request`, the fixed length of the stream will be used as the `Content-Length` header value as opposed to use chunked encoding when using any other type of stream. An error will occur if too many, or too few bytes are written through the stream. ### Constructor ```js let { readable, writable } = new FixedLengthStream(1000); ``` * `FixedLengthStream(length)` FixedLengthStream * Returns a new identity transform stream. * `length` maybe a `number` or `bigint` with a maximum value of `2^53 - 1`. ### Properties * `readable` ReadableStream * An instance of a `ReadableStream`. * `writable` WritableStream * An instance of a `WritableStream`. --- ## Related resources * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [Transform Streams in the WHATWG Streams API specification ↗](https://streams.spec.whatwg.org/#transform-stream) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/#page","headline":"TransformStream · Cloudflare Workers docs","description":"Use the TransformStream API in Workers to pipe data between readable and writable streams.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the WritableStream API in Workers to write data to a stream destination. title: WritableStream image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # WritableStream Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background A `WritableStream` is the `writable` property of a [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/). On the Workers platform, `WritableStream` cannot be directly created using the `WritableStream` constructor. A typical way to write to a `WritableStream` is to pipe a [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/) to it. ```js readableStream .pipeTo(writableStream) .then(() => console.log('All data successfully written!')) .catch(e => console.error('Something went wrong!', e)); ``` To write to a `WritableStream` directly, you must use its writer. ```js const writer = writableStream.getWriter(); writer.write(data); ``` Refer to the [WritableStreamDefaultWriter](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestreamdefaultwriter/) documentation for further detail. ## Properties * `locked` boolean * A Boolean value to indicate if the writable stream is locked to a writer. ## Methods * `abort(reasonstringoptional)` : Promise * Aborts the stream. This method returns a promise that fulfills with a response `undefined`. `reason` is an optional human-readable string indicating the reason for cancellation. `reason` will be passed to the underlying sink’s abort algorithm. If this writable stream is one side of a [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/), then its abort algorithm causes the transform’s readable side to become errored with `reason`. Warning Any data not yet written is lost upon abort. * `getWriter()` : WritableStreamDefaultWriter * Gets an instance of `WritableStreamDefaultWriter` and locks the `WritableStream` to that writer instance. --- ## Related resources * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [Writable streams in the WHATWG Streams API specification ↗](https://streams.spec.whatwg.org/#ws-model) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/#page","headline":"WritableStream · Cloudflare Workers docs","description":"Use the WritableStream API in Workers to write data to a stream destination.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use WritableStreamDefaultWriter in Workers to write data directly to a WritableStream. title: WritableStream DefaultWriter image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # WritableStream DefaultWriter Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestreamdefaultwriter/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background A writer is used when you want to write directly to a [WritableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/), rather than piping data to it from a [ReadableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/). For example: ```js function writeArrayToStream(array, writableStream) { const writer = writableStream.getWriter(); array.forEach(chunk => writer.write(chunk).catch(() => {})); return writer.close(); } writeArrayToStream([1, 2, 3, 4, 5], writableStream) .then(() => console.log('All done!')) .catch(e => console.error('Error with the stream: ' + e)); ``` ## Properties * `writer.desiredSize` int * The size needed to fill the stream’s internal queue, as an integer. Always returns 1, 0 (if the stream is closed), or `null` (if the stream has errors). * `writer.closed` Promise * A promise that indicates if the writer is closed. The promise is fulfilled when the writer stream is closed and rejected if there is an error in the stream. ## Methods * `abort(reasonstringoptional)` : Promise * Aborts the stream. This method returns a promise that fulfills with a response `undefined`. `reason` is an optional human-readable string indicating the reason for cancellation. `reason` will be passed to the underlying sink’s abort algorithm. If this writable stream is one side of a [TransformStream](https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/), then its abort algorithm causes the transform’s readable side to become errored with `reason`. Warning Any data not yet written is lost upon abort. * `close()` : Promise * Attempts to close the writer. Remaining writes finish processing before the writer is closed. This method returns a promise fulfilled with `undefined` if the writer successfully closes and processes the remaining writes, or rejected on any error. * `releaseLock()` : void * Releases the writer’s lock on the stream. Once released, the writer is no longer active. You can call this method before all pending `write(chunk)` calls are resolved. This allows you to queue a `write` operation, release the lock, and begin piping into the writable stream from another source, as shown in the example below. ```js let writer = writable.getWriter(); // Write a preamble. writer.write(new TextEncoder().encode('foo bar')); // While that’s still writing, pipe the rest of the body from somewhere else. writer.releaseLock(); await someResponse.body.pipeTo(writable); ``` * `write(chunkany)` : Promise * Writes a chunk of data to the writer and returns a promise that resolves if the operation succeeds. * The underlying stream may accept fewer kinds of type than `any`, it will throw an exception when encountering an unexpected type. --- ## Related resources * [Streams](https://developers.cloudflare.com/workers/runtime-apis/streams/) * [Writable streams in the WHATWG Streams API specification ↗](https://streams.spec.whatwg.org/#ws-model) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/streams/writablestreamdefaultwriter/#page","headline":"WritableStreamDefaultWriter · Cloudflare Workers docs","description":"Use WritableStreamDefaultWriter in Workers to write data directly to a WritableStream.","url":"https://developers.cloudflare.com/workers/runtime-apis/streams/writablestreamdefaultwriter/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Use the `connect()` API to create outbound TCP connections from Workers. title: TCP sockets image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # TCP sockets Last updated Jun 19, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) The Workers runtime provides the `connect()` API for creating outbound [TCP connections ↗](https://www.cloudflare.com/learning/ddos/glossary/tcp-ip/) from Workers. Many application-layer protocols are built on top of the Transmission Control Protocol (TCP). These application-layer protocols, including SSH, MQTT, SMTP, FTP, IRC, and most database wire protocols including MySQL, PostgreSQL, MongoDB, require an underlying TCP socket API in order to work. Note Connecting to a PostgreSQL database? You should use [Hyperdrive](https://developers.cloudflare.com/hyperdrive/), which provides the `connect()` API with built-in connection pooling and query caching. Note TCP Workers outbound connections are sourced from a prefix that is not part of [list of IP ranges ↗](https://www.cloudflare.com/ips/). ## `connect()` The `connect()` function returns a TCP socket, with both a [readable](https://developers.cloudflare.com/workers/runtime-apis/streams/readablestream/) and [writable](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/) stream of data. This allows you to read and write data on an ongoing basis, as long as the connection remains open. `connect()` is provided as a [Runtime API](https://developers.cloudflare.com/workers/runtime-apis/), and is accessed by importing the `connect` function from `cloudflare:sockets`. This process is similar to how one imports built-in modules in Node.js. Refer to the following codeblock for an example of creating a TCP socket, writing to it, and returning the readable side of the socket as a response: ```typescript import { connect } from 'cloudflare:sockets'; export default { async fetch(req): Promise { const gopherAddr = { hostname: "gopher.floodgap.com", port: 70 }; const url = new URL(req.url); try { const socket = connect(gopherAddr); const writer = socket.writable.getWriter() const encoder = new TextEncoder(); const encoded = encoder.encode(url.pathname + "\r\n"); await writer.write(encoded); await writer.close(); return new Response(socket.readable, { headers: { "Content-Type": "text/plain" } }); } catch (error) { return new Response("Socket connection failed: " + error, { status: 500 }); } } } satisfies ExportedHandler; ``` * `connect(address: SocketAddress | string, options?: optional SocketOptions)` : `Socket` * `connect()` accepts either a URL string or [SocketAddress](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socketaddress) to define the hostname and port number to connect to, and an optional configuration object, [SocketOptions](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socketoptions). It returns an instance of a [Socket](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socket). ### `SocketAddress` * `hostname` string * The hostname to connect to. Example: `cloudflare.com`. * `port` number * The port number to connect to. Example: `5432`. ### `SocketOptions` * `secureTransport` "off" | "on" | "starttls" — Defaults to `off` * Specifies whether or not to use [TLS ↗](https://www.cloudflare.com/learning/ssl/transport-layer-security-tls/) when creating the TCP socket. * `off` — Do not use TLS. * `on` — Use TLS. * `starttls` — Do not use TLS initially, but allow the socket to be upgraded to use TLS by calling [startTls()](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#opportunistic-tls-starttls). * `allowHalfOpen` boolean — Defaults to `false` * Defines whether the writable side of the TCP socket will automatically close on end-of-file (EOF). When set to `false`, the writable side of the TCP socket will automatically close on EOF. When set to `true`, the writable side of the TCP socket will remain open on EOF. * This option is similar to that offered by the Node.js [net module ↗](https://nodejs.org/api/net.html) and allows interoperability with code which utilizes it. ### `SocketInfo` * `remoteAddress` string | null * The address of the remote peer the socket is connected to. May not always be set. * `localAddress` string | null * The address of the local network endpoint for this socket. May not always be set. ### `Socket` * `readable` : ReadableStream * Returns the readable side of the TCP socket. * `writable` : WritableStream * Returns the writable side of the TCP socket. * The `WritableStream` returned only accepts chunks of `Uint8Array` or its views. * `opened` `Promise` * This promise is resolved when the socket connection is established and is rejected if the socket encounters an error. * `closed` `Promise` * This promise is resolved when the socket is closed and is rejected if the socket encounters an error. * `close()` `Promise` * Closes the TCP socket. Both the readable and writable streams are forcibly closed. * `startTls()` : Socket * Upgrades an insecure socket to a secure one that uses TLS, returning a new [Socket](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets#socket). Note that in order to call `startTls()`, you must set [secureTransport](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#socketoptions) to `starttls` when initially calling `connect()` to create the socket. ## Opportunistic TLS (StartTLS) Many TCP-based systems, including databases and email servers, require that clients use opportunistic TLS (otherwise known as [StartTLS ↗](https://en.wikipedia.org/wiki/Opportunistic%5FTLS)) when connecting. In this pattern, the client first creates an insecure TCP socket, without TLS, and then upgrades it to a secure TCP socket, that uses TLS. The `connect()` API simplifies this by providing a method, `startTls()`, which returns a new `Socket` instance that uses TLS: ```typescript import { connect } from "cloudflare:sockets" const address = { hostname: "example-postgres-db.com", port: 5432 }; const socket = connect(address, { secureTransport: "starttls" }); const secureSocket = socket.startTls(); ``` * `startTls()` can only be called if `secureTransport` is set to `starttls` when creating the initial TCP socket. * Once `startTls()` is called, the initial socket is closed and can no longer be read from or written to. In the example above, anytime after `startTls()` is called, you would use the newly created `secureSocket`. Any existing readers and writers based off the original socket will no longer work. You must create new readers and writers from the newly created `secureSocket`. * `startTls()` should only be called once on an existing socket. ## Handle errors To handle errors when creating a new TCP socket, reading from a socket, or writing to a socket, wrap these calls inside [try...catch ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) statement blocks. The following example opens a connection to Google.com, initiates a HTTP request, and returns the response. If this fails and throws an exception, it returns a [500](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-500/) response: ```typescript import { connect } from 'cloudflare:sockets'; const connectionUrl = { hostname: "google.com", port: 80 }; export interface Env { } export default { async fetch(req, env, ctx): Promise { try { const socket = connect(connectionUrl); const writer = socket.writable.getWriter(); const encoder = new TextEncoder(); const encoded = encoder.encode("GET / HTTP/1.0\r\n\r\n"); await writer.write(encoded); await writer.close(); return new Response(socket.readable, { headers: { "Content-Type": "text/plain" } }); } catch (error) { return new Response(`Socket connection failed: ${error}`, { status: 500 }); } } } satisfies ExportedHandler; ``` ## Close TCP connections You can close a TCP connection by calling `close()` on the socket. This will close both the readable and writable sides of the socket. ```typescript import { connect } from "cloudflare:sockets" const socket = connect({ hostname: "my-url.com", port: 70 }); const reader = socket.readable.getReader(); socket.close(); // After close() is called, you can no longer read from the readable side of the socket const reader = socket.readable.getReader(); // This fails ``` ## Considerations * Outbound TCP sockets to [Cloudflare IP ranges ↗](https://www.cloudflare.com/ips/) are blocked. * TCP sockets cannot be created in global scope and shared across requests. You should always create TCP sockets within a handler (ex: [fetch()](https://developers.cloudflare.com/workers/get-started/guide/#3-write-code), [scheduled()](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/), [queue()](https://developers.cloudflare.com/queues/configuration/javascript-apis/#consumer)) or [alarm()](https://developers.cloudflare.com/durable-objects/api/alarms/). * Each open TCP socket counts towards the maximum number of [open connections](https://developers.cloudflare.com/workers/platform/limits/#simultaneous-open-connections) that can be simultaneously open. * When created from within a Durable Object, an open TCP socket keeps the Durable Object in memory and causes it to incur duration charges for up to 15 minutes per connection. After 15 minutes, the socket stops keeping the Durable Object alive (the socket itself continues operating) and the [standard eviction rules](https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/) resume. * By default, Workers cannot create outbound TCP connections on port `25` to send email to SMTP mail servers. [Cloudflare Email Workers](https://developers.cloudflare.com/email-service/api/route-emails/) provides APIs to process and forward email. * Support for handling inbound TCP connections is [coming soon ↗](https://blog.cloudflare.com/workers-tcp-socket-api-connect-databases/). Currently, it is not possible to make an inbound TCP connection to your Worker, for example, by using the `CONNECT` HTTP method. ## Troubleshooting Review descriptions of common error messages you may see when working with TCP Sockets, what the error messages mean, and how to solve them. ### `proxy request failed, cannot connect to the specified address` Your socket is connecting to an address that was disallowed. Examples of a disallowed address include Cloudflare IPs, `localhost`, and private network IPs. If you need to connect to addresses on port `80` or `443` to make HTTP requests, use [fetch](https://developers.cloudflare.com/workers/runtime-apis/fetch/). ### `TCP Loop detected` Your socket is connecting back to the Worker that initiated the outbound connection. In other words, the Worker is connecting back to itself. This is currently not supported. ### `Connections to port 25 are prohibited` Your socket is connecting to an address on port `25`. This is usually the port used for SMTP mail servers. Workers cannot create outbound connections on port `25`. Consider using [Cloudflare Email Workers](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/) instead. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#page","headline":"TCP sockets · Cloudflare Workers docs","description":"Use the connect() API to create outbound TCP connections from Workers.","url":"https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-19","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: A set of low-level functions for common cryptographic tasks. title: Web Crypto image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Web Crypto Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background The Web Crypto API provides a set of low-level functions for common cryptographic tasks. The Workers runtime implements the full surface of this API, but with some differences in the [supported algorithms](#supported-algorithms) compared to those implemented in most browsers. Performing cryptographic operations using the Web Crypto API is significantly faster than performing them purely in JavaScript. If you want to perform CPU-intensive cryptographic operations, you should consider using the Web Crypto API. The Web Crypto API is implemented through the `SubtleCrypto` interface, accessible via the global `crypto.subtle` binding. A simple example of calculating a digest (also known as a hash) is: ```js const myText = new TextEncoder().encode('Hello world!'); const myDigest = await crypto.subtle.digest( { name: 'SHA-256', }, myText // The data you want to hash as an ArrayBuffer ); console.log(new Uint8Array(myDigest)); ``` Some common uses include [signing requests](https://developers.cloudflare.com/workers/examples/signing-requests/). Caution The Web Crypto API differs significantly from the [Node.js Crypto API](https://developers.cloudflare.com/workers/runtime-apis/nodejs/crypto/). If you are working with code that relies on the Node.js Crypto API, you can use it by enabling the [nodejs\_compat compatibility flag](https://developers.cloudflare.com/workers/runtime-apis/nodejs/). --- ## Constructors * `crypto.DigestStream(algorithm)` DigestStream * A non-standard extension to the `crypto` API that supports generating a hash digest from streaming data. The `DigestStream` itself is a [WritableStream](https://developers.cloudflare.com/workers/runtime-apis/streams/writablestream/) that does not retain the data written into it. Instead, it generates a hash digest automatically when the flow of data has ended. ### Parameters * `algorithm`string | object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Syntax). ### Usage ```js export default { async fetch(req) { // Fetch from origin const res = await fetch(req); // We need to read the body twice so we `tee` it (get two instances) const [bodyOne, bodyTwo] = res.body.tee(); // Make a new response so we can set the headers (responses from `fetch` are immutable) const newRes = new Response(bodyOne, res); // Create a SHA-256 digest stream and pipe the body into it const digestStream = new crypto.DigestStream("SHA-256"); bodyTwo.pipeTo(digestStream); // Get the final result const digest = await digestStream.digest; // Turn it into a hex string const hexString = [...new Uint8Array(digest)] .map(b => b.toString(16).padStart(2, '0')) .join('') // Set a header with the SHA-256 hash and return the response newRes.headers.set("x-content-digest", `SHA-256=${hexString}`); return newRes; } } ``` ```ts export default { async fetch(req): Promise { // Fetch from origin const res = await fetch(req); // We need to read the body twice so we `tee` it (get two instances) const [bodyOne, bodyTwo] = res.body.tee(); // Make a new response so we can set the headers (responses from `fetch` are immutable) const newRes = new Response(bodyOne, res); // Create a SHA-256 digest stream and pipe the body into it const digestStream = new crypto.DigestStream("SHA-256"); bodyTwo.pipeTo(digestStream); // Get the final result const digest = await digestStream.digest; // Turn it into a hex string const hexString = [...new Uint8Array(digest)] .map(b => b.toString(16).padStart(2, '0')) .join('') // Set a header with the SHA-256 hash and return the response newRes.headers.set("x-content-digest", `SHA-256=${hexString}`); return newRes; } } satisfies ExportedHandler; ``` ## Methods * `crypto.randomUUID()` : string * Generates a new random (version 4) UUID as defined in [RFC 4122 ↗](https://www.rfc-editor.org/rfc/rfc4122.txt). * `crypto.getRandomValues(bufferArrayBufferView)` : ArrayBufferView * Fills the passed `ArrayBufferView` with cryptographically sound random values and returns the `buffer`. ### Parameters * `buffer`ArrayBufferView * Must be an Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array. ## SubtleCrypto Methods These methods are all accessed via [crypto.subtle ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto#Methods), which is also documented in detail on MDN. ### encrypt * `encrypt(algorithm, key, data)` : Promise * Returns a Promise that fulfills with the encrypted data corresponding to the clear text, algorithm, and key given as parameters. #### Parameters * `algorithm`object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#Syntax). * `key`CryptoKey * `data`BufferSource ### decrypt * `decrypt(algorithm, key, data)` : Promise * Returns a Promise that fulfills with the clear data corresponding to the ciphertext, algorithm, and key given as parameters. #### Parameters * `algorithm`object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/decrypt#Syntax). * `key`CryptoKey * `data`BufferSource ### sign * `sign(algorithm, key, data)` : Promise * Returns a Promise that fulfills with the signature corresponding to the text, algorithm, and key given as parameters. #### Parameters * `algorithm`string | object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#Syntax). * `key`CryptoKey * `data`ArrayBuffer ### verify * `verify(algorithm, key, signature, data)` : Promise * Returns a Promise that fulfills with a Boolean value indicating if the signature given as a parameter matches the text, algorithm, and key that are also given as parameters. #### Parameters * `algorithm`string | object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify#Syntax). * `key`CryptoKey * `signature`ArrayBuffer * `data`ArrayBuffer ### digest * `digest(algorithm, data)` : Promise * Returns a Promise that fulfills with a digest generated from the algorithm and text given as parameters. #### Parameters * `algorithm`string | object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#Syntax). * `data`ArrayBuffer ### generateKey * `generateKey(algorithm, extractable, keyUsages)` : Promise | Promise * Returns a Promise that fulfills with a newly-generated `CryptoKey`, for symmetrical algorithms, or a `CryptoKeyPair`, containing two newly generated keys, for asymmetrical algorithms. For example, to generate a new AES-GCM key: ```js let keyPair = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt'] ); ``` #### Parameters * `algorithm`object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey#Syntax). * `extractable`bool * `keyUsages`Array * An Array of strings indicating the [possible usages of the new key ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey#Syntax). ### deriveKey * `deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)` : Promise * Returns a Promise that fulfills with a newly generated `CryptoKey` derived from the base key and specific algorithm given as parameters. #### Parameters * `algorithm`object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#Syntax). * `baseKeyCryptoKey` * `derivedKeyAlgorithmobject` * Defines the algorithm the derived key will be used for in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#Syntax). * `extractablebool` * `keyUsagesArray` * An Array of strings indicating the [possible usages of the new key ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#Syntax) ### deriveBits * `deriveBits(algorithm, baseKey, length)` : Promise * Returns a Promise that fulfills with a newly generated buffer of pseudo-random bits derived from the base key and specific algorithm given as parameters. It returns a Promise which will be fulfilled with an `ArrayBuffer` containing the derived bits. This method is very similar to `deriveKey()`, except that `deriveKey()` returns a `CryptoKey` object rather than an `ArrayBuffer`. Essentially, `deriveKey()` is composed of `deriveBits()` followed by `importKey()`. #### Parameters * `algorithm`object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits#Syntax). * `baseKey`CryptoKey * `length`int * Length of the bit string to derive. ### importKey * `importKey(format, keyData, algorithm, extractable, keyUsages)` : Promise * Transform a key from some external, portable format into a `CryptoKey` for use with the Web Crypto API. #### Parameters * `format`string * Describes [the format of the key to be imported ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#Syntax). * `keyData`ArrayBuffer * `algorithm`object * Describes the algorithm to be used, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#Syntax). * `extractable`bool * `keyUsages`Array * An Array of strings indicating the [possible usages of the new key ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#Syntax) ### exportKey * `exportKey(formatstring, keyCryptoKey)` : Promise * Transform a `CryptoKey` into a portable format, if the `CryptoKey` is `extractable`. #### Parameters * `format`string * Describes the [format in which the key will be exported ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey#Syntax). * `key`CryptoKey ### wrapKey * `wrapKey(format, key, wrappingKey, wrapAlgo)` : Promise * Transform a `CryptoKey` into a portable format, and then encrypt it with another key. This renders the `CryptoKey` suitable for storage or transmission in untrusted environments. #### Parameters * `format`string * Describes the [format in which the key will be exported ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/wrapKey#Syntax) before being encrypted. * `key`CryptoKey * `wrappingKey`CryptoKey * `wrapAlgo`object * Describes the algorithm to be used to encrypt the exported key, including any required parameters, in [an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/wrapKey#Syntax). ### unwrapKey * `unwrapKey(format, key, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)` : Promise * Transform a key that was wrapped by `wrapKey()` back into a `CryptoKey`. #### Parameters * `format`string * Described the [data format of the key to be unwrapped ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/unwrapKey#Syntax). * `key`CryptoKey * `unwrappingKey`CryptoKey * `unwrapAlgo`object * Describes the algorithm that was used to encrypt the wrapped key, [in an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/unwrapKey#Syntax). * `unwrappedKeyAlgo`object * Describes the key to be unwrapped, [in an algorithm-specific format ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/unwrapKey#Syntax). * `extractable`bool * `keyUsages`Array * An Array of strings indicating the [possible usages of the new key ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/unwrapKey#Syntax) ### timingSafeEqual * `timingSafeEqual(a, b)` : bool * Compare two buffers in a way that is resistant to timing attacks. This is a non-standard extension to the Web Crypto API. #### Parameters * `a`ArrayBuffer | TypedArray * `b`ArrayBuffer | TypedArray ### Supported algorithms Workers implements all operations of the [WebCrypto standard ↗](https://www.w3.org/TR/WebCryptoAPI/), as shown in the following table. A checkmark (✓) indicates that this feature is believed to be fully supported according to the spec. An x (✘) indicates that this feature is part of the specification but not implemented. If a feature only implements the operation partially, details are listed. | Algorithm | sign()verify() | encrypt()decrypt() | digest() | deriveBits()deriveKey() | generateKey() | wrapKey()unwrapKey() | exportKey() | importKey() | | ---------------------------- | -------------- | ------------------ | -------- | ----------------------- | ------------- | -------------------- | ----------- | ----------- | | RSASSA PKCS1 v1.5 | ✓ | | | | ✓ | | ✓ | ✓ | | RSA PSS | ✓ | | | | ✓ | | ✓ | ✓ | | RSA OAEP | | ✓ | | | ✓ | ✓ | ✓ | ✓ | | ECDSA | ✓ | | | | ✓ | | ✓ | ✓ | | ECDH | | | | ✓ | ✓ | | ✓ | ✓ | | Ed25519[1](#footnote-1) | ✓ | | | | ✓ | | ✓ | ✓ | | X25519[1](#footnote-1) | | | | ✓ | ✓ | | ✓ | ✓ | | NODE ED25519[2](#footnote-2) | ✓ | | | | ✓ | | ✓ | ✓ | | AES CTR | | ✓ | | | ✓ | ✓ | ✓ | ✓ | | AES CBC | | ✓ | | | ✓ | ✓ | ✓ | ✓ | | AES GCM | | ✓ | | | ✓ | ✓ | ✓ | ✓ | | AES KW | | | | | ✓ | ✓ | ✓ | ✓ | | HMAC | ✓ | | | | ✓ | | ✓ | ✓ | | SHA 1 | | | ✓ | | | | | | | SHA 256 | | | ✓ | | | | | | | SHA 384 | | | ✓ | | | | | | | SHA 512 | | | ✓ | | | | | | | MD5[3](#footnote-3) | | | ✓ | | | | | | | HKDF | | | | ✓ | | | | ✓ | | PBKDF2 | | | | ✓ | | | | ✓ | **Footnotes:** 1. Algorithms as specified in the [Secure Curves API ↗](https://wicg.github.io/webcrypto-secure-curves). 2. Legacy non-standard EdDSA is supported for the Ed25519 curve in addition to the Secure Curves version. Since this algorithm is non-standard, note the following while using it: * Use `NODE-ED25519` as the algorithm and `namedCurve` parameters. * Unlike NodeJS, Cloudflare will not support raw import of private keys. * The algorithm implementation may change over time. While Cloudflare cannot guarantee it at this time, Cloudflare will strive to maintain backward compatibility and compatibility with NodeJS's behavior. Any notable compatibility notes will be communicated in release notes and via this developer documentation. 3. MD5 is not part of the WebCrypto standard but is supported in Cloudflare Workers for interacting with legacy systems that require MD5\. MD5 is considered a weak algorithm. Do not rely upon MD5 for security. --- ## Related resources * [SubtleCrypto documentation on MDN ↗](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) * [SubtleCrypto documentation as part of the W3C Web Crypto API specification ↗](https://www.w3.org/TR/WebCryptoAPI//#subtlecrypto-interface) * [Example: signing requests](https://developers.cloudflare.com/workers/examples/signing-requests/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#page","headline":"Web Crypto · Cloudflare Workers docs","description":"A set of low-level functions for common cryptographic tasks.","url":"https://developers.cloudflare.com/workers/runtime-apis/web-crypto/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Standardized APIs for use by Workers running on Cloudflare's global network. title: Web standards image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Web standards Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/web-standards/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## JavaScript standards The Cloudflare Workers runtime is [built on top of the V8 JavaScript and WebAssembly engine](https://developers.cloudflare.com/workers/reference/how-workers-works/). The Workers runtime is updated at least once a week, to at least the version of V8 that is currently used by Google Chrome's stable release. This means you can safely use the latest JavaScript features, with no need for transpilers. All of the [standard built-in objects ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference) supported by the current Google Chrome stable release are supported, with a few notable exceptions: * For security reasons, the following are not allowed: * `eval()` * `new Function` * [WebAssembly.compile ↗](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript%5Finterface/compile%5Fstatic) * [WebAssembly.compileStreaming ↗](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript%5Finterface/compileStreaming%5Fstatic) * `WebAssembly.instantiate` with a [buffer parameter ↗](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript%5Finterface/instantiate%5Fstatic#primary%5Foverload%5F%E2%80%94%5Ftaking%5Fwasm%5Fbinary%5Fcode) * [WebAssembly.instantiateStreaming ↗](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript%5Finterface/instantiateStreaming%5Fstatic) * `Date.now()` returns the time of the last I/O; it does not advance during code execution. --- ## Web standards and global APIs The following methods are available per the [Worker Global Scope ↗](https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope): ### Base64 utility methods * atob() * Decodes a string of data which has been encoded using base-64 encoding. * btoa() * Creates a base-64 encoded ASCII string from a string of binary data. ### Timers * setInterval() * Schedules a function to execute every time a given number of milliseconds elapses. * clearInterval() * Cancels the repeated execution set using [setInterval() ↗](https://developer.mozilla.org/en-US/docs/Web/API/setInterval). * setTimeout() * Schedules a function to execute in a given amount of time. * clearTimeout() * Cancels the delayed execution set using [setTimeout() ↗](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout). * [scheduler.wait()](https://developers.cloudflare.com/workers/runtime-apis/scheduler/) * Returns a Promise that resolves after a given number of milliseconds. An `await`\-able alternative to `setTimeout()`. Note Timers are only available inside of [the Request Context](https://developers.cloudflare.com/workers/runtime-apis/request/#the-request-context). ### `performance.timeOrigin` and `performance.now()` * performance.timeOrigin * Returns the high resolution time origin. Workers uses the UNIX epoch as the time origin, meaning that `performance.timeOrigin` will always return `0`. * performance.now() * Returns a `DOMHighResTimeStamp` representing the number of milliseconds elapsed since `performance.timeOrigin`. Note that Workers intentionally reduces the precision of `performance.now()` such that it returns the time of the last I/O and does not advance during code execution. Effectively, because of this, and because `performance.timeOrigin` is always, `0`, `performance.now()` will always equal `Date.now()`, yielding a consistent view of the passage of time within a Worker. ### `EventTarget` and `Event` The [EventTarget ↗](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) and [Event ↗](https://developer.mozilla.org/en-US/docs/Web/API/Event) API allow objects to publish and subscribe to events. ### `AbortController` and `AbortSignal` The [AbortController ↗](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and [AbortSignal ↗](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) APIs provide a common model for canceling asynchronous operations. ### Fetch global * fetch() * Starts the process of fetching a resource from the network. Refer to [Fetch API](https://developers.cloudflare.com/workers/runtime-apis/fetch/). Note The Fetch API is only available inside of [the Request Context](https://developers.cloudflare.com/workers/runtime-apis/request/#the-request-context). --- ## Encoding API Both `TextEncoder` and `TextDecoder` support UTF-8 encoding/decoding. [Refer to the MDN documentation for more information ↗](https://developer.mozilla.org/en-US/docs/Web/API/Encoding%5FAPI). The [TextEncoderStream ↗](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoderStream) and [TextDecoderStream ↗](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream) classes are also available. --- ## URL API The URL API supports URLs conforming to HTTP and HTTPS schemes. [Refer to the MDN documentation for more information ↗](https://developer.mozilla.org/en-US/docs/Web/API/URL) Note The default URL class behavior differs from the URL Spec documented above. A new spec-compliant implementation of the URL class can be enabled using the `url_standard` [compatibility flag](https://developers.cloudflare.com/workers/configuration/compatibility-flags/). --- ## Compression Streams The `CompressionStream` and `DecompressionStream` classes support the deflate, deflate-raw and gzip compression methods. [Refer to the MDN documentation for more information ↗](https://developer.mozilla.org/en-US/docs/Web/API/Compression%5FStreams%5FAPI) --- ## URLPattern API The `URLPattern` API provides a mechanism for matching URLs based on a convenient pattern syntax. [Refer to the MDN documentation for more information ↗](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern). --- ## `Intl` The `Intl` API allows you to format dates, times, numbers, and more to the format that is used by a provided locale (language and region). [Refer to the MDN documentation for more information ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Intl). --- ## `navigator.userAgent` When the [global\_navigator](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-navigator) compatibility flag is set, the [navigator.userAgent ↗](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent) property is available with the value `'Cloudflare-Workers'`. This can be used, for example, to reliably determine that code is running within the Workers environment. ## Unhandled promise rejections The [unhandledrejection ↗](https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection%5Fevent) event is emitted by the global scope when a JavaScript promise is rejected without a rejection handler attached. The [rejectionhandled ↗](https://developer.mozilla.org/en-US/docs/Web/API/Window/rejectionhandled%5Fevent) event is emitted by the global scope when a JavaScript promise rejection is handled late (after a rejection handler is attached to the promise after an `unhandledrejection` event has already been emitted). ```js addEventListener("unhandledrejection", (event) => { console.log(event.promise); // The promise that was rejected. console.log(event.reason); // The value or Error with which the promise was rejected. }); addEventListener("rejectionhandled", (event) => { console.log(event.promise); // The promise that was rejected. console.log(event.reason); // The value or Error with which the promise was rejected. }); ``` --- ## `navigator.sendBeacon(url[, data])` When the [global\_navigator](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-navigator) compatibility flag is set, the [navigator.sendBeacon(...) ↗](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon) API is available to send an HTTP `POST` request containing a small amount of data to a web server. This API is intended as a means of transmitting analytics or diagnostics information asynchronously on a best-effort basis. For example, you can replace: ```js const promise = fetch("https://example.com", { method: "POST", body: "hello world", }); ctx.waitUntil(promise); ``` with `navigator.sendBeacon(...)`: ```js navigator.sendBeacon("https://example.com", "hello world"); ``` ## The Web File System Access API When the `enable_web_file_system` compatibility flag is set, Workers supports the [Web File System Access API ↗](https://developer.mozilla.org/en-US/docs/Web/API/File%5FSystem%5FAccess%5FAPI), which allows you to read and write files and directories to a virtual file system within the Worker environment. This API provides access to the same in-memory virtual file system as the [node:fs module](https://developers.cloudflare.com/workers/runtime-apis/nodejs/fs/) but does not require Node.js compatibility to be enabled. ```js const root = await navigator.storage.getDirectory(); export default { async fetch(request) { const fileHandle = await root.getFileHandle("hello.txt", { create: true }); const writable = await fileHandle.createWritable(); await writable.write("Hello, world!"); await writable.close(); const file = await fileHandle.getFile(); const contents = await file.text(); return new Response(contents, { status: 200 }); }, }; ``` Please refer to the [MDN documentation ↗](https://developer.mozilla.org/en-US/docs/Web/API/File%5FSystem%5FAccess%5FAPI) for more information on using this API, and to the [node:fs documentation](https://developers.cloudflare.com/workers/runtime-apis/nodejs/fs/) for details on the virtual file system structure and limitations. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/web-standards/#page","headline":"JavaScript and web standards · Cloudflare Workers docs","description":"Standardized APIs for use by Workers running on Cloudflare's global network.","url":"https://developers.cloudflare.com/workers/runtime-apis/web-standards/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Execute code written in a language other than JavaScript or write an entire Cloudflare Worker in Rust. title: WebAssembly (Wasm) image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # WebAssembly (Wasm) Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/webassembly/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) [WebAssembly ↗](https://webassembly.org/) (abbreviated Wasm) allows you to compile languages like [Rust](https://developers.cloudflare.com/workers/languages/rust/), Go, or C to a binary format that can run in a wide variety of environments, including [web browsers ↗](https://developer.mozilla.org/en-US/docs/WebAssembly#browser%5Fcompatibility), Cloudflare Workers, and other WebAssembly runtimes. You can use WebAssembly to: * Execute code written in a language other than JavaScript, via `WebAssembly.instantiate()`. Note `WebAssembly.instantiate()` only supports pre-compiled modules as documented in the [web-standards documentation](https://developers.cloudflare.com/workers/runtime-apis/web-standards/#javascript-standards). * Write an entire Cloudflare Worker in Rust, using bindings that make Workers' JavaScript APIs available directly from your Rust code. Most programming languages can be compiled to Wasm, although support varies across languages and compilers. Guides are available for the following languages: * [Wasm in JavaScript](https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/) ## Supported proposals WebAssembly is a rapidly evolving set of standards, with [many proposed APIs ↗](https://webassembly.org/roadmap/) which are in various stages of development. In general, Workers supports the same set of features that are available in Google Chrome. ### SIMD SIMD is supported on Workers. For more information on using SIMD in WebAssembly, refer to [Fast, parallel applications with WebAssembly SIMD ↗](https://v8.dev/features/simd). ### Threading Threading is not possible in Workers. Each Worker runs in a single thread, and the [Web Worker ↗](https://developer.mozilla.org/en-US/docs/Web/API/Web%5FWorkers%5FAPI) API is not supported. ## Binary size Compiling to WebAssembly often requires including additional runtime dependencies. As a result, Workers that use WebAssembly are typically larger than an equivalent Worker written in JavaScript. The larger your Worker is, the longer it may take your Worker to start. Refer to [Worker startup time ↗](https://developers.cloudflare.com/workers/platform/limits/#worker-startup-time) for more information. We recommend using tools like [wasm-opt ↗](https://github.com/brson/wasm-opt-rs) to optimize the size of your Wasm binary. ## WebAssembly System Interface (WASI) The [WebAssembly System Interface ↗](https://wasi.dev/) (abbreviated WASI) is a modular system interface for WebAssembly that standardizes a set of underlying system calls for networking, file system access, and more. Applications can depend on the WebAssembly System Interface to behave identically across host environments and operating systems. WASI is an earlier and more rapidly evolving set of standards than Wasm. WASI support is experimental on Cloudflare Workers, with only some syscalls implemented. Refer to our [open source implementation of WASI ↗](https://github.com/cloudflare/workers-wasi), and [blog post about WASI on Workers ↗](https://blog.cloudflare.com/announcing-wasi-on-workers/) demonstrating its use. ### Resources on WebAssembly * [Serverless Rust with Cloudflare Workers ↗](https://blog.cloudflare.com/cloudflare-workers-as-a-serverless-rust-platform/) * [WebAssembly on Cloudflare Workers ↗](https://blog.cloudflare.com/webassembly-on-cloudflare-workers/) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/webassembly/#page","headline":"WebAssembly (Wasm) · Cloudflare Workers docs","description":"Execute code written in a language other than JavaScript or write an entire Cloudflare Worker in Rust.","url":"https://developers.cloudflare.com/workers/runtime-apis/webassembly/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Import and instantiate WebAssembly modules in Cloudflare Workers using JavaScript. title: Wasm in JavaScript image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Wasm in JavaScript Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Wasm can be used from within a Worker written in JavaScript or TypeScript by importing a Wasm module, and instantiating an instance of this module using [WebAssembly.instantiate() ↗](https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript%5Finterface/instantiate). This can be used to accelerate computationally intensive operations which do not involve significant I/O. This guide demonstrates the basics of Wasm and JavaScript interoperability. ## Simple Wasm Module In this guide, you will use the WebAssembly Text Format to create a simple Wasm module to understand how imports and exports work. In practice, you would not write code in this format. You would instead use the programming language of your choice and compile directly to WebAssembly Binary Format (`.wasm`). Review the following example module (`;;` denotes a comment): ```txt ;; src/simple.wat (module ;; Import a function from JavaScript named `imported_func` ;; which takes a single i32 argument and assign to ;; variable $i (func $i (import "imports" "imported_func") (param i32)) ;; Export a function named `exported_func` which takes a ;; single i32 argument and returns an i32 (func (export "exported_func") (param $input i32) (result i32) ;; Invoke `imported_func` with $input as argument local.get $input call $i ;; Return $input local.get $input return ) ) ``` Using [wat2wasm ↗](https://github.com/WebAssembly/wabt), convert the WAT format to WebAssembly Binary Format: ```sh wat2wasm src/simple.wat -o src/simple.wasm ``` ## Bundling Wrangler will bundle any Wasm module that ends in `.wasm` or `.wasm?module`, so that it is available at runtime within your Worker. This is done using a default bundling rule which can be customized in the [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). Refer to [Wrangler Bundling](https://developers.cloudflare.com/workers/wrangler/bundling/) for more information. ## Use from JavaScript After you have converted the WAT format to WebAssembly Binary Format, import and use the Wasm module in your existing JavaScript or TypeScript Worker: ```typescript import mod from "./simple.wasm"; // Define imports available to Wasm instance. const importObject = { imports: { imported_func: (arg: number) => { console.log(`Hello from JavaScript: ${arg}`); }, }, }; // Create instance of WebAssembly Module `mod`, supplying // the expected imports in `importObject`. This should be // done at the top level of the script to avoid instantiation on every request. const instance = await WebAssembly.instantiate(mod, importObject); export default { async fetch() { // Invoke the `exported_func` from our Wasm Instance with // an argument. const retval = instance.exports.exported_func(42); // Return the return value! return new Response(`Success: ${retval}`); }, }; ``` When invoked, this Worker should log `Hello from JavaScript: 42` and return `Success: 42`, demonstrating the ability to invoke Wasm methods with arguments from JavaScript and vice versa. ## Next steps In practice, you will likely compile a language of your choice (such as Rust) to WebAssembly binaries. Many languages provide a `bindgen` to simplify the interaction between JavaScript and Wasm. These tools may integrate with your JavaScript bundler, and provide an API other than the WebAssembly API for initializing and invoking your Wasm module. As an example, refer to the [Rust wasm-bindgen documentation ↗](https://wasm-bindgen.github.io/wasm-bindgen/examples/without-a-bundler.html). Alternatively, to write your entire Worker in Rust, Workers provides many of the same [Runtime APIs](https://developers.cloudflare.com/workers/runtime-apis) and [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) when using the `workers-rs` crate. For more information, refer to the [Workers Rust guide](https://developers.cloudflare.com/workers/languages/rust/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/#page","headline":"Wasm in JavaScript · Cloudflare Workers docs","description":"Import and instantiate WebAssembly modules in Cloudflare Workers using JavaScript.","url":"https://developers.cloudflare.com/workers/runtime-apis/webassembly/javascript/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Communicate in real time with your Cloudflare Workers. title: WebSockets image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # WebSockets Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/runtime-apis/websockets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Background WebSockets allow you to communicate in real time with your Cloudflare Workers serverless functions. For a complete example, refer to [Using the WebSockets API](https://developers.cloudflare.com/workers/examples/websockets/). Note If your application needs to coordinate among multiple WebSocket connections, such as a chat room or game match, you will need clients to send messages to a single-point-of-coordination. Durable Objects provide a single-point-of-coordination for Cloudflare Workers, and are often used in parallel with WebSockets to persist state over multiple clients and connections. In this case, refer to [Durable Objects](https://developers.cloudflare.com/durable-objects/) to get started, and prefer using the Durable Objects' extended [WebSockets API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/). ## Constructor ```js // { 0: , 1: } let websocketPair = new WebSocketPair(); ``` The WebSocketPair returned from this constructor is an Object, with two WebSockets at keys `0` and `1`. These WebSockets are commonly referred to as `client` and `server`. The below example combines `Object.values` and ES6 destructuring to retrieve the WebSockets as `client` and `server`: ```js let [client, server] = Object.values(new WebSocketPair()); ``` ## Methods ### accept * `accept(options?)` * Accepts the WebSocket connection and begins terminating requests for the WebSocket on Cloudflare's global network. This effectively enables the Workers runtime to begin responding to and handling WebSocket requests. #### Parameters * `options` object optional * An optional configuration object with the following properties: * `allowHalfOpen` boolean optional — When `true`, the runtime will not automatically send a reciprocal Close frame when a Close frame is received from the peer. Instead, `readyState` remains `CLOSING` until you explicitly call `close()`. This is useful for [WebSocket proxying](#close-behavior) where you need to coordinate the close across both sides of the proxy. Defaults to `false`. ### addEventListener * `addEventListener(eventWebSocketEvent, callbackFunctionFunction)` * Add callback functions to be executed when an event has occurred on the WebSocket. #### Parameters * `event` WebSocketEvent * The WebSocket event (refer to [Events](https://developers.cloudflare.com/workers/runtime-apis/websockets/#events)) to listen to. * `callbackFunction(messageMessage)` Function * A function to be called when the WebSocket responds to a specific event. ### close * `close(codenumber, reasonstring)` * Close the WebSocket connection. #### Parameters * `codeinteger` optional * An integer indicating the close code sent by the server. This should match an option from the [list of status codes ↗](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#status%5Fcodes) provided by the WebSocket spec. * `reasonstring` optional * A human-readable string indicating why the WebSocket connection was closed. ### send * `send(messagestring | ArrayBuffer | ArrayBufferView)` * Send a message to the other WebSocket in this WebSocket pair. #### Parameters * `messagestring` * The message to send down the WebSocket connection to the corresponding client. This should be a string or something coercible into a string; for example, strings and numbers will be simply cast into strings, but objects and arrays should be cast to JSON strings using `JSON.stringify`, and parsed in the client. --- ## Properties ### readyState * `readyState` number * Returns the current state of the WebSocket connection. Possible values: | Constant | Value | Description | | -------------------- | ----- | ------------------------------------------------ | | WebSocket.CONNECTING | 0 | The connection is not yet open. | | WebSocket.OPEN | 1 | The connection is open and ready to communicate. | | WebSocket.CLOSING | 2 | The connection is in the process of closing. | | WebSocket.CLOSED | 3 | The connection is closed. | ### binaryType * `binaryType` string * Controls how binary frames received on this WebSocket are surfaced to the `message` event. Valid values are `"blob"` and `"arraybuffer"`. The value is consulted when each incoming binary frame is dispatched, so assigning a new value affects subsequent messages only. The default is controlled by the [websocket\_standard\_binary\_type](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#websocket-standard-binary-type) compatibility flag. Refer to [Binary messages](#binary-messages) for details. --- ## Events * `close` * An event indicating the WebSocket has closed. The `CloseEvent` includes `code` (number), `reason` (string), and `wasClean` (boolean) properties. * `error` * An event indicating there was an error with the WebSocket. * `message` * An event indicating a new message received from the client, including the data passed by the client. Note WebSocket messages received by a Worker have a size limit of 32 MiB (33,554,432 bytes). If a larger message is sent, the WebSocket will be automatically closed with a `1009` "Message is too large" response. ## Types ### Message * `data` any - The data passed back from the other WebSocket in your pair. * `type` string - Defaults to `message`. --- ## Close behavior With the [web\_socket\_auto\_reply\_to\_close](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#websocket-auto-reply-to-close) compatibility flag (enabled by default on compatibility dates on or after `2026-04-07`), the Workers runtime automatically sends a reciprocal Close frame when it receives a Close frame from the peer. The `readyState` transitions to `CLOSED` before the `close` event fires. This matches the [WebSocket specification ↗](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close%5Fevent) and standard browser behavior. If you still call `close()` inside the `close` event handler, the call is silently ignored. Existing code that manually replies to Close frames will continue to work without changes. ```js server.addEventListener("close", (event) => { // readyState is already CLOSED — no need to call server.close(). console.log(server.readyState); // WebSocket.CLOSED console.log(event.code); // 1000 console.log(event.wasClean); // true }); ``` ### Half-open mode for proxying The automatic close behavior can interfere with WebSocket proxying, where a Worker sits between a client and a backend and needs to coordinate the close on both sides independently. To support this, pass `{ allowHalfOpen: true }` to `accept()`: ```js server.accept({ allowHalfOpen: true }); server.addEventListener("close", (event) => { // readyState is still CLOSING here, giving you time // to coordinate the close on the other side. console.log(server.readyState); // WebSocket.CLOSING // Manually close when ready. server.close(event.code, "done"); }); ``` Note WebSockets created with `new WebSocket(url)` always auto-reply to Close frames after this flag takes effect. There is no way to pass `allowHalfOpen` because these WebSockets are automatically accepted. If you need half-open behavior for a client WebSocket, use `fetch()` with the `Upgrade: websocket` header instead, then call `resp.webSocket.accept({ allowHalfOpen: true })`. ### Prior behavior On compatibility dates before `2026-04-07` (or with the `web_socket_manual_reply_to_close` flag), receiving a Close frame leaves the WebSocket in `CLOSING` state, and your code must call `close()` to complete the handshake. Failing to do so can result in `1006` abnormal closure errors on the client. --- ## Binary messages WebSocket frames carry either text or binary payloads, and the choice between the two is made by the sender at the time the frame is sent. Text frames are always delivered to the `message` event as JavaScript strings. Binary frames are delivered either as [Blob ↗](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or as [ArrayBuffer ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/ArrayBuffer), depending on the WebSocket's `binaryType`. With the [websocket\_standard\_binary\_type](https://developers.cloudflare.com/workers/configuration/compatibility-flags/#websocket-standard-binary-type) compatibility flag (enabled by default on compatibility dates on or after `2026-03-17`), `binaryType` defaults to `"blob"` and binary frames are delivered as `Blob` objects. This matches the [WebSocket specification ↗](https://websockets.spec.whatwg.org/) and standard browser behavior. Without the flag, `binaryType` defaults to `"arraybuffer"` and binary frames are delivered as `ArrayBuffer`, matching the runtime's historical behavior. The `binaryType` property itself is always available. To opt back into `ArrayBuffer` delivery for a single WebSocket, assign `binaryType` before calling `accept()`: ```js const resp = await fetch("https://example.com", { headers: { Upgrade: "websocket" }, }); const ws = resp.webSocket; // Opt back into ArrayBuffer delivery for this WebSocket. ws.binaryType = "arraybuffer"; ws.accept(); ws.addEventListener("message", (event) => { if (typeof event.data === "string") { // Text frame. } else { // event.data is an ArrayBuffer because we set binaryType above. } }); ``` ### Reading binary payloads An incoming binary frame is fully buffered before the `message` event fires, regardless of `binaryType`. The choice between `Blob` and `ArrayBuffer` does not change when or whether the frame is received — only how you access its bytes: * With `"arraybuffer"`, `event.data` is an [ArrayBuffer ↗](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/ArrayBuffer). You can inspect its size and read bytes synchronously (for example, `new Uint8Array(event.data)`). * With `"blob"`, `event.data` is a [Blob ↗](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Reading the bytes is asynchronous — for example, `await event.data.arrayBuffer()` or `await event.data.bytes()`. Under the new default, a binary message handler must be `async` in order to read the payload. If you want to keep an existing synchronous handler, set `binaryType` to `"arraybuffer"` on the WebSocket. ### When the value takes effect Per the [WebSocket specification ↗](https://websockets.spec.whatwg.org/#feedback-from-the-protocol), `binaryType` is mutable: the value is consulted at the moment each binary frame is dispatched to the `message` event, so assigning a new value affects only subsequent messages. If you want every binary message on a WebSocket to be delivered as the same type, assign `binaryType` before calling `accept()`. That guarantees the setting is in place before the runtime starts dispatching any incoming frames. ### Worker-wide opt-out If you are not ready to migrate and want to keep `ArrayBuffer` as the default for every WebSocket in your Worker, add the `no_websocket_standard_binary_type` flag to your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/). Individual WebSockets can still override the default by assigning `binaryType`. Note This flag has no effect on the Durable Object hibernatable WebSocket [webSocketMessage](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) handler. That handler always receives binary data as `ArrayBuffer`, since it operates outside the normal WebSocket read loop. --- ## Related resources * [Mozilla Developer Network's (MDN) documentation on the WebSocket class ↗](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) * [Our WebSocket template for building applications on Workers using WebSockets ↗](https://github.com/cloudflare/websocket-template) Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/runtime-apis/websockets/#page","headline":"WebSockets · Cloudflare Workers docs","description":"Communicate in real time with your Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/runtime-apis/websockets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Create full-stack applications deployed to Cloudflare Workers. title: Static Assets image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Static Assets Last updated Jul 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) You can upload static assets (HTML, CSS, images and other files) as part of your Worker, and Cloudflare will handle caching and serving them to web browsers. **Start from CLI** \- Scaffold a React SPA with an API Worker, and use the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/). npmyarnpnpm ``` npm create cloudflare@latest -- my-react-app --framework=react ``` ``` yarn create cloudflare my-react-app --framework=react ``` ``` pnpm create cloudflare@latest my-react-app --framework=react ``` --- **Or just deploy to Cloudflare** [![Deploy to Workers](https://deploy.workers.cloudflare.com/button)](https://dash.cloudflare.com/?to=/:account/workers-and-pages/create/deploy-to-workers&repository=https://github.com/cloudflare/templates/tree/main/vite-react-template) Learn more about supported frameworks on Workers. ### [Supported frameworks](https://developers.cloudflare.com/workers/framework-guides/) Start building on Workers with our framework guides. ### How it works When you deploy your project, Cloudflare deploys both your Worker code and your static assets in a single operation. This deployment operates as a tightly integrated "unit" running across Cloudflare's network, combining static file hosting, custom logic, and global caching. The **assets directory** specified in your [Wrangler configuration file](https://developers.cloudflare.com/workers/wrangler/configuration/#assets) is central to this design. During deployment, Wrangler automatically uploads the files from this directory to Cloudflare's infrastructure. Once deployed, requests for these assets are routed efficiently to locations closest to your users. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-spa", "main": "src/index.js", // Set this to today's date "compatibility_date": "2026-08-14", "assets": { "directory": "./dist", "binding": "ASSETS" } } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-spa" main = "src/index.js" # Set this to today's date compatibility_date = "2026-08-14" [assets] directory = "./dist" binding = "ASSETS" ``` Note If you are using the [Cloudflare Vite plugin](https://developers.cloudflare.com/workers/vite-plugin/), you do not need to specify `assets.directory`. For more information about using static assets with the Vite plugin, refer to the [plugin documentation](https://developers.cloudflare.com/workers/vite-plugin/reference/static-assets/). By adding an [**assets binding**](https://developers.cloudflare.com/workers/static-assets/binding/#binding), you can directly fetch and serve assets within your Worker code. ```js // index.js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { return new Response(JSON.stringify({ name: "Cloudflare" }), { headers: { "Content-Type": "application/json" }, }); } return env.ASSETS.fetch(request); }, }; ``` ```python from workers import WorkerEntrypoint, Response from urllib.parse import urlparse class Default(WorkerEntrypoint): async def fetch(self, request): # Example of serving static assets url = urlparse(request.url) if url.path.startswith("/api/): return Response.json({"name": "Cloudflare"}) return await self.env.ASSETS.fetch(request) ``` ### Routing behavior By default, if a requested URL matches a file in the static assets directory, that file will be served — without invoking Worker code. If no matching asset is found and a Worker script is present, the request will be processed by the Worker. The Worker can return a response or choose to defer again to static assets by using the [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/) (e.g. `env.ASSETS.fetch(request)`). If no Worker script is present, a `404 Not Found` response is returned. The default behavior for requests which don't match a static asset can be changed by setting the [not\_found\_handling option under assets](https://developers.cloudflare.com/workers/wrangler/configuration/#assets) in your Wrangler configuration file: * [not\_found\_handling = "single-page-application"](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/): Sets your application to return a `200 OK` response with `index.html` for requests which don't match a static asset. Use this if you have a Single Page Application. We recommend pairing this with selective routing using `run_worker_first` for [advanced routing control](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/#advanced-routing-control). * [not\_found\_handling = "404-page"](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/#custom-404-pages): Sets your application to return a `404 Not Found` response with the nearest `404.html` for requests which don't match a static asset. ```jsonc { "assets": { "directory": "./dist", "not_found_handling": "single-page-application" } } ``` ```toml [assets] directory = "./dist" not_found_handling = "single-page-application" ``` If you want the Worker code to execute before serving assets, you can use the `run_worker_first` option. This can be set to `true` to invoke the Worker script for all requests, or configured as an array of route patterns for selective Worker-script-first routing: **Invoking your Worker script on specific paths:** ```jsonc { "name": "my-spa-worker", // Set this to today's date "compatibility_date": "2026-08-14", "main": "./src/index.ts", "assets": { "directory": "./dist/", "not_found_handling": "single-page-application", "binding": "ASSETS", "run_worker_first": ["/api/*", "!/api/docs/*"] } } ``` ```toml name = "my-spa-worker" # Set this to today's date compatibility_date = "2026-08-14" main = "./src/index.ts" [assets] directory = "./dist/" not_found_handling = "single-page-application" binding = "ASSETS" run_worker_first = [ "/api/*", "!/api/docs/*" ] ``` For a more advanced pattern, refer to [SPA shell with bootstrap data](https://developers.cloudflare.com/workers/examples/spa-shell/), which uses HTMLRewriter to inject prefetched API data into the HTML stream. ### [Routing options](https://developers.cloudflare.com/workers/static-assets/routing/) Learn more about how you can customize routing behavior. ### Caching behavior Cloudflare provides automatic caching for static assets across its network, ensuring fast delivery to users worldwide. When a static asset is requested, it is automatically cached for future requests. * **First Request:** When an asset is requested for the first time, it is fetched from storage and cached at the nearest Cloudflare location. * **Subsequent Requests:** If a request for the same asset reaches a data center that does not have it cached, Cloudflare's [tiered caching system](https://developers.cloudflare.com/cache/how-to/tiered-cache/) allows it to be retrieved from a nearby cache rather than going back to storage. This improves cache hit ratio, reduces latency, and reduces unnecessary origin fetches. ## Try it out ### [Vite + React SPA tutorial](https://developers.cloudflare.com/workers/vite-plugin/tutorial/) Learn how to build and deploy a full-stack Single Page Application with static assets and API routes. ## Learn more ### [Supported frameworks](https://developers.cloudflare.com/workers/framework-guides/) Start building on Workers with our framework guides. ### [Billing and limitations](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/) Learn more about how requests are billed, current limitations, and troubleshooting. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/workers/static-assets/#page","headline":"Static Assets · Cloudflare Workers docs","description":"Create full-stack applications deployed to Cloudflare Workers.","url":"https://developers.cloudflare.com/workers/static-assets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-07-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Billing, troubleshooting, and limitations for Static assets on Workers title: Billing and Limitations image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Billing and Limitations Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Billing Requests to a project with static assets can either return static assets or invoke the Worker script, depending on if the request [matches a static asset or not](https://developers.cloudflare.com/workers/static-assets/routing/). * Requests to static assets are free and unlimited. Requests to the Worker script (for example, in the case of SSR content) are billed according to Workers pricing. Refer to [pricing](https://developers.cloudflare.com/workers/platform/pricing/#example-2) for an example. * There is no additional cost for storing Assets. * **Important note for free tier users**: When using [run\_worker\_first](https://developers.cloudflare.com/workers/static-assets/binding/#run%5Fworker%5Ffirst), requests matching the specified patterns will always invoke your Worker script. If you exceed your free tier request limits, these requests will receive a 429 (Too Many Requests) response instead of falling back to static asset serving. Negative patterns (patterns beginning with `!/`) will continue to serve assets correctly, as requests are directed to assets, without invoking your Worker script. ## Limitations See the [Platform Limits](https://developers.cloudflare.com/workers/platform/limits/#static-assets) ## Troubleshooting * `assets.bucket is a required field` — if you see this error, you need to update Wrangler to at least `3.78.10` or later. `bucket` is not a required field. Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/#page","headline":"Billing and Limitations · Cloudflare Workers docs","description":"Billing, troubleshooting, and limitations for Static assets on Workers","url":"https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Details on how to configure Workers static assets and its binding. title: Configuration and Bindings image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Configuration and Bindings Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/binding/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Configuring a Worker with assets requires specifying a [directory](https://developers.cloudflare.com/workers/static-assets/binding/#directory) and, optionally, an [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/), in your Worker's Wrangler file. The [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/) allows you to dynamically fetch assets from within your Worker script (e.g. `env.ASSETS.fetch()`), similarly to how you might with a make a `fetch()` call with a [Service binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/http/). Only one collection of static assets can be configured in each Worker. ## `directory` The folder of static assets to be served. For many frameworks, this is the `./public/`, `./dist/`, or `./build/` folder. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", // Set this to today's date "compatibility_date": "2026-08-14", "assets": { "directory": "./public/", }, } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" # Set this to today's date compatibility_date = "2026-08-14" [assets] directory = "./public/" ``` ### Ignoring assets Sometime there are files in the asset directory that should not be uploaded. In this case, create a `.assetsignore` file in the root of the assets directory. This file takes the same format as `.gitignore`. Wrangler will not upload asset files that match lines in this file. **Example** You are migrating from a Pages project where the assets directory is `dist`. You do not want to upload the server-side Worker code nor Pages configuration files as public client-side assets. Add the following `.assetsignore` file: ```txt _worker.js _redirects _headers ``` Now Wrangler will not upload these files as client-side assets when deploying the Worker. ## `run_worker_first` Controls whether to invoke the Worker script regardless of a request which would have otherwise matched an asset. `run_worker_first = false` (default) will serve any static asset matching a request, while `run_worker_first = true` will unconditionally [invoke your Worker script](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/#run-your-worker-script-first). ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", // Set this to today's date "compatibility_date": "2026-08-14", "main": "src/index.ts", // The following configuration unconditionally invokes the Worker script at // `src/index.ts`, which can programmatically fetch assets via the ASSETS binding "assets": { "directory": "./public/", "binding": "ASSETS", "run_worker_first": true, }, } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" # Set this to today's date compatibility_date = "2026-08-14" main = "src/index.ts" [assets] directory = "./public/" binding = "ASSETS" run_worker_first = true ``` You can also specify `run_worker_first` as an array of route patterns to selectively run the Worker script first only for specific routes. The array supports glob patterns with `*` for deep matching and negative patterns with `!` prefix. Negative patterns have precedence over non-negative patterns. The Worker will run first when a non-negative pattern matches and none of the negative pattern matches. The order in which the patterns are listed is not significant. `run_worker_first` is often paired with the [not\_found\_handling = "single-page-application" setting](https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/#advanced-routing-control): ```jsonc { "name": "my-spa-worker", // Set this to today's date "compatibility_date": "2026-08-14", "main": "./src/index.ts", "assets": { "directory": "./dist/", "not_found_handling": "single-page-application", "binding": "ASSETS", "run_worker_first": ["/api/*", "!/api/docs/*"] } } ``` ```toml name = "my-spa-worker" # Set this to today's date compatibility_date = "2026-08-14" main = "./src/index.ts" [assets] directory = "./dist/" not_found_handling = "single-page-application" binding = "ASSETS" run_worker_first = [ "/api/*", "!/api/docs/*" ] ``` In this configuration, requests to `/api/*` routes will invoke the Worker script first, except for `/api/docs/*` which will follow the default asset-first routing behavior. Common uses for `run_worker_first` include authentication checks, A/B testing, and [injecting bootstrap data into your SPA shell](https://developers.cloudflare.com/workers/examples/spa-shell/). ## `binding` Configuring the optional [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings) gives you access to the collection of assets from within your Worker script. ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-worker", "main": "./src/index.js", // Set this to today's date "compatibility_date": "2026-08-14", "assets": { "directory": "./public/", "binding": "ASSETS", }, } ``` ```toml "$schema" = "./node_modules/wrangler/config-schema.json" name = "my-worker" main = "./src/index.js" # Set this to today's date compatibility_date = "2026-08-14" [assets] directory = "./public/" binding = "ASSETS" ``` In the example above, assets would be available through `env.ASSETS`. ### Runtime API Reference #### `fetch()` **Parameters** * `request: Request | URL | string` Pass a [Request object](https://developers.cloudflare.com/workers/runtime-apis/request/), URL object, or URL string. Requests made through this method have `html_handling` and `not_found_handling` configuration applied to them. **Response** * `Promise` Returns a static asset response for the given request. **Example** Your dynamic code can make new, or forward incoming requests to your project's static assets using the assets binding. For example, `env.ASSETS.fetch(request)`, `env.ASSETS.fetch(new URL('https://assets.local/my-file'))` or `env.ASSETS.fetch('https://assets.local/my-file')`. The hostname used in the URL (for example, `assets.local`) is not meaningful — any valid hostname will work. Only the URL pathname is used to match assets. Note If you need to fetch assets from within an [RPC method](https://developers.cloudflare.com/workers/runtime-apis/rpc/#fetching-static-assets) (where there is no incoming `request`), construct a URL using any hostname — for example, `this.env.ASSETS.fetch(new Request('https://assets.local/path/to/asset'))`. Take the following example that configures a Worker script to return a response under all requests headed for `/api/`. Otherwise, the Worker script will pass the incoming request through to the asset binding. In this case, because a Worker script is only invoked when the requested route has not matched any static assets, this will always evaluate [not\_found\_handling](https://developers.cloudflare.com/workers/static-assets/#routing-behavior) behavior. ```js export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { // TODO: Add your custom /api/* logic here. return new Response("Ok"); } // Passes the incoming request through to the assets binding. // No asset matched this request, so this will evaluate `not_found_handling` behavior. return env.ASSETS.fetch(request); }, }; ``` ```ts interface Env { ASSETS: Fetcher; } export default { async fetch(request, env): Promise { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { // TODO: Add your custom /api/* logic here. return new Response("Ok"); } // Passes the incoming request through to the assets binding. // No asset matched this request, so this will evaluate `not_found_handling` behavior. return env.ASSETS.fetch(request); }, } satisfies ExportedHandler; ``` ## Routing configuration For the various static asset routing configuration options, refer to [Routing](https://developers.cloudflare.com/workers/static-assets/routing/). ## Smart Placement [Smart Placement](https://developers.cloudflare.com/workers/configuration/placement/) can be used to place a Worker's code close to your back-end infrastructure. Smart Placement will only have an effect if you specified a `main`, pointing to your Worker code. ### Smart Placement with Worker Code First If you desire to run your [Worker code ahead of assets](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/#run-your-worker-script-first) by setting `run_worker_first=true`, all requests must first travel to your Smart-Placed Worker. As a result, you may experience increased latency for asset requests. Use Smart Placement with `run_worker_first=true` when you need to integrate with other backend services, authenticate requests before serving any assets, or if you want to make modifications to your assets before serving them. If you want some assets served as quickly as possible to the user, but others to be served behind a smart-placed Worker, considering splitting your app into multiple Workers and [using service bindings to connect them](https://developers.cloudflare.com/workers/configuration/placement/#multiple-workers). ### Smart Placement with Assets First Enabling Smart Placement with `run_worker_first=false` (or not specifying it) lets you serve assets from as close as possible to your users, but moves your Worker logic to run most efficiently (such as near a database). Use Smart Placement with `run_worker_first=false` (or not specifying it) when prioritizing fast asset delivery. This will not impact the [default routing behavior](https://developers.cloudflare.com/workers/static-assets/#routing-behavior). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/binding/#page","headline":"Configuration and Bindings · Cloudflare Workers docs","description":"Details on how to configure Workers static assets and its binding.","url":"https://developers.cloudflare.com/workers/static-assets/binding/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Bindings"]} ``` --- --- description: Upload assets through the Workers API. title: Direct Uploads image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Direct Uploads Last updated Aug 10, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/direct-upload/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) Note Directly uploading assets via APIs is an advanced approach which, unless you are building a programmatic integration, most users will not need. Instead, we encourage users to deploy your Worker with [Wrangler](https://developers.cloudflare.com/workers/static-assets/get-started/#1-create-a-new-worker-project-using-the-cli). Our API empowers users to upload and include static assets as part of a Worker. These static assets can be served for free, and additionally, users can also fetch assets through an optional [assets binding](https://developers.cloudflare.com/workers/static-assets/binding/) to power more advanced applications. This guide will describe the process for attaching assets to your Worker directly with the API. sequenceDiagram participant User participant Workers API User<<->>Workers API: Submit manifest
POST /client/v4/accounts/:accountId/workers/scripts/:scriptName/assets-upload-session User<<->>Workers API: Upload files
POST /client/v4/accounts/:accountId/workers/assets/upload?base64=true User<<->>Workers API: Upload script version
PUT /client/v4/accounts/:accountId/workers/scripts/:scriptName sequenceDiagram participant User participant Workers API User<<->>Workers API: Submit manifest
POST /client/v4/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName/assets-upload-session User<<->>Workers API: Upload files
POST /client/v4/accounts/:accountId/workers/assets/upload?base64=true User<<->>Workers API: Upload script version
PUT /client/v4/accounts/:accountId/workers/dispatch/namespaces/:dispatchNamespace/scripts/:scriptName The asset upload flow can be distilled into three distinct phases: 1. Registration of a manifest 2. Upload of the assets 3. Deployment of the Worker ## Upload manifest The asset manifest is a ledger which keeps track of files we want to use in our Worker. This manifest is used to track assets associated with each Worker version, and eliminate the need to upload unchanged files prior to a new upload. The [manifest upload request](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/assets/subresources/upload/methods/create/) describes each file which we intend to upload. Each file is its own key representing the file path and name, and is an object which contains metadata about the file. `hash` represents a 32 hexadecimal character hash of the file, while `size` is the size (in bytes) of the file. ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/scripts/{script_name}/assets-upload-session \ --header 'content-type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ "manifest": { "/filea.html": { "hash": "08f1dfda4574284ab3c21666d1", "size": 12 }, "/fileb.html": { "hash": "4f1c1af44620d531446ceef93f", "size": 23 }, "/filec.html": { "hash": "54995e302614e0523757a04ec1", "size": 23 } } }' ``` ```bash curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/dispatch/namespaces/{dispatch_namespace}/scripts/{script_name}/assets-upload-session \ --header 'content-type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ "manifest": { "/filea.html": { "hash": "08f1dfda4574284ab3c21666d1", "size": 12 }, "/fileb.html": { "hash": "4f1c1af44620d531446ceef93f", "size": 23 }, "/filec.html": { "hash": "54995e302614e0523757a04ec1", "size": 23 } } }' ``` The resulting response will contain a JWT, which provides authentication during file upload. The JWT is valid for one hour. In addition to the JWT, the response instructs users how to optimally batch upload their files. These instructions are encoded in the `buckets` field. Each array in `buckets` contains a list of file hashes which should be uploaded together. Unmodified files will not be returned in the `buckets` field (as they do not need to be re-uploaded) if they have recently been uploaded in previous versions of your Worker. ```json { "result": { "jwt": "", "buckets": [ ["08f1dfda4574284ab3c21666d1", "4f1c1af44620d531446ceef93f"], ["54995e302614e0523757a04ec1"] ] }, "success": true, "errors": null, "messages": null } ``` Note If all assets have been previously uploaded, `buckets` will be empty, and `jwt` will contain a completion token. Uploading files is not necessary, and you can skip directly to [uploading a new script or version](https://developers.cloudflare.com/workers/static-assets/direct-upload/#createdeploy-new-version). ### Limitations * Limits differ based on account plan. Refer to [Account Plan Limits](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) for more information on limitations of static assets. ## Upload Static Assets The [file upload API](https://developers.cloudflare.com/api/resources/workers/subresources/assets/subresources/upload/methods/create/) requires files be uploaded using `multipart/form-data`. The contents of each file must be base64 encoded, and the `base64` query parameter in the URL must be set to `true`. The provided `Content-Type` header of each file part will be attached when eventually serving the file. If you wish to avoid sending a `Content-Type` header in your deployment, `application/null` may be sent at upload time. The `Authorization` header must be provided as a bearer token, using the JWT (upload token) from the aforementioned manifest upload call. Once every file in the manifest has been uploaded, a status code of 201 will be returned, with the `jwt` field present. This JWT is a final "completion" token which can be used to create a deployment of a Worker with this set of assets. This completion token is valid for 1 hour. ## Create/Deploy New Version [Script](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/), [Version](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/versions/methods/create/), and [Workers for Platform script](https://developers.cloudflare.com/api/resources/workers%5Ffor%5Fplatforms/subresources/dispatch/subresources/namespaces/subresources/scripts/methods/update/) upload endpoints require specifying a metadata part in the form data. Here, we can provide the completion token from the previous (upload assets) step. ```bash { "main_module": "main.js", "assets": { "jwt": "" }, "compatibility_date": "2021-09-14" } ``` If this is a Worker which already has assets, and you wish to just re-use the existing set of assets, we do not have to specify the completion token again. Instead, we can pass the boolean `keep_assets` option. ```bash { "main_module": "main.js", "keep_assets": true, "compatibility_date": "2021-09-14" } ``` Asset [routing configuration](https://developers.cloudflare.com/workers/wrangler/configuration/#assets) can be provided in the `assets` object, such as `html_handling` and `not_found_handling`. ```bash { "main_module": "main.js", "assets": { "jwt": "", "config" { "html_handling": "auto-trailing-slash" } }, "compatibility_date": "2021-09-14" } ``` Optionally, an assets binding can be provided if you wish to fetch and serve assets from within your Worker code. ```bash { "main_module": "main.js", "assets": { ... }, "bindings": [ ... { "name": "ASSETS", "type": "assets" } ... ] "compatibility_date": "2021-09-14" } ``` ## Programmatic Example This example is from [cloudflare-typescript ↗](https://github.com/cloudflare/cloudflare-typescript/blob/main/examples/workers/script-with-assets-upload.ts). ```js #!/usr/bin/env -S npm run tsn -T /** * Create a Worker that serves static assets * * This example demonstrates how to: * - Upload static assets to Cloudflare Workers * - Create and deploy a Worker that serves those assets * * Docs: * - https://developers.cloudflare.com/workers/static-assets/direct-upload * * Prerequisites: * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/ * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/ * * Environment variables: * - CLOUDFLARE_API_TOKEN (required) * - CLOUDFLARE_ACCOUNT_ID (required) * - ASSETS_DIRECTORY (required) * - CLOUDFLARE_SUBDOMAIN (optional) * * Usage: * Place your static files in the ASSETS_DIRECTORY, then run this script. * Assets will be available at: my-script-with-assets.$subdomain.workers.dev/$filename */ import crypto from "crypto"; import fs from "fs"; import { readFile } from "node:fs/promises"; import { extname } from "node:path"; import path from "path"; import { exit } from "node:process"; import Cloudflare from "cloudflare"; const WORKER_NAME = "my-worker-with-assets"; const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`; function loadConfig() { const apiToken = process.env["CLOUDFLARE_API_TOKEN"]; if (!apiToken) { throw new Error( "Missing required environment variable: CLOUDFLARE_API_TOKEN", ); } const accountId = process.env["CLOUDFLARE_ACCOUNT_ID"]; if (!accountId) { throw new Error( "Missing required environment variable: CLOUDFLARE_ACCOUNT_ID", ); } const assetsDirectory = process.env["ASSETS_DIRECTORY"]; if (!assetsDirectory) { throw new Error("Missing required environment variable: ASSETS_DIRECTORY"); } if (!fs.existsSync(assetsDirectory)) { throw new Error(`Assets directory does not exist: ${assetsDirectory}`); } const subdomain = process.env["CLOUDFLARE_SUBDOMAIN"]; return { apiToken, accountId, assetsDirectory, subdomain: subdomain || undefined, workerName: WORKER_NAME, }; } const config = loadConfig(); const client = new Cloudflare({ apiToken: config.apiToken, }); /** * Recursively reads all files from a directory and creates a manifest * mapping file paths to their hash and size. */ function createManifest(directory) { const manifest = {}; function processDirectory(currentDir, basePath = "") { try { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); const relativePath = path.join(basePath, entry.name); if (entry.isDirectory()) { processDirectory(fullPath, relativePath); } else if (entry.isFile()) { try { const fileContent = fs.readFileSync(fullPath); const extension = extname(relativePath).substring(1); // Generate a hash for the file const hash = crypto .createHash("sha256") .update(fileContent.toString("base64") + extension) .digest("hex") .slice(0, 32); // Normalize path separators to forward slashes const manifestPath = `/${relativePath.replace(/\\/g, "/")}`; manifest[manifestPath] = { hash, size: fileContent.length, }; console.log( `Added to manifest: ${manifestPath} (${fileContent.length} bytes)`, ); } catch (error) { console.warn(`Failed to process file ${fullPath}:`, error); } } } } catch (error) { throw new Error(`Failed to read directory ${currentDir}: ${error}`); } } processDirectory(directory); if (Object.keys(manifest).length === 0) { throw new Error(`No files found in assets directory: ${directory}`); } console.log(`Created manifest with ${Object.keys(manifest).length} files`); return manifest; } /** * Generates the Worker script content that serves static assets */ function generateWorkerScript(exampleFile) { return ` export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Serve a simple index page at the root if (url.pathname === '/') { return new Response( \` Static Assets Worker

This Worker serves static assets!

To access your assets, add /filename to the URL.

Try visiting \${url.origin}/${exampleFile}">/${exampleFile}

\`, { status: 200, headers: { 'Content-Type': 'text/html' } } ); } // Serve static assets for all other paths return env.ASSETS.fetch(request); } }; `.trim(); } /** * Creates upload payloads from buckets and manifest */ async function createUploadPayloads(buckets, manifest, assetsDirectory) { const payloads = []; for (const bucket of buckets) { const payload = {}; for (const hash of bucket) { // Find the file path for this hash const manifestEntry = Object.entries(manifest).find( ([_, data]) => data.hash === hash, ); if (!manifestEntry) { throw new Error(`Could not find file for hash: ${hash}`); } const [relativePath] = manifestEntry; const fullPath = path.join(assetsDirectory, relativePath); try { const fileContent = await readFile(fullPath); payload[hash] = fileContent.toString("base64"); console.log(`Prepared for upload: ${relativePath}`); } catch (error) { throw new Error(`Failed to read file ${fullPath}: ${error}`); } } payloads.push(payload); } return payloads; } /** * Uploads asset payloads */ async function uploadAssets(payloads, uploadJwt, accountId) { let completionJwt; console.log(`Uploading ${payloads.length} payload(s)...`); for (let i = 0; i < payloads.length; i++) { const payload = payloads[i]; console.log(`Uploading payload ${i + 1}/${payloads.length}...`); try { const response = await client.workers.assets.upload.create( { account_id: accountId, base64: true, body: payload, }, { headers: { Authorization: `Bearer ${uploadJwt}` }, }, ); if (response?.jwt) { completionJwt = response.jwt; } } catch (error) { throw new Error(`Failed to upload payload ${i + 1}: ${error}`); } } if (!completionJwt) { throw new Error("Upload completed but no completion JWT received"); } console.log("✅ All assets uploaded successfully"); return completionJwt; } async function main() { try { console.log( "🚀 Starting Worker creation and deployment with static assets...", ); console.log(`📁 Assets directory: ${config.assetsDirectory}`); console.log("📝 Creating asset manifest..."); const manifest = createManifest(config.assetsDirectory); const exampleFile = Object.keys(manifest)[0]?.replace(/^\//, "") || "file.txt"; const scriptContent = generateWorkerScript(exampleFile); let worker; try { worker = await client.workers.beta.workers.get(config.workerName, { account_id: config.accountId, }); console.log(`♻️ Worker ${config.workerName} already exists. Using it.`); } catch (error) { if (!(error instanceof Cloudflare.NotFoundError)) { throw error; } console.log(`✏️ Creating Worker ${config.workerName}...`); worker = await client.workers.beta.workers.create({ account_id: config.accountId, name: config.workerName, subdomain: { enabled: config.subdomain !== undefined, }, observability: { enabled: true, }, }); } console.log(`⚙️ Worker id: ${worker.id}`); console.log("🔄 Starting asset upload session..."); const uploadResponse = await client.workers.scripts.assets.upload.create( config.workerName, { account_id: config.accountId, manifest, }, ); const { buckets, jwt: uploadJwt } = uploadResponse; if (!uploadJwt || !buckets) { throw new Error("Failed to start asset upload session"); } let completionJwt; if (buckets.length === 0) { console.log("✅ No new assets to upload!"); // Use the initial upload JWT as completion JWT when no uploads are needed completionJwt = uploadJwt; } else { const payloads = await createUploadPayloads( buckets, manifest, config.assetsDirectory, ); completionJwt = await uploadAssets(payloads, uploadJwt, config.accountId); } console.log("✏️ Creating Worker version..."); // Create a new version with assets const version = await client.workers.beta.workers.versions.create( worker.id, { account_id: config.accountId, main_module: SCRIPT_FILENAME, compatibility_date: new Date().toISOString().split("T")[0], bindings: [ { type: "assets", name: "ASSETS", }, ], assets: { jwt: completionJwt, }, modules: [ { name: SCRIPT_FILENAME, content_type: "application/javascript+module", content_base64: Buffer.from(scriptContent).toString("base64"), }, ], }, ); console.log("🚚 Creating Worker deployment..."); // Create a deployment and point all traffic to the version we created await client.workers.scripts.deployments.create(config.workerName, { account_id: config.accountId, strategy: "percentage", versions: [ { percentage: 100, version_id: version.id, }, ], }); console.log("✅ Deployment successful!"); if (config.subdomain) { console.log(` 🌍 Your Worker is live! 📍 Base URL: https://${config.workerName}.${config.subdomain}.workers.dev/ 📄 Try accessing: https://${config.workerName}.${config.subdomain}.workers.dev/${exampleFile} `); } else { console.log(` ⚠️ Set up a route, custom domain, or workers.dev subdomain to access your Worker. Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically. `); } } catch (error) { console.error("❌ Deployment failed:", error); exit(1); } } main(); ``` ```ts #!/usr/bin/env -S npm run tsn -T /** * Create a Worker that serves static assets * * This example demonstrates how to: * - Upload static assets to Cloudflare Workers * - Create and deploy a Worker that serves those assets * * Docs: * - https://developers.cloudflare.com/workers/static-assets/direct-upload * * Prerequisites: * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/ * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/ * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/ * * Environment variables: * - CLOUDFLARE_API_TOKEN (required) * - CLOUDFLARE_ACCOUNT_ID (required) * - ASSETS_DIRECTORY (required) * - CLOUDFLARE_SUBDOMAIN (optional) * * Usage: * Place your static files in the ASSETS_DIRECTORY, then run this script. * Assets will be available at: my-script-with-assets.$subdomain.workers.dev/$filename */ import crypto from 'crypto'; import fs from 'fs'; import { readFile } from 'node:fs/promises'; import { extname } from 'node:path'; import path from 'path'; import { exit } from 'node:process'; import Cloudflare from 'cloudflare'; interface Config { apiToken: string; accountId: string; assetsDirectory: string; subdomain: string | undefined; workerName: string; } interface AssetManifest { [path: string]: { hash: string; size: number; }; } interface UploadPayload { [hash: string]: string; // base64 encoded content } const WORKER_NAME = 'my-worker-with-assets'; const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`; function loadConfig(): Config { const apiToken = process.env['CLOUDFLARE_API_TOKEN']; if (!apiToken) { throw new Error('Missing required environment variable: CLOUDFLARE_API_TOKEN'); } const accountId = process.env['CLOUDFLARE_ACCOUNT_ID']; if (!accountId) { throw new Error('Missing required environment variable: CLOUDFLARE_ACCOUNT_ID'); } const assetsDirectory = process.env['ASSETS_DIRECTORY']; if (!assetsDirectory) { throw new Error('Missing required environment variable: ASSETS_DIRECTORY'); } if (!fs.existsSync(assetsDirectory)) { throw new Error(`Assets directory does not exist: ${assetsDirectory}`); } const subdomain = process.env['CLOUDFLARE_SUBDOMAIN']; return { apiToken, accountId, assetsDirectory, subdomain: subdomain || undefined, workerName: WORKER_NAME, }; } const config = loadConfig(); const client = new Cloudflare({ apiToken: config.apiToken, }); /** * Recursively reads all files from a directory and creates a manifest * mapping file paths to their hash and size. */ function createManifest(directory: string): AssetManifest { const manifest: AssetManifest = {}; function processDirectory(currentDir: string, basePath = ''): void { try { const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); const relativePath = path.join(basePath, entry.name); if (entry.isDirectory()) { processDirectory(fullPath, relativePath); } else if (entry.isFile()) { try { const fileContent = fs.readFileSync(fullPath); const extension = extname(relativePath).substring(1); // Generate a hash for the file const hash = crypto .createHash('sha256') .update(fileContent.toString('base64') + extension) .digest('hex') .slice(0, 32); // Normalize path separators to forward slashes const manifestPath = `/${relativePath.replace(/\\/g, '/')}`; manifest[manifestPath] = { hash, size: fileContent.length, }; console.log(`Added to manifest: ${manifestPath} (${fileContent.length} bytes)`); } catch (error) { console.warn(`Failed to process file ${fullPath}:`, error); } } } } catch (error) { throw new Error(`Failed to read directory ${currentDir}: ${error}`); } } processDirectory(directory); if (Object.keys(manifest).length === 0) { throw new Error(`No files found in assets directory: ${directory}`); } console.log(`Created manifest with ${Object.keys(manifest).length} files`); return manifest; } /** * Generates the Worker script content that serves static assets */ function generateWorkerScript(exampleFile: string): string { return ` export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Serve a simple index page at the root if (url.pathname === '/') { return new Response( \` Static Assets Worker

This Worker serves static assets!

To access your assets, add /filename to the URL.

Try visiting \${url.origin}/${exampleFile}">/${exampleFile}

\`, { status: 200, headers: { 'Content-Type': 'text/html' } } ); } // Serve static assets for all other paths return env.ASSETS.fetch(request); } }; `.trim(); } /** * Creates upload payloads from buckets and manifest */ async function createUploadPayloads( buckets: string[][], manifest: AssetManifest, assetsDirectory: string ): Promise { const payloads: UploadPayload[] = []; for (const bucket of buckets) { const payload: UploadPayload = {}; for (const hash of bucket) { // Find the file path for this hash const manifestEntry = Object.entries(manifest).find( ([_, data]) => data.hash === hash ); if (!manifestEntry) { throw new Error(`Could not find file for hash: ${hash}`); } const [relativePath] = manifestEntry; const fullPath = path.join(assetsDirectory, relativePath); try { const fileContent = await readFile(fullPath); payload[hash] = fileContent.toString('base64'); console.log(`Prepared for upload: ${relativePath}`); } catch (error) { throw new Error(`Failed to read file ${fullPath}: ${error}`); } } payloads.push(payload); } return payloads; } /** * Uploads asset payloads */ async function uploadAssets( payloads: UploadPayload[], uploadJwt: string, accountId: string ): Promise { let completionJwt: string | undefined; console.log(`Uploading ${payloads.length} payload(s)...`); for (let i = 0; i < payloads.length; i++) { const payload = payloads[i]!; console.log(`Uploading payload ${i + 1}/${payloads.length}...`); try { const response = await client.workers.assets.upload.create( { account_id: accountId, base64: true, body: payload, }, { headers: { Authorization: `Bearer ${uploadJwt}` }, } ); if (response?.jwt) { completionJwt = response.jwt; } } catch (error) { throw new Error(`Failed to upload payload ${i + 1}: ${error}`); } } if (!completionJwt) { throw new Error('Upload completed but no completion JWT received'); } console.log('✅ All assets uploaded successfully'); return completionJwt; } async function main(): Promise { try { console.log('🚀 Starting Worker creation and deployment with static assets...'); console.log(`📁 Assets directory: ${config.assetsDirectory}`); console.log('📝 Creating asset manifest...'); const manifest = createManifest(config.assetsDirectory); const exampleFile = Object.keys(manifest)[0]?.replace(/^\//, '') || 'file.txt'; const scriptContent = generateWorkerScript(exampleFile); let worker; try { worker = await client.workers.beta.workers.get(config.workerName, { account_id: config.accountId, }); console.log(`♻️ Worker ${config.workerName} already exists. Using it.`); } catch (error) { if (!(error instanceof Cloudflare.NotFoundError)) { throw error; } console.log(`✏️ Creating Worker ${config.workerName}...`); worker = await client.workers.beta.workers.create({ account_id: config.accountId, name: config.workerName, subdomain: { enabled: config.subdomain !== undefined, }, observability: { enabled: true, }, }); } console.log(`⚙️ Worker id: ${worker.id}`); console.log('🔄 Starting asset upload session...'); const uploadResponse = await client.workers.scripts.assets.upload.create( config.workerName, { account_id: config.accountId, manifest, } ); const { buckets, jwt: uploadJwt } = uploadResponse; if (!uploadJwt || !buckets) { throw new Error('Failed to start asset upload session'); } let completionJwt: string; if (buckets.length === 0) { console.log('✅ No new assets to upload!'); // Use the initial upload JWT as completion JWT when no uploads are needed completionJwt = uploadJwt; } else { const payloads = await createUploadPayloads( buckets, manifest, config.assetsDirectory ); completionJwt = await uploadAssets( payloads, uploadJwt, config.accountId ); } console.log('✏️ Creating Worker version...'); // Create a new version with assets const version = await client.workers.beta.workers.versions.create(worker.id, { account_id: config.accountId, main_module: SCRIPT_FILENAME, compatibility_date: new Date().toISOString().split('T')[0]!, bindings: [ { type: 'assets', name: 'ASSETS', }, ], assets: { jwt: completionJwt, }, modules: [ { name: SCRIPT_FILENAME, content_type: 'application/javascript+module', content_base64: Buffer.from(scriptContent).toString('base64'), }, ], }); console.log('🚚 Creating Worker deployment...'); // Create a deployment and point all traffic to the version we created await client.workers.scripts.deployments.create(config.workerName, { account_id: config.accountId, strategy: 'percentage', versions: [ { percentage: 100, version_id: version.id, }, ], }); console.log('✅ Deployment successful!'); if (config.subdomain) { console.log(` 🌍 Your Worker is live! 📍 Base URL: https://${config.workerName}.${config.subdomain}.workers.dev/ 📄 Try accessing: https://${config.workerName}.${config.subdomain}.workers.dev/${exampleFile} `); } else { console.log(` ⚠️ Set up a route, custom domain, or workers.dev subdomain to access your Worker. Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically. `); } } catch (error) { console.error('❌ Deployment failed:', error); exit(1); } } main(); ``` Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/direct-upload/#page","headline":"Direct Uploads · Cloudflare Workers docs","description":"Upload assets through the Workers API.","url":"https://developers.cloudflare.com/workers/static-assets/direct-upload/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-10","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Run front-end websites — static or dynamic — directly on Cloudflare's global network. title: Get Started image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Get Started Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/get-started/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) For most front-end applications, you'll want to use a framework. Workers supports number of popular [frameworks](https://developers.cloudflare.com/workers/framework-guides/) that come with ready-to-use components, a pre-defined and structured architecture, and community support. View [framework specific guides](https://developers.cloudflare.com/workers/framework-guides/) to get started using a framework. Alternatively, you may prefer to build your website from scratch if: * You're interested in learning by implementing core functionalities on your own. * You're working on a simple project where you might not need a framework. * You want to optimize for performance by minimizing external dependencies. * You require complete control over every aspect of the application. * You want to build your own framework. This guide will instruct you through setting up and deploying a static site or a full-stack application without a framework on Workers. ## Deploy a static site This guide will instruct you through setting up and deploying a static site on Workers. ### 1\. Create a new Worker project using the CLI [C3 (create-cloudflare-cli) ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare) is a command-line tool designed to help you set up and deploy new applications to Cloudflare. Open a terminal window and run C3 to create your Worker project: npmyarnpnpm ``` npm create cloudflare@latest -- my-static-site ``` ``` yarn create cloudflare my-static-site ``` ``` pnpm create cloudflare@latest my-static-site ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `Static site`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). After setting up your project, change your directory by running the following command: ```sh cd my-static-site ``` ### 2\. Develop locally After you have created your Worker, run the [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) in the project directory to start a local server. This will allow you to preview your project locally during development. ```sh npx wrangler dev ``` ### 3\. Deploy your project Your project can be deployed to a `*.workers.dev` subdomain or a [Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/), from your own machine or from any CI/CD system, including [Cloudflare's own](https://developers.cloudflare.com/workers/ci-cd/builds/). The [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) will build and deploy your project. If you're using CI, ensure you update your ["deploy command"](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#build-settings) configuration appropriately. ```sh npx wrangler deploy ``` Note Learn about how assets are configured and how routing works from [Routing configuration](https://developers.cloudflare.com/workers/static-assets/routing/). ## Deploy a full-stack application This guide will instruct you through setting up and deploying dynamic and interactive server-side rendered (SSR) applications on Cloudflare Workers. When building a full-stack application, you can use any [Workers bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/), [including assets' own](https://developers.cloudflare.com/workers/static-assets/binding/), to interact with resources on the Cloudflare Developer Platform. ### 1\. Create a new Worker project [C3 (create-cloudflare-cli) ↗](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare) is a command-line tool designed to help you set up and deploy new applications to Cloudflare. Open a terminal window and run C3 to create your Worker project: npmyarnpnpm ``` npm create cloudflare@latest -- my-dynamic-site ``` ``` yarn create cloudflare my-dynamic-site ``` ``` pnpm create cloudflare@latest my-dynamic-site ``` For setup, select the following options: * For _What would you like to start with?_, choose `Hello World example`. * For _Which template would you like to use?_, choose `SSR / full-stack app`. * For _Which language do you want to use?_, choose `TypeScript`. * For _Do you want to use git for version control?_, choose `Yes`. * For _Do you want to deploy your application?_, choose `No` (we will be making some changes before deploying). After setting up your project, change your directory by running the following command: ```sh cd my-dynamic-site ``` ### 2\. Develop locally After you have created your Worker, run the [wrangler dev](https://developers.cloudflare.com/workers/wrangler/commands/general/#dev) in the project directory to start a local server. This will allow you to preview your project locally during development. ```sh npx wrangler dev ``` ### 3\. Modify your Project With your new project generated and running, you can begin to write and edit your project: * The `src/index.ts` file is populated with sample code. Modify its content to change the server-side behavior of your Worker. * The `public/index.html` file is populated with sample code. Modify its content, or anything else in `public/`, to change the static assets of your Worker. Then, save the files and reload the page. Your project's output will have changed based on your modifications. ### 4\. Deploy your Project Your project can be deployed to a `*.workers.dev` subdomain or a [Custom Domain](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/), from your own machine or from any CI/CD system, including [Cloudflare's own](https://developers.cloudflare.com/workers/ci-cd/builds/). The [wrangler deploy](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) will build and deploy your project. If you're using CI, ensure you update your ["deploy command"](https://developers.cloudflare.com/workers/ci-cd/builds/configuration/#build-settings) configuration appropriately. ```sh npx wrangler deploy ``` Note Learn about how assets are configured and how routing works from [Routing configuration](https://developers.cloudflare.com/workers/static-assets/routing/). Was this helpful? YesNo ## On this page [![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/) ```json {"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/static-assets/get-started/#page","headline":"Get Started · Cloudflare Workers docs","description":"Run front-end websites — static or dynamic — directly on Cloudflare's global network.","url":"https://developers.cloudflare.com/workers/static-assets/get-started/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-04-23","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}} ``` --- --- description: Learn about default and custom headers for Workers static assets, including Cache-Control, ETag, and Content-Type behavior. title: Headers image: https://developers.cloudflare.com/og-docs.png --- [Skip to content](#main-content) > Documentation Index > Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt > Use this file to discover all available pages before exploring further. # Headers Last updated Apr 23, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/workers/static-assets/headers/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/) ## Default headers When serving static assets, Workers will attach some headers to the response by default. These are: * **`Content-Type`** A `Content-Type` header is attached to the response if one is provided during [the asset upload process](https://developers.cloudflare.com/workers/static-assets/direct-upload/). [Wrangler](https://developers.cloudflare.com/workers/wrangler/commands/general/#deploy) automatically determines the MIME type of the file, based on its extension. * **`Cache-Control: public, max-age=0, must-revalidate`** Sent when the request does not have an `Authorization` or `Range` header, this response header tells the browser that the asset can be cached, but that the browser should revalidate the freshness of the content every time before using it. This default behavior ensures good website performance for static pages, while still guaranteeing that stale content will never be served. * **`ETag`** This header complements the default `Cache-Control` header. Its value is a hash of the static asset file, and browsers can use this in subsequent requests with an `If-None-Match` header to check for freshness, without needing to re-download the entire file in the case of a match. * **`CF-Cache-Status`** This header indicates whether the asset was served from the cache (`HIT`) or not (`MISS`).[1](#user-content-fn-1) Cloudflare reserves the right to attach new headers to static asset responses at any time in order to improve performance or harden the security of your Worker application. ## Custom headers The default response headers served on static asset responses can be overridden, removed, or added to, by creating a plain text file called `_headers` without a file extension, in the static asset directory of your project. This file will not itself be served as a static asset, but will instead be parsed by Workers and its rules will be applied to static asset responses. If you are using a framework, you will often have a directory named `public/` or `static/`, and this usually contains deploy-ready assets, such as favicons, `robots.txt` files, and site manifests. These files get copied over to a final output directory during the build, so this is the perfect place to author your `_headers` file. If you are not using a framework, the `_headers` file can go directly into your [static assets directory](https://developers.cloudflare.com/workers/static-assets/binding/#directory). Headers defined in the `_headers` file override what Cloudflare ordinarily sends. Caution Custom headers defined in the `_headers` file are not applied to responses generated by your Worker code, even if the request URL matches a rule defined in `_headers`. If you use a server-side rendered (SSR) framework, have configured `assets.run_worker_first`, or otherwise use a Worker script, you will likely need to attach any custom headers you wish to apply directly within that Worker script. ### Attach a header Header rules are defined in multi-line blocks. The first line of a block is the URL or URL pattern where the rule's headers should be applied. On the next line, an indented list of header names and header values must be written: ```txt [url] [name]: [value] ``` Using absolute URLs is supported, though be aware that absolute URLs must begin with `https` and specifying a port is not supported. `_headers` rules ignore the incoming request's port and protocol when matching against an incoming request. For example, a rule like `https://example.com/path` would match against requests to `other://example.com:1234/path`. You can define as many `[name]: [value]` pairs as you require on subsequent lines. For example: ```txt # This is a comment /secure/page X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: no-referrer /static/* Access-Control-Allow-Origin: * X-Robots-Tag: nosnippet https://myworker.mysubdomain.workers.dev/* X-Robots-Tag: noindex ``` An incoming request which matches multiple rules' URL patterns will inherit all rules' headers. Using the previous `_headers` file, the following requests will have the following headers applied: | Request URL | Headers | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | https://custom.domain/secure/page | X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: no-referrer | | https://custom.domain/static/image.jpg | Access-Control-Allow-Origin: \* X-Robots-Tag: nosnippet | | https://myworker.mysubdomain.workers.dev/home | X-Robots-Tag: noindex | | https://myworker.mysubdomain.workers.dev/secure/page | X-Frame-Options: DENY X-Content-Type-Options: nosniff Referrer-Policy: no-referrer X-Robots-Tag: noindex | | https://myworker.mysubdomain.workers.dev/static/styles.css | Access-Control-Allow-Origin: \* X-Robots-Tag: nosnippet, noindex | You may define up to 100 header rules. Each line in the `_headers` file has a 2,000 character limit. The entire line, including spacing, header name, and value, counts towards this limit. If a header is applied twice in the `_headers` file, the values are joined with a comma separator. ### Detach a header You may wish to remove a default header or a header which has been added by a more pervasive rule. This can be done by prepending the header name with an exclamation mark and space (`! `). ```txt /* Content-Security-Policy: default-src 'self'; /*.jpg ! Content-Security-Policy ``` ### Match a path The same URL matching features that [\_redirects](https://developers.cloudflare.com/workers/static-assets/redirects/) offers is also available to the `_headers` file. Note, however, that redirects are applied before headers, so when a request matches both a redirect and a header, the redirect takes priority. #### Splats When matching, a splat pattern — signified by an asterisk (`*`) — will greedily match all characters. You may only include a single splat in the URL. The matched value can be referenced within the header value as the `:splat` placeholder. #### Placeholders A placeholder can be defined with `:placeholder_name`. A colon (`:`) followed by a letter indicates the start of a placeholder and the placeholder name that follows must be composed of alphanumeric characters and underscores (`:[A-Za-z]\w*`). Every named placeholder can only be referenced once. Placeholders match all characters apart from the delimiter, which when part of the host, is a period (`.`) or a forward-slash (`/`) and may only be a forward-slash (`/`) when part of the path. Similarly, the matched value can be used in the header values with `:placeholder_name`. ```txt /movies/:title x-movie-name: You are watching ":title" ``` #### Examples ##### Cross-Origin Resource Sharing (CORS) To enable other domains to fetch every static asset from your Worker, the following can be added to the `_headers` file: ```txt /* Access-Control-Allow-Origin: * ``` This applies the \`Access-Control-Allow-Origin\` header to any incoming URL. Note that the CORS specification only allows \`\*\`, \`null\`, or an exact origin as valid \`Access-Control-Allow-Origin\` values — wildcard patterns within origins are not supported. To allow CORS from specific [preview URLs](https://developers.cloudflare.com/workers/versions-and-deployments/preview-urls/), you will need to handle this dynamically in your Worker code rather than through the \`\_headers\` file. ##### Prevent your workers.dev URLs showing in search results [Google ↗](https://developers.google.com/search/docs/advanced/robots/robots%5Fmeta%5Ftag#directives) and other search engines often support the `X-Robots-Tag` header to instruct its crawlers how your website should be indexed. For example, to prevent your `\*.\*.workers.dev` URLs from being indexed, add the following to your `_headers` file: ```txt https://:version.:subdomain.workers.dev/* X-Robots-Tag: noindex ``` ##### Configure custom browser cache behavior If you have a folder of fingerprinted assets (assets which have a hash in their filename), you can configure more aggressive caching behavior in the browser to improve performance for repeat visitors: ```txt /static/* Cache-Control: public, max-age=31556952, immutable ``` ##### Harden security for an application Caution If you are server-side rendering (SSR) or using a Worker to generate responses in any other way and wish to attach security headers, the headers should be sent from the Worker's `Response` instead of using a `_headers` file. For example, if you have an API endpoint and want to allow cross-origin requests, you should ensure that your Worker code attaches CORS headers to its responses, including to `OPTIONS` requests. You can prevent click-jacking by informing browsers not to embed your application inside another (for example, with an `