The key components to structuring a query in the Query Builder are:
Visualizations: An aggregate function like average, count, percentile, or unique that performs a calculation on a group of values to return a single value. Each aggregate function returns a graph visualization and a summary table.
Filters: A condition that allows you to exclude data not matching the criteria.
Search: A condition that only returns the data matching the specified string.
Group by: A function to collapse a field into only its distinct values, allowing you to more granularly apply aggregate functions.
Order by: A sorting function to order the returned rows.
Limits: A cap on the number of returned rows, allowing you to focus on what is important.
The Query Builder relies on structured logs for efficient indexed queries and extracting metrics from logs. Workers Observability natively supports and encourages structured logs. Structured logs store context-rich metadata as key-value pairs in the form of distinct fields (high dimensionality), each with many potential unique values (high cardinality). Invocation Logs, which can be enabled in your Worker, contain deep insights from Cloudflare’s network, and are a great example of a structured log. By logging important metadata as a structured log, you empower yourself to answer questions about your system that you couldn’t predict when writing the code.
Internally at Cloudflare, we’ve already found tremendous value from this new product. During development, the Workers Observability team was able to use the Query Builder to discover a bug in the Workers Observability team’s staging environment. A query on the number of the events per script returned the following response:
\n \n \n
After mapping this drop in recorded events against recent staging deployments, the team was able to isolate and root cause the introduction of the bug. Along with fixing the bug, the team also introduced new staging alerts to prevent errors like this from going unnoticed.
\n \n \n
Queries built with the Query Builder or Workers Logs can be saved with a custom name and description. You can star your favorite queries, and also share them with your teammates using a shareable link, making it easier than ever to debug together and invest in developing visualizations from your telemetry data.
You can now monitor CPU time and wall time for every Workers invocation across all of our observability offerings, including Tail Workers, Workers Logpush, and Workers Logs. These metrics help show how much time is spent executing code compared to the total elapsed time for the invocation, including I/O time.
For example, using the CPU time and wall time surfaced in the Invocation Log, you can use the Query Builder to show the p90 CPU time and wall time traffic for a single Worker script.
In February, we released a new view into your Workers’ metrics to help you monitor your gradual deployments with improved visualizations. Today, we are also launching a new Workers Metrics overview page in the Observability tab. Now you can easily compare metrics across Workers and understand the current state of your deployments, all from a single view.
Invocations are mechanisms to trigger the execution of a Worker or Durable Object in response to an event, such as an alarm, cron job, or a fetch.
When the Worker or Durable Object executes, log events are emitted. To date, we have surfaced logs in an events view where each log is ordered by the time it was published.
We’re now introducing an Invocations View, so you can group and view all logs from each invocation. These views are available in each Worker’s view and the Workers Observability tab.
You can now use the Workers Observability API to programmatically retrieve your telemetry data and populate the tool of your choice.
The API allows you to automate, integrate, and customize in ways that our dashboard may not. For example, you may want to analyze your logs in a notebook or correlate your Workers logs with logs from a different source. Leveraging the Workers Observability API can help you optimize your monitoring strategy, automate repetitive tasks, and improve flexibility in how you interact with your telemetry data.
We’re just getting started. We have lots in store to help make Cloudflare’s developer observability best-in-class. Join us in Discord in the #workers-observability channel for feedback and feature requests.
"],"published_at":[0,"2025-04-09T14:00+00:00"],"updated_at":[0,"2025-04-09T13:00:02.916Z"],"feature_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5l4YWLStEIxmAPpvMxzTPG/a6859e8e710dbac5102d1ab3cba5c5d4/image6.png"],"tags":[1,[[0,{"id":[0,"2xCnBweKwOI3VXdYsGVbMe"],"name":[0,"Developer Week"],"slug":[0,"developer-week"]}],[0,{"id":[0,"4HIPcb68qM0e26fIxyfzwQ"],"name":[0,"Developers"],"slug":[0,"developers"]}],[0,{"id":[0,"3E2D6D4cI82KIloPpl0WZW"],"name":[0,"General Availability"],"slug":[0,"general-availability"]}],[0,{"id":[0,"6hbkItfupogJP3aRDAq6v8"],"name":[0,"Cloudflare Workers"],"slug":[0,"workers"]}],[0,{"id":[0,"AgGcOor8Su1lTA6MASW4O"],"name":[0,"Workers Logs"],"slug":[0,"workers-logs"]}],[0,{"id":[0,"2abeEIbz2KGPZKzPZ3YiCZ"],"name":[0,"Workers Observability"],"slug":[0,"workers-observability"]}]]],"relatedTags":[0],"authors":[1,[[0,{"name":[0,"Rohin Lohe"],"slug":[0,"rohin"],"bio":[0,null],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4vCBepc4EU7EHnLJW3oIUq/c5fff23d4de1d78b58f16c4679c0f333/rohin.jpg"],"location":[0,null],"website":[0,null],"twitter":[0,null],"facebook":[0,null]}]]],"meta_description":[0,"We’ve improved Observability for Workers by announcing the General Availability of Workers Logs and the introduction of the Query Builder to help you investigate log events across all of your Workers."],"primary_author":[0,{}],"localeList":[0,{"name":[0,"blog-english-only"],"enUS":[0,"English for Locale"],"zhCN":[0,"No Page for Locale"],"zhHansCN":[0,"No Page for Locale"],"zhTW":[0,"No Page for Locale"],"frFR":[0,"No Page for Locale"],"deDE":[0,"No Page for Locale"],"itIT":[0,"No Page for Locale"],"jaJP":[0,"No Page for Locale"],"koKR":[0,"No Page for Locale"],"ptBR":[0,"No Page for Locale"],"esLA":[0,"No Page for Locale"],"esES":[0,"No Page for Locale"],"enAU":[0,"No Page for Locale"],"enCA":[0,"No Page for Locale"],"enIN":[0,"No Page for Locale"],"enGB":[0,"No Page for Locale"],"idID":[0,"No Page for Locale"],"ruRU":[0,"No Page for Locale"],"svSE":[0,"No Page for Locale"],"viVN":[0,"No Page for Locale"],"plPL":[0,"No Page for Locale"],"arAR":[0,"No Page for Locale"],"nlNL":[0,"No Page for Locale"],"thTH":[0,"No Page for Locale"],"trTR":[0,"No Page for Locale"],"heIL":[0,"No Page for Locale"],"lvLV":[0,"No Page for Locale"],"etEE":[0,"No Page for Locale"],"ltLT":[0,"No Page for Locale"]}],"url":[0,"https://blog.cloudflare.com/introducing-workers-observability-logs-metrics-and-queries-all-in-one-place"],"metadata":[0,{"title":[0,"Introducing Workers Observability: logs, metrics, and queries – all in one place"],"description":[0,"We’ve improved Observability for Workers by announcing the General Availability of Workers Logs and the introduction of the Query Builder to help you investigate log events across all of your Workers."],"imgPreview":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7vhRMQaLgFHTy7ZsiqzUN8/be29ec2543d75b55a325095824476efa/OG_Share_2024__36_.png"]}]}],[0,{"id":[0,"6rBnmM6UiFX8ca7XXlOU2X"],"title":[0,"Cloudflare Snippets are now Generally Available"],"slug":[0,"snippets"],"excerpt":[0,"Cloudflare Snippets are now generally available, enabling fast, cost-free JavaScript-based HTTP traffic modifications across all paid plans. "],"featured":[0,false],"html":[0,"\n
\n
Program your traffic at the edge — fast, flexible, and free
Cloudflare Snippets are now generally available (GA) for all paid plans, giving you a fast, flexible way to control HTTP traffic using lightweight JavaScript “code rules” — at no extra cost.
Need to transform headers dynamically, fine-tune caching, rewrite URLs, retry failed requests, replace expired links, throttle suspicious traffic, or validate authentication tokens? Snippets provide a production-ready solution built for performance, security, and control.
With GA, we’re introducing a new code editor to streamline writing and testing logic. This summer, we’re also rolling out an integration with Secrets Store — enabling you to bind and manage sensitive values like API keys directly in Snippets, securely and at scale.
Snippets bring the power of JavaScript to Cloudflare Rules, letting you write logic that runs before a request reaches your origin or after a response returns from upstream. They’re ideal when built-in rule actions aren’t quite enough. While Cloudflare Rules let you define traffic logic without code, Snippets extend that model with greater flexibility for advanced scenarios.
Cloudflare Snippets started as a bold idea: bring the power of JavaScript-based logic to Cloudflare Rules, without the complexity of a full-stack developer platform.
Over the past two years, Snippets have evolved into a production-ready “code rules” solution, shaping the future of HTTP traffic control.
2022: Cloudflare Snippets were announced during Developer Week as a solution for users needing flexible HTTP traffic modifications without a full Worker.
2023:Alpha launch — hundreds of users tested Snippets for high-performance traffic logic.
2024:7x traffic growth, processing 17,000 requests per second. Terraform support and production-grade backend were released.
2025:General Availability — Snippets introduces a new code editor, increased limits alongside other Cloudflare Rules products, integration with Trace, and a production-grade experience built for scale, handling over 2 million requests per second at peak. Integration with the Secrets Store is rolling out this summer.
Cloudflare Trace now shows exactly which Snippets were triggered on a request. This makes it easier to debug traffic behavior, verify logic execution, and understand how your Snippets interact with other products in the request pipeline.
Whether you’re fine-tuning header logic or troubleshooting a routing issue, Trace gives you real-time insight into how your edge logic behaves in production.
In the third quarter, you’ll be able to securely access API keys, authentication tokens, and other sensitive values from Secrets Store directly in your Snippets. No more plaintext secrets in your code, no more workarounds.
\n \n \n
Once rolled out, secrets can be configured for Snippets via the dashboard or API under the new “Settings” button.
Snippets are fast, flexible, and free, but how do they compare to Cloudflare Workers? Both allow you to programmatically control traffic. However, they solve different problems:
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n
Feature
\n
\n
\n
Snippets
\n
\n
\n
Workers
\n
\n
\n
\n
\n
Execute scripts based on request attributes (headers, geolocation, cookies, etc.)
\n
\n
\n
✅
\n
\n
\n
❌
\n
\n
\n
\n
\n
Modify HTTP requests/responses or serve a different response
\n
\n
\n
✅
\n
\n
\n
✅
\n
\n
\n
\n
\n
Add, remove, or rewrite headers dynamically
\n
\n
\n
✅
\n
\n
\n
✅
\n
\n
\n
\n
\n
Cache assets at the edge
\n
\n
\n
✅
\n
\n
\n
✅
\n
\n
\n
\n
\n
Route traffic dynamically between origins
\n
\n
\n
✅
\n
\n
\n
✅
\n
\n
\n
\n
\n
Authenticate requests, pre-sign URLs, run A/B testing
\n
\n
\n
✅
\n
\n
\n
✅
\n
\n
\n
\n
\n
Perform compute-intensive tasks (e.g., AI inference, image processing)
\n
\n
\n
❌
\n
\n
\n
✅
\n
\n
\n
\n
\n
Store persistent data (e.g., KV, Durable Objects, D1)
\n
\n
\n
❌
\n
\n
\n
✅
\n
\n
\n
\n
\n
Deploy via CLI (Wrangler)
\n
\n
\n
❌
\n
\n
\n
✅
\n
\n
\n
\n
\n
Use TypeScript, Python, Rust or other programming languages
\n
\n
\n
❌
\n
\n
\n
✅
\n
\n
\n \n
\n
\n
Use Snippets when:
You need ultra-fast conditional traffic modifications directly on Cloudflare’s network.
You want to extend Cloudflare Rules beyond built-in actions.
You need free, unlimited invocations within the execution limits.
You are migrating from VCL, Akamai’s EdgeWorkers, or on-premise logic.
Use Workers when:
Your application requires state management, Developer Platform product integrations, or high compute limits.
You are building APIs, full-stack applications, or complex workflows.
You need logging, debugging tools, CLI support, and gradual rollouts.
Still unsure? Check out our detailed guide for best practices.
Below are practical use cases demonstrating Snippets. Each script can be dynamically triggered using our powerful Rules language, so you can granularly control which requests your Snippets will be applied to.
Ensure reliability by automatically rerouting requests when your primary origin returns an unexpected response:
\n
export default {\n async fetch(request) {\n const response = await fetch(request); // send original request to the origin\n\n if (!response.ok && !response.redirected) { // if response is not 200 OK or a redirect, send to another origin\n const newRequest = new Request(request); // clone the original request to construct a new request\n newRequest.headers.set("X-Rerouted", "1"); // add a header to identify a re-routed request at the new origin\n const url = new URL(request.url); // clone the original URL\n url.hostname = "backup.example.com"; // send request to a different origin / hostname\n return await fetch(url, newRequest); // serve response from the backup origin\n }\n\n return response; // otherwise, serve response from the primary origin\n },\n};
Cloudflare Snippets are now generally available, bringing fast, cost-free, and intelligent HTTP traffic control to all paid plans.
With native integration into Cloudflare Rules and Terraform — and Secrets Store integration coming this summer — Snippets provide the most efficient way to manage advanced traffic logic at scale.
Explore Snippets in the Cloudflare Dashboard and start optimizing your traffic with lightweight, flexible rules that enhance performance and reduce complexity.
"],"published_at":[0,"2025-04-09T14:00+00:00"],"updated_at":[0,"2025-04-09T13:32:42.794Z"],"feature_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4K75EXkUFjERUZC1whnHvh/5c9a5ec68e5a3024bbb0fc605f1378d8/Feature_Image.png"],"tags":[1,[[0,{"id":[0,"2xCnBweKwOI3VXdYsGVbMe"],"name":[0,"Developer Week"],"slug":[0,"developer-week"]}],[0,{"id":[0,"4eBircwQ1VIos6rC8wgALG"],"name":[0,"Snippets"],"slug":[0,"snippets"]}],[0,{"id":[0,"3E2D6D4cI82KIloPpl0WZW"],"name":[0,"General Availability"],"slug":[0,"general-availability"]}],[0,{"id":[0,"78aSAeMjGNmCuetQ7B4OgU"],"name":[0,"JavaScript"],"slug":[0,"javascript"]}],[0,{"id":[0,"3aRZvV7ApVpkYKGhnNQH4w"],"name":[0,"CDN"],"slug":[0,"cdn"]}],[0,{"id":[0,"4Hr4XSACbfTmrYAA7iESxH"],"name":[0,"Edge Rules"],"slug":[0,"edge-rules"]}],[0,{"id":[0,"4HIPcb68qM0e26fIxyfzwQ"],"name":[0,"Developers"],"slug":[0,"developers"]}]]],"relatedTags":[0],"authors":[1,[[0,{"name":[0,"Nikita Cano"],"slug":[0,"nikita"],"bio":[0,"Product Manager, Rules and Insights (Configuration Rules, Compression Rules, Page Rules, Redirect Rules, Origin Rules, Snippets, Trace, Traffic Sequence, Transform Rules, URL Normalization, etc.)"],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6hGB1W3bIJaibV4F7Qe0L8/56f554eeae28828297135a54eacc3065/nikita.jpeg"],"location":[0,"London, United Kingdom"],"website":[0,"https://nikitacano.com"],"twitter":[0,null],"facebook":[0,null]}]]],"meta_description":[0,"Cloudflare Snippets are now GA, enabling fast, cost-free JavaScript-based HTTP traffic modifications across all paid plans. With Terraform support and upcoming integration with Secrets Store, Snippets provide a powerful, production-ready solution for managing HTTP traffic via code rules."],"primary_author":[0,{}],"localeList":[0,{"name":[0,"blog-english-only"],"enUS":[0,"English for Locale"],"zhCN":[0,"No Page for Locale"],"zhHansCN":[0,"No Page for Locale"],"zhTW":[0,"No Page for Locale"],"frFR":[0,"No Page for Locale"],"deDE":[0,"No Page for Locale"],"itIT":[0,"No Page for Locale"],"jaJP":[0,"No Page for Locale"],"koKR":[0,"No Page for Locale"],"ptBR":[0,"No Page for Locale"],"esLA":[0,"No Page for Locale"],"esES":[0,"No Page for Locale"],"enAU":[0,"No Page for Locale"],"enCA":[0,"No Page for Locale"],"enIN":[0,"No Page for Locale"],"enGB":[0,"No Page for Locale"],"idID":[0,"No Page for Locale"],"ruRU":[0,"No Page for Locale"],"svSE":[0,"No Page for Locale"],"viVN":[0,"No Page for Locale"],"plPL":[0,"No Page for Locale"],"arAR":[0,"No Page for Locale"],"nlNL":[0,"No Page for Locale"],"thTH":[0,"No Page for Locale"],"trTR":[0,"No Page for Locale"],"heIL":[0,"No Page for Locale"],"lvLV":[0,"No Page for Locale"],"etEE":[0,"No Page for Locale"],"ltLT":[0,"No Page for Locale"]}],"url":[0,"https://blog.cloudflare.com/snippets"],"metadata":[0,{"title":[0,"Cloudflare Snippets are now Generally Available"],"description":[0,"Cloudflare Snippets are now GA, enabling fast, cost-free JavaScript-based HTTP traffic modifications across all paid plans. With Terraform support and upcoming integration with Secrets Store, Snippets provide a powerful, production-ready solution for managing HTTP traffic via code rules."],"imgPreview":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1u82DgH9pMX5KZvYTBK5fl/bad7c50cde89c9153e80d19107be709a/OG_Share_2024__35_.png"]}]}],[0,{"id":[0,"67CgcpMED2Rw0BozjKbdUz"],"title":[0,"Your frontend, backend, and database — now in one Cloudflare Worker"],"slug":[0,"full-stack-development-on-cloudflare-workers"],"excerpt":[0,"You can now deploy static sites and full-stack applications on Cloudflare Workers. Framework support for React Router v7, Astro, Vue and more are generally available today and Cloudflare Vite plugin. "],"featured":[0,false],"html":[0,"
In September 2024, we introduced beta support for hosting, storing, and serving static assets for free on Cloudflare Workers — something that was previously only possible on Cloudflare Pages. Being able to host these assets — your client-side JavaScript, HTML, CSS, fonts, and images — was a critical missing piece for developers looking to build a full-stack application within a single Worker.
Today we’re announcing ten big improvements to building apps on Cloudflare. All together, these new additions allow you to build and host projects ranging from simple static sites to full-stack applications, all on Cloudflare Workers:
You can build complete full-stack apps on Workers without a framework: you can “just use Vite" and React together, and build a backend API in the same Worker. See our Vite + React template for an example.
The Cloudflare Vite plugin is now v1.0 and generally available. The Vite plugin allows you to run Vite’s development server in the Workers runtime (workerd), meaning you get all the benefits of Vite, including Hot Module Replacement, while still being able to use features that are exclusive to Workers (like Durable Objects).
You can now use static _headers and _redirects configuration files for your applications on Workers, something that was previously only available on Pages. These files allow you to add simple headers and configure redirects without executing any Worker code.
In addition to PostgreSQL, you can now connect to MySQL databases in addition from Cloudflare Workers, via Hyperdrive. Bring your existing Planetscale, AWS, GCP, Azure, or other MySQL database, and Hyperdrive will take care of pooling connections to your database and eliminating unnecessary roundtrips by caching queries.
More Node.js APIs are available in the Workers Runtime — including APIs from the crypto, tls, net, and dns modules. We’ve also increased the maximum CPU time for a Workers request from 30 seconds to 5 minutes.
The Images binding in Workers is generally available, allowing you to build more flexible, programmatic workflows.
These improvements allow you to build both simple static sites and more complex server-side rendered applications. Like Pages, you only get charged when your Worker code runs, meaning you can host and serve static sites for free. When you want to do any rendering on the server or need to build an API, simply add a Worker to handle your backend. And when you need to read or write data in your app, you can connect to an existing database with Hyperdrive, or use any of our storage solutions: Workers KV, R2, Durable Objects, or D1.
If you'd like to dive straight into code, you can deploy a single-page application built with Vite and React, with the option to connect to a hosted database with Hyperdrive, by clicking this “Deploy to Cloudflare” button:
Previously, you needed to choose between building on Cloudflare Pages or Workers (or use Pages for one part of your app, and Workers for another) just to get started. This meant figuring out what your app needed from the start, and hoping that if your project evolved, you wouldn’t be stuck with the wrong platform and architecture. Workers was designed to be a flexible platform, allowing developers to evolve projects as needed — and so, we’ve worked to bring pieces of Pages into Workers over the years.
Now that Workers supports both serving static assets and server-side rendering, you should start with Workers. Cloudflare Pages will continue to be supported, but, going forward, all of our investment, optimizations, and feature work will be dedicated to improving Workers. We aim to make Workers the best platform for building full-stack apps, building upon your feedback of what went well with Pages and what we could improve.
Before, building an app on Pages meant you got a really easy, opinionated on-ramp, but you’d eventually hit a wall if your application got more complex. If you wanted to use Durable Objects to manage state, you would need to set up an entirely separate Worker to do so, ending up with a complicated deployment and more overhead. You also were limited to real-time logs, and could only roll out changes all in one go.
When you build on Workers, you can immediately bind to any other Developer Platform service (including Durable Objects, Email Workers, and more), and manage both your front end and back end in a single project — all with a single deployment. You also get the whole suite of Workers observability tooling built into the platform, such as Workers Logs. And if you want to rollout changes to only a certain percentage of traffic, you can do so with Gradual Deployments.
These latest improvements are part of our goal to bring the best parts of Pages into Workers. For example, we now support static _headers and _redirects config files, so that you can easily take an existing project from Pages (or another platform) and move it over to Workers, without needing to change your project. We also directly integrate with GitHub and GitLab with Workers Builds, providing automatic builds and deployments. And starting today, Preview URLs are posted back to your repository as a comment, with feature branch aliases and environments coming soon.
To learn how to migrate an existing project from Pages to Workers, read our migration guide.
Next, let’s talk about how you can build applications with different rendering modes on Workers.
As a quick primer, here are all the architectures and rendering modes we’ll be discussing that are supported on Workers:
Static sites: When you visit a static site, the server immediately returns pre-built static assets — HTML, CSS, JavaScript, images, and fonts. There’s no dynamic rendering happening on the server at request-time. Static assets are typically generated at build-time and served directly from a CDN, making static sites fast and easily cacheable. This approach works well for sites with content that rarely changes.
Single-Page Applications (SPAs): When you load an SPA, the server initially sends a minimal HTML shell and a JavaScript bundle (served as static assets). Your browser downloads this JavaScript, which then takes over to render the entire user interface client-side. After the initial load, all navigation occurs without full-page refreshes, typically via client-side routing. This creates a fast, app-like experience.
Server-Side Rendered (SSR) applications: When you first visit a site that uses SSR, the server generates a fully-rendered HTML page on-demand for that request. Your browser immediately displays this complete HTML, resulting in a fast first page load. Once loaded, JavaScript "hydrates" the page, adding interactivity. Subsequent navigations can either trigger new server-rendered pages or, in many modern frameworks, transition into client-side rendering similar to an SPA.
Next, we’ll dive into how you can build these kinds of applications on Workers, starting with setting up your development environment.
Before uploading your application, you need to bundle all of your client-side code into a directory of static assets. Wrangler bundles and builds your code when you run wrangler dev, but we also now support Vite with our new Vite plugin. This is a great option for those already using Vite’s build tooling and development server — you can continue developing (and testing with Vitest) using Vite’s development server, all using the Workers runtime.
To get started using the Cloudflare Vite plugin, you can scaffold a React application using Vite and our plugin, by running:
The Vite plugin informs Wrangler that this /dist directory contains the project’s built static assets — which, in this case, includes client-side code, some CSS files, and images.
Once deployed, this single-page application (SPA) architecture will look something like this:
\n \n \n
When a request comes in, Cloudflare looks at the pathname and automatically serves any static assets that match that pathname. For example, if your static assets directory includes a blog.html file, requests for example.com/blog get that file.
If you have a static site created by a static site generator (SSG) like Astro, all you need to do is create a wrangler.jsonc file (or wrangler.toml) and tell Cloudflare where to find your built assets:
Once you’ve added this configuration, you can simply build your project and run wrangler deploy. Your entire site will then be uploaded and ready for traffic on Workers. Once deployed and requests start flowing in, your static site will be cached across Cloudflare’s network.
\n \n \n
You can try starting a fresh Astro project on Workers today by running:
By enabling this, the platform assumes that any navigation request (requests which include a Sec-Fetch-Mode: navigate header) are intended for static assets and will serve up index.html whenever a matching static asset match cannot be found. For non-navigation requests (such as requests for data) that don't match a static asset, Cloudflare will invoke the Worker script. With this setup, you can render the frontend with React, use a Worker to handle back-end operations, and use Vite to help stitch the two together. This is a great option for porting over older SPAs built with create-react-app, which was recently sunset.
Another thing to note in this Wrangler configuration file: we’ve defined a Hyperdrive binding and enabled Smart Placement. Hyperdrive lets us use an existing database and handles connection pooling. This solves a long-standing challenge of connecting Workers (which run in a highly distributed, serverless environment) directly to traditional databases. By design, Workers operate in lightweight V8 isolates with no persistent TCP sockets and a strict CPU/memory limit. This isolation is great for security and speed, but it makes it difficult to hold open database connections. Hyperdrive addresses these constraints by acting as a “bridge” between Cloudflare’s network and your database, taking care of the heavy lifting of maintaining stable connections or pools so that Workers can reuse them. By turning on Smart Placement, we also ensure that if requests to our Worker originate far from the database (causing latency), Cloudflare can choose to relocate both the Worker—which handles the database connection—and the Hyperdrive “bridge” to a location closer to the database, reducing round-trip times.
SPA example: Worker code
Let’s look at the “Deploy to Cloudflare” example at the top of this blog. In api/index.js, we’ve defined an API (using Hono) which connects to a hosted database through Hyperdrive.
\n
import { Hono } from "hono";\nimport postgres from "postgres";\nimport booksRouter from "./routes/books";\nimport bookRelatedRouter from "./routes/book-related";\n\nconst app = new Hono();\n\n// Setup SQL client middleware\napp.use("*", async (c, next) => {\n // Create SQL client\n const sql = postgres(c.env.HYPERDRIVE.connectionString, {\n max: 5,\n fetch_types: false,\n });\n\n c.env.SQL = sql;\n\n // Process the request\n await next();\n\n // Close the SQL connection after the response is sent\n c.executionCtx.waitUntil(sql.end());\n});\n\napp.route("/api/books", booksRouter);\napp.route("/api/books/:id/related", bookRelatedRouter);\n\n\nexport default {\n fetch: app.fetch,\n};
\n
When deployed, our app’s architecture looks something like this:
\n \n \n
If Smart Placement moves the placement of my Worker to run closer to my database, it could look like this:
We’ve also continued to make progress supporting Node.js APIs, recently adding support for the crypto, tls, net, and dns modules. This allows existing applications and libraries that rely on these Node.js modules to run on Workers. Let’s take a look at an example:
Previously, if you tried to use the mongodb package, you encountered the following error:
\n
Error: [unenv] dns.resolveTxt is not implemented yet!
\n
This occurred when mongodb used the node:dns module to do a DNS lookup of a hostname. Even if you avoided that issue, you would have encountered another error when mongodb tried to use node:tls to securely connect to a database.
Now, you can use mongodb as expected because node:dns and node:tls are supported. The same can be said for libraries relying on node:crypto and node:net.
Additionally, Workers now expose environment variables and secrets on the process.env object when the nodejs_compat compatibility flag is on and the compatibility date is set to 2025-04-01 or beyond. Some libraries (and developers) assume that this object will be populated with variables, and rely on it for top-level configuration. Without the tweak, libraries may have previously broken unexpectedly and developers had to write additional logic to handle variables on Cloudflare Workers.
Now, you can just access your variables as you would in Node.js.
We have also raised the maximum CPU time per Worker request from 30 seconds to 5 minutes. This allows for compute-intensive operations to run for longer without timing out. Say you want to use the newly supported node:crypto module to hash a very large file, you can now do this on Workers without having to rely on external compute for CPU-intensive operations.
We’ve also made improvements to Workers Builds, which allows you to connect a Git repository to your Worker, so that you can have automatic builds and deployments on every pushed change. Workers Builds was introduced during Builder Day 2024, and initially only allowed you to connect a repository to an existing Worker. Now, you can bring a repository and immediately deploy it as a new Worker, reducing the amount of setup and button clicking needed to bring a project over. We’ve improved the performance of Workers Builds by reducing the latency of build starts by 6 seconds — they now start within 10 seconds on average. We also boosted API responsiveness, achieving a 7x latency improvement thanks to Smart Placement.
Note: On April 2, 2025, Workers Builds transitioned to a new pricing model, as announced during Builder Day 2024. Free plan users are now capped at 3,000 minutes of build time, and Workers Paid subscription users will have a new usage-based model with 6,000 free minutes included and $0.005 per build minute pricing after. To better support concurrent builds, Paid plans will also now get six (6) concurrent builds, making it easier to work across multiple projects and monorepos. For more information on pricing, see the documentation.
Last week, we wrote a blog post that covers how the Images binding enables more flexible, programmatic workflows for image optimization.
Previously, you could access image optimization features by calling fetch() in your Worker. This method requires the original image to be retrievable by URL. However, you may have cases where images aren’t accessible from a URL, like when you want to compress user-uploaded images before they are uploaded to your storage. With the Images binding, you can directly optimize an image by operating on its body as a stream of bytes.
We’re excited to see what you’ll build, and are focused on new features and improvements to make it easier to create any application on Workers. Much of this work was made even better by community feedback, and we encourage everyone to join our Discord to participate in the discussion.
"],"published_at":[0,"2025-04-08T14:05+00:00"],"updated_at":[0,"2025-04-09T15:51:08.013Z"],"feature_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4XxYGJI8hHbY3GQWzuM2BA/bfad24f662883f169142de42806316b3/Feature_Image.png"],"tags":[1,[[0,{"id":[0,"2xCnBweKwOI3VXdYsGVbMe"],"name":[0,"Developer Week"],"slug":[0,"developer-week"]}],[0,{"id":[0,"4HIPcb68qM0e26fIxyfzwQ"],"name":[0,"Developers"],"slug":[0,"developers"]}],[0,{"id":[0,"7zEmuPybEdq3jhbDEc1fQN"],"name":[0,"Front End"],"slug":[0,"front-end"]}],[0,{"id":[0,"6Irshc0o9CUpLTphYWQ5mH"],"name":[0,"Full Stack"],"slug":[0,"full-stack"]}],[0,{"id":[0,"3E2D6D4cI82KIloPpl0WZW"],"name":[0,"General Availability"],"slug":[0,"general-availability"]}],[0,{"id":[0,"3kr4meEhp1NrKwm01XXeqk"],"name":[0,"Cloudflare Pages"],"slug":[0,"cloudflare-pages"]}],[0,{"id":[0,"6hbkItfupogJP3aRDAq6v8"],"name":[0,"Cloudflare Workers"],"slug":[0,"workers"]}],[0,{"id":[0,"715fxP0LCYITiFP6JZ7rkX"],"name":[0,"MySQL"],"slug":[0,"mysql"]}],[0,{"id":[0,"5EP9xxxSTGvFx3RIxjqIgP"],"name":[0,"Hyperdrive"],"slug":[0,"hyperdrive"]}]]],"relatedTags":[0],"authors":[1,[[0,{"name":[0,"Korinne Alpers"],"slug":[0,"korinne-alpers"],"bio":[0],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/5cfasTtVaoOsISarcB9F4Z/1686863c1bbd4dc0b74c7229cfcee42c/Korinne_Alpers.jpg"],"location":[0],"website":[0],"twitter":[0],"facebook":[0]}]]],"meta_description":[0,"You can now deploy static sites and full-stack applications on Cloudflare Workers – the primitives are all here. Framework support for React Router v7, Astro, Vue, and more are generally available today, as is the Cloudflare Vite plugin. Connect to Postgres and MySQL databases with Hyperdrive."],"primary_author":[0,{}],"localeList":[0,{"name":[0,"blog-english-only"],"enUS":[0,"English for Locale"],"zhCN":[0,"No Page for Locale"],"zhHansCN":[0,"No Page for Locale"],"zhTW":[0,"No Page for Locale"],"frFR":[0,"No Page for Locale"],"deDE":[0,"No Page for Locale"],"itIT":[0,"No Page for Locale"],"jaJP":[0,"No Page for Locale"],"koKR":[0,"No Page for Locale"],"ptBR":[0,"No Page for Locale"],"esLA":[0,"No Page for Locale"],"esES":[0,"No Page for Locale"],"enAU":[0,"No Page for Locale"],"enCA":[0,"No Page for Locale"],"enIN":[0,"No Page for Locale"],"enGB":[0,"No Page for Locale"],"idID":[0,"No Page for Locale"],"ruRU":[0,"No Page for Locale"],"svSE":[0,"No Page for Locale"],"viVN":[0,"No Page for Locale"],"plPL":[0,"No Page for Locale"],"arAR":[0,"No Page for Locale"],"nlNL":[0,"No Page for Locale"],"thTH":[0,"No Page for Locale"],"trTR":[0,"No Page for Locale"],"heIL":[0,"No Page for Locale"],"lvLV":[0,"No Page for Locale"],"etEE":[0,"No Page for Locale"],"ltLT":[0,"No Page for Locale"]}],"url":[0,"https://blog.cloudflare.com/full-stack-development-on-cloudflare-workers"],"metadata":[0,{"title":[0,"Your frontend, backend, and database — now in one Cloudflare Worker"],"description":[0,"You can now deploy static sites and full-stack applications on Cloudflare Workers – the primitives are all here. Framework support for React Router v7, Astro, Vue, and more are generally available today, as is the Cloudflare Vite plugin. Connect to Postgres and MySQL databases with Hyperdrive."],"imgPreview":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/nEHopEC5RW0uFf2J4V05f/225d3e9189bbdcf1610215a0e93ddf78/OG_Share_2024__33_.png"]}]}],[0,{"id":[0,"2xXeB9HQ7cEwSs5NT84Wrx"],"title":[0,"Deploy your Next.js app to Cloudflare Workers with the Cloudflare adapter for OpenNext"],"slug":[0,"deploying-nextjs-apps-to-cloudflare-workers-with-the-opennext-adapter"],"excerpt":[0,"With the 1.0-beta release of the Cloudflare adapter for OpenNext, you can host your Next.js 14 and 15 applications on Cloudflare Workers."],"featured":[0,false],"html":[0,"
We first announced the Cloudflare adapter for OpenNext at Builder Day 2024. It transforms Next.js applications to enable them to run on Cloudflare’s infrastructure.
Over the seven months since that September announcement, we have been working hard to improve the adapter. It is now more tightly integrated with OpenNext to enable supporting many more Next.js features. We kept improving the Node.js compatibility of Workers and unenv was also improved to polyfill the Node.js features not yet implemented by the runtime.
With all of this work, we are proud to announce the 1.0.0-beta release of @opennextjs/cloudflare. Using the Cloudflare adapter is now the preferred way to deploy Next applications to the Cloudflare platform, instead of Next on Pages.
Read on to learn what is possible today, and about our plans for the coming months.
OpenNext is a build tool designed to transform Next.js applications into packages optimized for deployment across various platforms. Initially created for serverless environments on AWS Lambda, OpenNext has expanded its capabilities to support a wider range of environments, including Cloudflare Workers and traditional Node.js servers.
By integrating with the OpenNext codebase, the Cloudflare adapter is now able to support many more features than its original version. We are also leveraging the end-to-end (e2e) test suite of OpenNext to validate the implementation of these features.\n\nBeing part of OpenNext allows us to support future Next.js features shortly after they are released. We intend to support the latest minor version of Next.js 14 and all the minor versions of Next.js 15.
Most of the Next.js 15 features are supported in @opennextjs/cloudflare. You can find an exhaustive list on the OpenNext website, but here are a few highlights:
Middleware allows modifying the response by rewriting, redirecting, or modifying the request and response headers, or responding directly before the request hits the app.
The adapter easily integrates with Cloudflare Images to deliver optimized images.
We are working on adding more features:
Microsoft Windows is not yet fully supported by the adapter. We plan to fully support Windows for development in the 1.0 release.
The adapter currently only supports the Node runtime of Next.js. You can opt-out of the Edge runtime by removing export const runtime = "edge" from your application. We plan to add support for the edge runtime in the next major release. Note that applications deployed to Cloudflare Workers run close to the user, whatever the Next.js runtime used, giving similar performance.
Composable caching (use cache) should also be supported in the next major release. It is a canary feature of Next.js that is still in development. It will be supported in OpenNext once it stabilizes.
While the adapter has vastly improved over the last several months, we should also mention the updates to the ecosystem that are enabling more applications to be supported.
NodeJS compatibility for Workers is becoming more comprehensive with the crypto, dns, timers, tls, and net NodeJS modules now being natively implemented by the Workers runtime. The remaining modules that are not yet implemented are supported through unenv.
The Worker size limit was bumped from 1 MiB to 3 MiB on free plans and from 10 MiB to 15 MiB for paid plans.
You can then iterate on your application using the Next.js dev server by running npm run dev.
Once you are happy with your application in the development server, you can run the application on Workers locally by executing npm run preview, or deploy the application with npm run deploy.
"],"published_at":[0,"2025-04-08T14:00+00:00"],"updated_at":[0,"2025-04-08T15:12:52.153Z"],"feature_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/1Gg8cO6ERWqiDNog07xt9q/6acf1db0494a091914745c44a2948f0f/image1.png"],"tags":[1,[[0,{"id":[0,"2xCnBweKwOI3VXdYsGVbMe"],"name":[0,"Developer Week"],"slug":[0,"developer-week"]}],[0,{"id":[0,"6hbkItfupogJP3aRDAq6v8"],"name":[0,"Cloudflare Workers"],"slug":[0,"workers"]}]]],"relatedTags":[0],"authors":[1,[[0,{"name":[0,"Dario Piotrowicz"],"slug":[0,"dario"],"bio":[0,null],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/23kOes6NFYUjdoPUWdaUdq/485828df0ae319f08e6b01c0acc6486e/dario.png"],"location":[0,"UK"],"website":[0,null],"twitter":[0,null],"facebook":[0,null]}],[0,{"name":[0,"Victor Berchet"],"slug":[0,"victor-berchet"],"bio":[0],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/s9DHTbRIKnmQ9tRYfkJvc/adbe09b00d39831703ab5b740b3bd372/Victor_Berchet.jpg"],"location":[0],"website":[0],"twitter":[0],"facebook":[0]}],[0,{"name":[0,"Nicolas (Guest Author)"],"slug":[0,"nicolas-guest-author"],"bio":[0],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4YgP47iRhXzZUipbOYwuZS/da4f98eec3101072183ed522c96ed7be/IMG_20250326_160125.jpg"],"location":[0],"website":[0],"twitter":[0],"facebook":[0]}],[0,{"name":[0,"James Anderson (Guest Author)"],"slug":[0,"james-anderson-guest-author"],"bio":[0],"profile_image":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6VOJd4s366Q3PvzZ74WQ1g/83f99bc26107942fa21d5a77978f6eca/4PPZ5PC.jpeg"],"location":[0],"website":[0],"twitter":[0],"facebook":[0]}]]],"meta_description":[0,"With the 1.0-beta release of the Cloudflare adapter for OpenNext, you can host your Next.js 14 and 15 applications on Cloudflare Workers."],"primary_author":[0,{}],"localeList":[0,{"name":[0,"blog-english-only"],"enUS":[0,"English for Locale"],"zhCN":[0,"No Page for Locale"],"zhHansCN":[0,"No Page for Locale"],"zhTW":[0,"No Page for Locale"],"frFR":[0,"No Page for Locale"],"deDE":[0,"No Page for Locale"],"itIT":[0,"No Page for Locale"],"jaJP":[0,"No Page for Locale"],"koKR":[0,"No Page for Locale"],"ptBR":[0,"No Page for Locale"],"esLA":[0,"No Page for Locale"],"esES":[0,"No Page for Locale"],"enAU":[0,"No Page for Locale"],"enCA":[0,"No Page for Locale"],"enIN":[0,"No Page for Locale"],"enGB":[0,"No Page for Locale"],"idID":[0,"No Page for Locale"],"ruRU":[0,"No Page for Locale"],"svSE":[0,"No Page for Locale"],"viVN":[0,"No Page for Locale"],"plPL":[0,"No Page for Locale"],"arAR":[0,"No Page for Locale"],"nlNL":[0,"No Page for Locale"],"thTH":[0,"No Page for Locale"],"trTR":[0,"No Page for Locale"],"heIL":[0,"No Page for Locale"],"lvLV":[0,"No Page for Locale"],"etEE":[0,"No Page for Locale"],"ltLT":[0,"No Page for Locale"]}],"url":[0,"https://blog.cloudflare.com/deploying-nextjs-apps-to-cloudflare-workers-with-the-opennext-adapter"],"metadata":[0,{"title":[0,"Deploy your Next.js app to Cloudflare Workers with the Cloudflare adapter for OpenNext"],"description":[0,"With the 1.0-beta release of the Cloudflare adapter for OpenNext, you can host your Next.js 14 and 15 applications on Cloudflare Workers."],"imgPreview":[0,"https://cf-assets.www.cloudflare.com/zkvhlag99gkb/7sOms48C6EDeiYcUJnHey4/6e91c81d664eabdd2df9afd07d389ac5/OG_Share_2024__39_.png"]}]}]]],"locale":[0,"en-us"],"translations":[0,{"posts.by":[0,"By"],"footer.gdpr":[0,"GDPR"],"lang_blurb1":[0,"This post is also available in {lang1}."],"lang_blurb2":[0,"This post is also available in {lang1} and {lang2}."],"lang_blurb3":[0,"This post is also available in {lang1}, {lang2} and {lang3}."],"footer.press":[0,"Press"],"header.title":[0,"The Cloudflare Blog"],"search.clear":[0,"Clear"],"search.filter":[0,"Filter"],"search.source":[0,"Source"],"footer.careers":[0,"Careers"],"footer.company":[0,"Company"],"footer.support":[0,"Support"],"footer.the_net":[0,"theNet"],"search.filters":[0,"Filters"],"footer.our_team":[0,"Our team"],"footer.webinars":[0,"Webinars"],"page.more_posts":[0,"More posts"],"posts.time_read":[0,"{time} min read"],"search.language":[0,"Language"],"footer.community":[0,"Community"],"footer.resources":[0,"Resources"],"footer.solutions":[0,"Solutions"],"footer.trademark":[0,"Trademark"],"header.subscribe":[0,"Subscribe"],"footer.compliance":[0,"Compliance"],"footer.free_plans":[0,"Free plans"],"footer.impact_ESG":[0,"Impact/ESG"],"posts.follow_on_X":[0,"Follow on X"],"footer.help_center":[0,"Help center"],"footer.network_map":[0,"Network Map"],"header.please_wait":[0,"Please Wait"],"page.related_posts":[0,"Related posts"],"search.result_stat":[0,"Results {search_range} of {search_total} for {search_keyword}"],"footer.case_studies":[0,"Case Studies"],"footer.connect_2024":[0,"Connect 2024"],"footer.terms_of_use":[0,"Terms of Use"],"footer.white_papers":[0,"White Papers"],"footer.cloudflare_tv":[0,"Cloudflare TV"],"footer.community_hub":[0,"Community Hub"],"footer.compare_plans":[0,"Compare plans"],"footer.contact_sales":[0,"Contact Sales"],"header.contact_sales":[0,"Contact Sales"],"header.email_address":[0,"Email Address"],"page.error.not_found":[0,"Page not found"],"footer.developer_docs":[0,"Developer docs"],"footer.privacy_policy":[0,"Privacy Policy"],"footer.request_a_demo":[0,"Request a demo"],"page.continue_reading":[0,"Continue reading"],"footer.analysts_report":[0,"Analyst reports"],"footer.for_enterprises":[0,"For enterprises"],"footer.getting_started":[0,"Getting Started"],"footer.learning_center":[0,"Learning Center"],"footer.project_galileo":[0,"Project Galileo"],"pagination.newer_posts":[0,"Newer Posts"],"pagination.older_posts":[0,"Older Posts"],"posts.social_buttons.x":[0,"Discuss on X"],"search.icon_aria_label":[0,"Search"],"search.source_location":[0,"Source/Location"],"footer.about_cloudflare":[0,"About Cloudflare"],"footer.athenian_project":[0,"Athenian Project"],"footer.become_a_partner":[0,"Become a partner"],"footer.cloudflare_radar":[0,"Cloudflare Radar"],"footer.network_services":[0,"Network services"],"footer.trust_and_safety":[0,"Trust & Safety"],"header.get_started_free":[0,"Get Started Free"],"page.search.placeholder":[0,"Search Cloudflare"],"footer.cloudflare_status":[0,"Cloudflare Status"],"footer.cookie_preference":[0,"Cookie Preferences"],"header.valid_email_error":[0,"Must be valid email."],"search.result_stat_empty":[0,"Results {search_range} of {search_total}"],"footer.connectivity_cloud":[0,"Connectivity cloud"],"footer.developer_services":[0,"Developer services"],"footer.investor_relations":[0,"Investor relations"],"page.not_found.error_code":[0,"Error Code: 404"],"search.autocomplete_title":[0,"Insert a query. Press enter to send"],"footer.logos_and_press_kit":[0,"Logos & press kit"],"footer.application_services":[0,"Application services"],"footer.get_a_recommendation":[0,"Get a recommendation"],"posts.social_buttons.reddit":[0,"Discuss on Reddit"],"footer.sse_and_sase_services":[0,"SSE and SASE services"],"page.not_found.outdated_link":[0,"You may have used an outdated link, or you may have typed the address incorrectly."],"footer.report_security_issues":[0,"Report Security Issues"],"page.error.error_message_page":[0,"Sorry, we can't find the page you are looking for."],"header.subscribe_notifications":[0,"Subscribe to receive notifications of new posts:"],"footer.cloudflare_for_campaigns":[0,"Cloudflare for Campaigns"],"header.subscription_confimation":[0,"Subscription confirmed. Thank you for subscribing!"],"posts.social_buttons.hackernews":[0,"Discuss on Hacker News"],"footer.diversity_equity_inclusion":[0,"Diversity, equity & inclusion"],"footer.critical_infrastructure_defense_project":[0,"Critical Infrastructure Defense Project"]}],"localesAvailable":[1,[]],"footerBlurb":[0,"Cloudflare's connectivity cloud protects entire corporate networks, helps customers build Internet-scale applications efficiently, accelerates any website or Internet application, wards off DDoS attacks, keeps hackers at bay, and can help you on your journey to Zero Trust.
Visit 1.1.1.1 from any device to get started with our free app that makes your Internet faster and safer.
To learn more about our mission to help build a better Internet, start here. If you're looking for a new career direction, check out our open positions."]}" ssr="" client="load" opts="{"name":"Post","value":true}" await-children="">
The following was originally written as a guest post by Ashcon Partovi, while a computer science and business undergraduate at the University of British Columbia in Vancouver, Canada. He's the founder of a popular Minecraft multiplayer server, stratus.network, that provides competitive, team-based gameplay to thousands of players every week. He also now works at Cloudflare.
If you've ever played a video game in the past couple of years, chances are you know about Minecraft. You might be familiar with the game or even planted a tree or two, but what you might not know about is the vast number of Minecraft online communities. In this post, I'm going to describe how I used Cloudflare Workers to deploy and scale a production-grade API that solves a big problem for these Minecraft websites.
Introducing the Problem
Here is an example of my Minecraftplayer profile from one of the many multiplayer websites. It shows some identity information such as my username, a bitmap of my avatar, and a preview of my friends. Although rendering this page with 49 bitmap avatars may seem like an easy task, it's far from trivial. In fact, it's unnecessarily complicated.
Here is the current workflow to render a player profile on a website given their username:
Yikes, that's 5 complex operations required to render a single avatar! But that's not all, in my example profile, there are 49 avatars, which would require a total of 5 * 49 = 245 operations.
And that's just fetching the data, we haven't even started to serve it to players! Then you have to setup a host to serve the web traffic, ensure that the service scales with demand, handle cache expiration of assets, and deploy across multiple regions. Then you have to deploy There has to be a better way!
Prototyping with Workers
I'm a strong believer in the future of serverless computing. So naturally, when I learned how Cloudflare Workers allow you to run JavaScript code in 150+ points of presence, I started to tinker with the possibilities of solving this problem. After looking at the documentation and using the Workers playground, I quickly put together some JavaScript code that aggregated all that profile complexity into a single request.
Within a couple minutes I had my first Workers implementation! I gave it my username and it was able to make all the necessary sub-requests to return my player's bitmap texture.
After realizing the potential of Workers, I started to wonder if I could use it for more than just a single script. What if I could design and deploy a production-ready API for Minecraft that runs exclusively on Workers?
Designing an API
I wanted to address an essential problem for Minecraft developers: too many APIs with too many restrictions. The hassle of parsing multiple requests and handling errors prevents developers from focusing on creating great experiences for players. There needs to be a solution that requires only 1 HTTP request with no rate limiting and no client-side caching. After looking at the various use-cases for the existing APIs, I created a JSON schema that encompassed all the essential data into a single response:
One of the primary goals I had in mind was to minimize sub-requests by clients. For example, instead of giving developers a URL to a image/png static asset, why not fetch it for them and embed it as a base64 string? Now that's simplicity!
Getting Started
For this project, I decided to use CoffeeScript, which transcompiles to JavaScript and has a simple syntax. We'll also need to use Webpack to bundle all of our code into a single JavaScript file to upload to Cloudflare.
# Welcome to CoffeeScript!
str = "heyo! #{40+2}" # 'heyo! 42'
num = 12 if str? # 12
arr = [1, null, "apple"] # [1, null, 'apple']
val = arr[1]?.length() # null
hash = # {key: 'value'}
key: "value"
add = (a, b, {c, d} = {}) ->
c ?= 3
d ?= 4
a + b + c + d
add(1, 2, d: 5) # 1 + 2 + 3 + 5 = 11
First, let's make sure we have the proper dependencies installed for the project! These commands will create a package.json file and a node_modules/ folder in our workspace.
Before we start coding, we'll create a src/index.coffee file and make sure everything is working so far.
addEventListener('fetch', (event) ->
event.respondWith(route(event.request)))
# We will populate this with our own logic after we test it!
route = (request) ->
fetch('https://api.ashcon.app/mojang/v1/user/ElectroidFilms')
Open your terminal in the workspace/ directory and run the following commands:
npm run build
npm run preview
Your computer's default internet browser will open up a new window and preview the result of our Worker. If you see a JSON response, then everything is working properly and we're ready to go!
Writing Production Code for Workers
Now that we're setup with a working example, we can design our source code file structure. It's important that we break up our code into easily testable chunks, so I've gone ahead and outlined the approach that I took with this project:
src/
index.coffee # routing and serving requests
api.coffee # logic layer to mutate and package requests
mojang.coffee # non-logic layer to send upstream requests
http.coffee # HTTP requesting, parsing, and responding
util.coffee # util methods and extensions
If you've feeling adventurous, I've included a simplified version of my API code that you can browse through below. If you look at each file, you'll have a fully working implementation by the end! Otherwise, you can continue reading to learn about my deployment and analysis of the APIs impact.
http.coffee
Since our API will be making several HTTP requests, it's a good idea to code some common request and respond methods that can be reused among multiple requests. At the very least, we need to support parsing JSON or base64 responses and sending JSON or string data back to the client.
# Send a Http request and get a response.
#
# @param {string} url - Url to send the request.
# @param {string} method - Http method (get, post, etc).
# @param {integer} ttl - Time in seconds for Cloudflare to cache the request.
# @param {boolean} json - Whether to parse the response as json.
# @param {boolean} base64 - Whether to parse the response as a base64 string.
# @returns {promise<
# json -> [err, json]
# base64 -> string|null
# else -> response
# >} - A different response based on the method parameters above.
export request = (url, {method, ttl, json, base64} = {}) ->
method ?= "GET"
response = await fetch(url, method: method, cf: {cacheTtl: ttl} if ttl)
if json # Return a tuple of [err, json].
if err = coerce(response.status)
[err, null]
else
[null, await response.json()]
else if base64 # Return base64 string or null.
if response.ok
Buffer.from(await response.arrayBuffer(), "binary").toString("base64")
else # If no parser is specified, just return the raw response.
response
export get = (url, options = {}) ->
request(url, Object.assign(options, {method: "GET"}))
# Respond to a client with a http response.
#
# @param {object} data - Data to send back in the response.
# @param {integer} code - Http status code.
# @param {string} type - Http content type.
# @param {boolean} json - Whether to respond in json.
# @param {boolean} text - Whether to respond in plain text.
# @returns {response} - Raw response object.
export respond = (data, {code, type, json, text} = {}) ->
code ?= 200
if json
type = "application/json"
# "Pretty-print" our JSON response with 2 spaces.
data = JSON.stringify(data, undefined, 2)
else if text
type = "text/plain"
data = String(data)
else
type ?= "application/octet-stream"
new Response(data, {status: code, headers: {"Content-Type": type}})
export error = (reason = null, {code, type} = {}) ->
code ?= 500
type ?= "Internal Error"
# An example would be: "Internal Error - 500 (this is the reason)"
respond("#{code} - #{type}" + (if reason then " (#{reason})" else ""), code: code, text: true)
export badRequest = (reason = null) ->
error(reason, code: 400, type: "Bad Request")
export notFound = (reason = null) ->
error(reason, code: 404, type: "Not Found")
export tooManyRequests = (reason = null) ->
error(reason, code: 429, type: "Too Many Requests")
# Convert common http error codes into error responses.
#
# @param {integer} code - Http status code.
# @returns {response|null} - An error response or null if a 200 code.
export coerce = (code) ->
switch code
when 200 then null
# Some Minecraft APIs use 204 as a stand-in for a 404.
when 204 then notFound()
when 400 then invalidRequest()
# Theoretically this should never happen, but sometimes does.
when 429 then tooManyRequests()
else error("Unknown Response", code: code)
The cf key can be used to control various Cloudflare features, including how sub-requests are cached. See the Workers documentation for a more in-depth explanation.
cf:
cacheTtl: 120 # Cache for 2 mins.
# Pro+ only.
polish: "lossless" # Compress image data.
# Enterprise only.
cacheTtlByStatus:
"200-299": 60 # Cache for 60 secs.
"300-399": 0 # Cache but expire instantly.
"400-404": 10 # Cache for 10 secs.
"405-599": -1 # Do not cache at all.
cacheKey: url # Cache lookup key, defaults to the request URL.
mojang.coffee
Now that we have code to send and parse requests, we can create an interface to retrieve data from the upstream APIs. It's good to note that there should be no mutation logic in this file, it's purpose is just to get the old APIs, not change them.
import { get } from "./http"
# Get the UUID of a username at the current time.
#
# @param {string} name - Minecraft username.
# @throws {204} - When no user exists with that name.
# @returns {[err, json]} - An error or username and UUID response.
export usernameToUuid = (name) ->
get("https://api.mojang.com/users/profiles/minecraft/#{name}", json: true)
# Get the history of usernames for the given UUID.
#
# @param {string} id - The UUID to check the username history.
# @returns {[err, json]} - An error or the username history.
export uuidToUsernameHistory = (id) ->
get("https://api.mojang.com/user/profiles/#{id}/names", json: true)
# Get the session profile of the UUID.
#
# @param {string} id - UUID to get the session profile.
# @returns {[err, json]} - An error or the session profile.
export uuidToProfile = (id) ->
get("https://sessionserver.mojang.com/session/minecraft/profile/#{id}", json: true)
api.coffee
This is where the bulk of our API logic will reside. I've broken up the process into 3 interdependent tasks that are executed in order:
Given a username, fetch its UUID.
Given a UUID, fetch the user's profile.
Given a user's profile, decode and fetch the textures.
import { get, respond, error, notFound, badRequest } from "./http"
import { usernameToUuid, uuidToProfile, uuidToUsernameHistory } from "./mojang"
# Get the uuid of a user given their username.
#
# @param {string} name - Minecraft username, must be alphanumeric 16 characters.
# @returns {[err, response]} - An error or the dashed uuid of the user.
export uuid = (name) ->
if name.asUsername() # Fits regex of a Minecraft username.
[err, res] = await usernameToUuid(name)
if id = res?.id?.asUuid(dashed: true)
[null, respond(id, text: true)]
else # Response was received, but contains no UUID.
[err || notFound(), null]
else
[badRequest("malformed username '#{name}'"), null]
# Get the full profile of a user given their uuid or username.
#
# @param {string} id - Minecraft username or uuid.
# @returns {[err, json]} - An error or user profile.
export user = (id) ->
if id.asUsername()
[err, res] = await uuid(id)
if err # Could not find a player with that username.
[err, null]
else # Recurse with the new UUID.
await user(id = await res.text())
else if id.asUuid()
# Fetch the profile and usernames in parallel.
[[err0, profile], [err1, history]] = await Promise.all([
uuidToProfile(id = id.asUuid())
uuidToUsernameHistory(id)])
# Extract the textures from the profile.
# Since this operation is complex, off-load
# the logic into its own method.
[err2, texture] = await textures(profile)
if err = err0 || err1 || err2
[err, null] # One of the last three operations failed.
else
# Everything is good, now just put the data together.
[null, respond(
uuid: profile.id.asUuid(dashed: true)
username: profile.name
username_history: history.map((item) ->
username: item.name
changed_at: item.changedToAt?.asDate())
textures: texture
cached_at: new Date(),
json: true)]
else
[badRequest("malformed uuid '#{id}'"), null]
# Parse and decode base64 textures from the user profile.
#
# @param {json} profile - User profile from #uuidToProfile(id).
# @returns {json} - Enhanced user profile with more convient texture fields.
textures = (profile) ->
unless profile # Will occur if the profile api failed.
return [error("no user profile found"), null]
properties = profile.properties
if properties.length == 1
texture = properties[0]
else
texture = properties.filter((pair) -> pair.name == "textures" && pair.value?)[0]
# If a embedded texture does not exist or is empty,
# that user does not have a custom skin.
if !texture || (texture = JSON.parse(atob(texture.value)).textures).isEmpty()
skinUrl = "http://assets.mojang.com/SkinTemplates/steve.png"
# Fetch the skin and cape data in parallel, and cache for a day.
[skin, cape] = await Promise.all([
get(skinUrl ?= texture.SKIN?.url, base64: true, ttl: 86400)
get(capeUrl = texture.CAPE?.url, base64: true, ttl: 86400)])
unless skin
[error("unable to fetch skin '#{skinUrl}'"), null]
else
texture =
slim: texture.SKIN?.metadata?.model == "slim"
skin: {url: skinUrl, data: skin}
cape: {url: capeUrl, data: cape} if capeUrl
[null, texture]
index.coffee
Now, we parse the request's route and respond with the corresponding API.
import "./util"
import { notFound } from "./http"
import { uuid, user } from "./api"
addEventListener("fetch", (event) ->
event.respondWith(route(event.request)))
route = (request) ->
[base, version, method, id] = request.url.split("/")[3..6]
if base == "mojang" && id?
if version == "v1"
v1(method, id)
else
notFound("unknown api version '#{version}'")
else
notFound("unknown route")
v1 = (method, id) ->
if method == "uuid"
[err, res] = await uuid(id)
else if method == "user"
[err, res] = await user(id)
err || res || notFound("unknown v1 route '#{method}'")
util.coffee
Finally, we'll add some prototype extensions that we used along the way.
# Insert a string at a given index.
#
# @param {integer} i - Index to insert the string at.
# @param {string} str - String to insert.
String::insert = (i, str) ->
this.slice(0, i) + str + this.slice(i)
# Ensure that the string is a valid Uuid.
#
# If dashed is enabled, it is possible the input
# string is not the same as the output string.
#
# @param {boolean} dashed - Whether to return a dashed uuid.
# @returns {string|null} - A uuid or null.
String::asUuid = ({dashed} = {}) ->
if match = uuidPattern.exec(this)
uuid = match[1..].join("")
if dashed
uuid.insert(8, "-")
.insert(12+1, "-")
.insert(16+2, "-")
.insert(20+3, "-")
else
uuid
uuidPattern = /^([0-9a-f]{8})(?:-|)([0-9a-f]{4})(?:-|)(4[0-9a-f]{3})(?:-|)([0-9a-f]{4})(?:-|)([0-9a-f]{12})$/i
# Ensure that the string is a valid Minecraft username.
#
# @returns {string|null} - Minecraft username or null.
String::asUsername = ->
if usernamePattern.test(this) then this else false
usernamePattern = /^[0-9A-Za-z_]{1,16}$/i
# Ensure that the unix number is a Date.
#
# @returns {date} - The number as a floored date.
Number::asDate = ->
new Date(Math.floor(this))
# Determine if the object is empty.
#
# @returns {boolean} - Whether the object is empty.
Object::isEmpty = ->
Object.keys(this).length == 0
Analyzing a Workers Deployment
I've had this code deployed and tested by real Minecraft users for the past few weeks. As a developer that has global web traffic, it's pivotal that players can quickly get access to my services. The essential advantage of Workers is that I don't need to deploy several replicas of my code to different cloud regions, it's everywhere! That means players from any part of the world get the same great web experience with minimal latency.
As of today, the API is processing over 400k requests per day from users all over the world! Cloudflare caches responses in the closest point of presence to the client, so I don't need to setup a database and developers don't need to worry about rate-limiting.
Since each request to the API generates 4 to 5 additional sub-requests, it handles approximately 1.8 million fetches per day with a 88% cache hit rate.
Wrapping Up
Cloudflare Workers have enabled me to solve complex technical problems without worrying about host infrastructure or cloud regions. It's simple, easy to deploy, and works blazing fast all around the world. And for 50 cents for every 1 million requests, it's incomparable to the other serverless solutions on the market.
If you're not already convinced to start using Workers, here's the deployment history of my API. I went from 0 to 5 million requests with no scaling, no resizing, no servers, no clusters, and no containers. Just code.
Visit 1.1.1.1 from any device to get started with our free app that makes your Internet faster and safer.
To learn more about our mission to help build a better Internet, start here. If you're looking for a new career direction, check out our open positions.
We’ve improved Observability for Workers by announcing the General Availability of Workers Logs and the introduction of the Query Builder to help you investigate log events across all of your Workers....
You can now deploy static sites and full-stack applications on Cloudflare Workers. Framework support for React Router v7, Astro, Vue and more are generally available today and Cloudflare Vite plugin. ...