Containers on Cloudflare Workers: The full picture

Cloudflare has been working on a container platform for some time, and it's finally ready for public testing. In late June 2025, Containers will enter open beta, giving developers access to a model that merges the simplicity of Workers with the power of full Linux environments.

Workers are great for lightweight, event-driven code, but they have limits. Containers fill the gaps: running user-generated code in any language, executing CLI tools that need a complete Linux environment, using several gigabytes of memory or multiple CPU cores, or porting an existing application from AWS, GCP, or Azure without a major rewrite.

The key architectural idea is a deep integration with Workers and Durable Objects. Rather than forcing developers into YAML-heavy configuration and external control planes, the platform lets Workers act as an API gateway (handling routing, authentication, caching, and rate-limiting before a request hits a container), a service mesh (creating private connections between containers with programmable routing), and an orchestrator (for custom scheduling, scaling, and health check logic). If you need to extend the platform, you write code — not Kubernetes operators or control plane configuration.

Three deployment patterns, one config format

Containers deploy through the same wrangler deploy command you already use for Workers. A container image is built from your local project, pushed to Cloudflare's registry, and made ready to boot across the globe. The Worker is deployed alongside it.

BLOG-2799 Feature Image

Stateful: code execution per user session

Suppose you're building a platform where users can run LLM-generated code. The code isn't trusted, so each user gets an isolated sandbox that persists state between requests. You need an on-demand container per session.

The config is minimal — a single container declaration. In your Worker, you get the container through the binding and fetch it with an ID. The first call boots the container; it sleeps after a configurable idle timeout. You pay only while the container actively runs.

import { Container } from "cloudflare:workers";

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname.startsWith("/execute-code")) {
      const { sessionId, messages } = await request.json();
      // pass in prompt to get the code from Llama 4
      const codeToExecute = await env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", { messages });

      // get a different container for each user session
      const id = env.CODE_EXECUTOR.idFromName(sessionId);
      const sandbox = env.CODE_EXECUTOR.get(id);

      // execute a request on the container
      return sandbox.fetch("/execute-code", { method: "POST", body: codeToExecute });
    }

    // ... rest of Worker ...
  },
};

// define your container using the Container class from cloudflare:workers
export class CodeExecutor extends Container {
  defaultPort = 8080;
  sleepAfter = "1m";
}

Cloudflare handles pre-warming and caching automatically. New containers boot in a Cloudflare location — a point of presence (PoP) — close to the incoming request, and low-latency workloads are served regardless of region.

Autoscaled: FFmpeg in the cloud

Without state, the workflow gets even simpler. A stateless, autoscaling application can be declared directly and run everywhere. For example, a service that converts video to animated GIFs with FFmpeg needs no session affinity, but you still want to avoid streaming data across an ocean. To achieve this, a container is declared in Wrangler config, with autoscaling enabled.

$ wrangler deploy

This configuration will create a global rollout across Cloudflare's network, accessible via a single Worker!

Routing is just one line: env.GIF_MAKER.fetch sends requests to the closest running instance.

"containers": [
  {
    "class_name": "GifMaker",
    "image": "./Dockerfile", // container source code can be alongside Worker code
    "instance_type": "basic",
    "autoscaling": {
      "minimum_instances": 1,
      "cpu_target": 75,
    }
  }
],
// ...rest of wrangler.jsonc...

Deployment happens in one command: wrangler deploy — no more provisioning, artifact registries, or region selection.

Inside the architecture: Durable Objects as sidecars

All of this routing is built on Durable Objects. The Container class from cloudflare:workers is wrap-around that exposes helper methods for common patterns. Under it all, the Durable Object has a container handle and the ability to manage its lifecycle.

import { Container } from "cloudflare:workers";

export class GifMaker extends Container {
  defaultPort: 1337,
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname === "/make-gif") {
      return env.GIF_MAKER.fetch(request)
    }

    // ... rest of Worker ...
  },
};

The real power comes from treating a Durable Object as a programmable sidecar for the container. A code_executor Durable Object proxies requests to its associated container and can extend its functionality at the protocol level.

  • Manual control: You can start, stop, and execute commands directly on a container by calling RPC methods on its Durable Object. A new container object on the context gives you methods with names like start(), stop(), fetch(), and exec().
  • Status hooks: You can monitor lifecycle events and run logic in response. For example, a job runner may need to post the result to a Queue after the container exits — you define that behavior in a containerStatus() hook.
  • State persistence: When a container will sleep and changes state, you need to capture that state and restore it on the next start. Two special hooks let you persist and reload state: containerSleep() to write data to storage (from a sidecar Worker), and containerStart() to restore it before the container boots.

The added benefit: everything communicates through the Durable Object, meaning you can write code to route, transform, and manage requests without needing to touch network config.

Extending the platform with Workers

Enforcing policy with an API gateway

Ingress control in Workers lets you implement precisely the gateway logic you want. A typical pattern is version-based routing: when a request comes in with a custom header, route it to one container version; otherwise use the stable one. Rate-limiting and authentication also work here, so you can fail early before a request ever gets to your container.

BLOG-2799 Image 1

The integration with Workers also matters for egress. By default, containers are private and can only be accessed via Workers, which can connect to them by multiple ports. The connection remains encrypted from the end user all the way to the container's port and, for egress, data is relayed through Cloudflare's network — no TLS certificates to manage in-device. Even better for LLM-ish patterns, Workers can connect to a container via WebSocket (see the repository with demos).

This trait has an interesting side effect: the architecture works like a service mesh. Egress is locked down by default unless you explicitly enable Internet access and opt into an outbound proxy behavior. Combined with crypto egress and External Service protection, you get a security boundary that is simple now and hardened by default.

class MyContainer extends DurableObject {
  // these RPC methods are callable from a Worker
  async customBoot(entrypoint, envVars) {
    this.ctx.container.start({ entrypoint, env: envVars });
  }

  async stopContainer() {
    const SIGTERM = 15;
    this.ctx.container.signal(SIGTERM);
  }

  async startBackupScript() {
    await this.ctx.container.exec(["./backup"]);
  }
}

With egress confined to logic in a Worker, you can shape what containers can call the moment they boot, rather than patching firewalls later.

Orchestration logic you can read

Cloudflare doesn't force a specific scheduling policy. Suppose you want to pre-warm containers that load external data over a slow startup process; just call an RPC on a manager Durable Object, which then executes a scheduled instance count in a specific region via a location hint. Health checks are still Workers code — you simply define a getStatus() method on a custom Container class and read its result. This isn't just toy code; there's a self-contained load-balancer example that shows how a standard Durable Object can route based on live instance state.

class BuilderContainer extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env)
    async function onContainerExit() {
      await this.env.QUEUE.send({ status: "success", message: "Build Complete" });
    }

    async function onContainerError(err) {
      await this.env.QUEUE.send({ status: "error", message: err});
    }

    this.ctx.container.start();
    this.ctx.container.monitor().then(onContainerExit).catch(onContainerError); 
  }

  async isRunning() { return this.ctx.container.running; }
}

Composing with Workflows and Agents

Containers get far more interesting when Agents (LLM-tool-calling agents, for example) and Workflows come into play.

Durable execution with a compression container

Workflows are ideal for larger batch jobs like take-from-R2, compress-to-tar, push-back-to-R2. The container handles FFmpeg compression directly; the Worker supplies the logic for retries and cleanup. Workflows keep it durable and everything happens with Workers-native auth — no complicated token handling.

import { startAndWaitForPort } from "./helpers"

class MyContainer extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env)
    this.ctx.blockConcurrencyWhile(async () => {
      this.ctx.storage.sql.exec('CREATE TABLE IF NOT EXISTS state (value TEXT)');
      this.ctx.storage.sql.exec("INSERT INTO state (value) SELECT '' WHERE NOT EXISTS 
(SELECT * FROM state)");
      await startAndWaitForPort(this.ctx.container, 8080);
      await this.setupContainer();
      this.ctx.container.monitor().then(this.onContainerExit); 
    });
  }

  async setupContainer() {
    const initialState = this.ctx.storage.sql.exec('SELECT * FROM state LIMIT 1').one().value;
    return this.ctx.container
      .getTcpPort(8080)
      .fetch("http://container/state", { body: initialState, method: 'POST' });
  }

  async onContainerExit() {
    const response = await this.ctx.container
      .getTcpPort(8080)
      .fetch('http://container/state');
    const newState = await response.text();
    this.ctx.storage.sql.exec('UPDATE state SET value = ?', newState);
  }
}

Because R2's secure URLs can be provided inline into the container start command, a single Workflow step can be triggered on schedule or event, guaranteed to succeed while the config stays tiny.

Container tools for Agents

AI agents that return "cloud infrastructure as code" output sometimes need to invoke tools that aren't HTTP APIs, like Terraform's CLI. With Containers, an Agent tool executed on demand could run terraform plan  inside a sandbox, passing the output back to the model.

export default {
  async fetch(request, env) {
    const isExperimental = request.headers.get("x-version") === "experimental";
    
    if (isExperimental) {
      return env.MY_SERVICE_EXPERIMENTAL.fetch(request);
    } else {
      return env.MY_SERVICE_STANDARD.fetch(request);
    }
  },
};

The full example is in the cloudflare/containers-demos repo. Containers are surprisingly straightforward to wire into the Agent intent/inference loop, turning CLI tools into remote, state-managed tools.

With the basic container primitives on Workers — routing, lifecycle, autoscaling defaults — putting a complex app in production now takes a couple of days of learning instead of months. Cloudflare Containers feel like the same standard container you'd run today, but with most of the boring, unavoidable-and-slow platform-agnostic boilerplate collapsing into Workers-native config.

Containers are currently in private preview with a waitlist; open beta begins at the end of June 2025. For more demos, see the cloudflare/containers-demos repository.

Pricing Computed Per Use, Not Per Instance

Cloudflare Containers is introducing a per-use billing model that aligns cost with actual workload activity. You are charged only while a container is processing a request or has been manually started. Charges cease when the container sleeps, which can be configured to happen automatically after a set timeout. This structure supports scaling to zero and maintains high utilization even under unpredictable traffic patterns.

Billing is metered in 10ms increments during active execution at these rates:

  • Memory: $0.0000025 per GB-second
  • CPU: $0.000020 per vCPU-second
  • Disk: $0.00000007 per GB-second

Monthly egress is free up to 1 TB. Beyond that, outbound data transfer from a Container is priced per region. The final regional pricing structure will be finalized before the beta launch, with the goal of clear and transparent rates across all dimensions.

For lighter tasks, Workers remain the more economical choice because they do not bill for time spent waiting on I/O. With Cloudflare's unified platform, routing a request to either a Worker or a Container is a simple code-level decision, making it straightforward to match each part of an application with the most cost-effective compute type.

Comparing Real-World Costs

Direct comparisons between container and function services are often misleading due to differing use cases. Cloudflare points to its own acquisition, Baselime, as an example. Before moving, Baselime was a heavy AWS Lambda user; after migrating to Cloudflare, their cloud compute bill dropped by 80%.

To illustrate how costs might stack up, we can look at a representative hybrid application. This example does not seek to disadvantage other platforms but to provide a realistic estimate for a specific workload. The comparison below pits Cloudflare Containers plus Workers against Google Cloud Run, a respected container platform.

Example Application Setup

Consider an application handling 50 million requests each month, where every request requires an average of 500 ms of wall-time. Half of these requests need the full resources of a container, while the other half can be handled entirely by serverless functions.

Requests per month

Wall-time (duration)

Compute required

Cloudflare

Google Cloud

25 million

500ms

Container + serverless functions

Containers + Workers

Google Cloud Run + Google Cloud Run Functions

25 million

500ms

Serverless functions

Workers

Google Cloud Run Functions

Container Costs

On Cloudflare and Cloud Run, a single container instance can handle 50 concurrent requests. In this scenario, that instance is sized with 4 GB of memory and half of a vCPU. To serve 25 million requests at 500 ms each, the total required compute comes to 625,000 seconds.

For this model, the traffic is bursty, so we avoid paying for idle time. Therefore, the Cloud Run estimate uses its request-based pricing model rather than an always-on instance.

Price per vCPU second

Price per GB-second of memory

Price per 1m requests

Monthly Price for Compute + Requests

Cloudflare Containers

$0.000020

$0.0000025

$0.30

$20.00

Google Cloud Run

$0.000024

$0.0000025

$0.40

$23.75

* Comparison does not include free tiers for either provider and uses a single Tier 1 GCP region.

The compute pricing between the two platforms is broadly similar. However, the differentiating factor is not the per-second rate but the architectural integration. With Cloudflare, containers run on-demand globally without the need to configure or manage regions, and each container includes a programmable sidecar with its own database via Durable Objects.

Function Costs

The remaining requests, which need less compute, are handled by Workers on the Cloudflare side and by Cloud Run Functions on the Google side. Supported languages for this tier include JavaScript, TypeScript, Python, and Rust.

These 25 million requests also take 500 ms, but they spend only 20 ms of that time actively using the CPU. The other 480 ms is spent waiting on I/O. Because Workers only bill for active CPU time, this scenario presents a significant cost saving. This ratio—high wall time versus low CPU time—is common in AI applications that wait for inference results, or in standard REST APIs that are often idle while waiting for database or network responses.

Cloud Run Functions, in contrast, are billed for the entire wall-time execution on an instance with 0.083 vCPU and 128 MB of memory.

Total Price for “wall-time”

Total Price for “CPU-time”

Total Price for Compute + Requests

Cloudflare Workers

N/A

$0.83

$8.33

Google Cloud Run Functions

$1.44

N/A

$11.44

* Comparison does not include free tiers and uses a single Tier 1 GCP region.

This estimate assumes the Cloud Run Functions are configured with a maximum of 20 concurrent requests per instance. On that platform, optimal concurrency settings vary by the function’s efficiency and the acceptable tail latency, and getting it wrong can lead to either idle resource cost or performance degradation. Workers avoid this configuration task entirely by scaling horizontally across over 300 locations without user-defined concurrency limits.

The Sum of All Costs

The true measure of value is the total cost of development and operation across an entire application lifecycle. In modern systems, no single compute model is the best fit for every component. Many applications are integrating generative AI and making inference requests to LLMs, which involves communicating with many external services. These systems are often real-time, hold long-lived connections, and operate in parallel. The days of running a single monolithic VM or container for everything are gone.

Developers in 2025 are mixing many compute types to build their products. The friction of doing so is the real question, and the answer lies in the platform’s ability to shift traffic between models. Cloudflare supports this by letting a Worker act as a request router. From a single Worker, you can rate-limit, serve server-side rendered pages and static assets, handle authentication, make inference requests to AI models, or run business logic via Workflows. The Worker can then delegate to a container for heavier compute with no more complexity than an if-else statement.

Public Beta Timeline

The open beta for Cloudflare Containers is scheduled for late June 2025. Cloudflare states that they are currently gathering feedback and finalizing the APIs. The team’s stated intent is to deliver an integrated platform where containers, Workers, Workflows, and Agents operate as one cohesive system for building complete, modern applications.

BLOG-2799 Image 4