Skip to content

Migrate to the Durable Object scheduling policy

Last updated View as MarkdownAgent setup

You cannot change the scheduling policy of an existing Container application. To move from the default policy to the durable_object policy, create a replacement Container application and cut traffic over to it.

The replacement application must use a new Durable Object class and namespace. The old and replacement applications cannot attach to the same Durable Object namespace.

Before you begin

  • Replace the Container class. The Container class does not support the durable_object policy. If your existing class extends Container, the replacement class must use the Durable Object Container API directly. Rebuild any Container class helpers your application relies on, such as port readiness checks, request proxying, and sleep timeouts. Refer to Migrate to the Durable Object Container API.
  • Check how the Worker declares Durable Object classes. A Worker uses either the exports field or the legacy migrations array, not both. Declare the replacement class with the same mechanism the Worker already uses. Moving from migrations to exports cannot be undone. If you want to move, do it as a separate change. Refer to Migrate from the legacy migrations flow.
  • Move external images to the Cloudflare managed registry. Named images with the durable_object policy must use a Dockerfile or a digest-pinned reference in the Cloudflare managed registry. If the existing image references Docker Hub, Amazon ECR, or Google Artifact Registry, push the image to the Cloudflare managed registry first.

Configuration changes

Map the existing application configuration to its durable_object equivalent:

default policy durable_object policy
image (Dockerfile path) dockerfile on a named images entry, selected with ctx.container.start()
image (registry reference) image on a named images entry. Must be a digest-pinned reference in the Cloudflare managed registry
image_build_context build_context on the named image
image_vars build_vars on the named image
instance_type The instance option in ctx.container.start(). Refer to Instance size changes
max_instances Not supported. Running instances count toward account limits. Enforce any per-application cap in application code
Application-wide image rollouts Application code that stops and starts each Container
observability Keep in Wrangler configuration, subject to durable_object restrictions
unsafe.configuration.experimental_flags Keep in Wrangler configuration
ssh and authorized_keys Keep in Wrangler configuration
Placement constraints, rollout settings, wrangler_ssh, and trusted_user_ca_keys Not supported on the replacement application

For the complete field compatibility list, refer to Wrangler configuration.

Instance size changes

With the durable_object policy, you set the instance size with the instance option of ctx.container.start() instead of instance_type in Wrangler configuration. instance accepts lite, standard-1, standard-2, standard-3, and standard-4. If your existing application uses one of the following instance_type values, choose a replacement:

  • dev: Use lite.
  • standard: Use standard-1.
  • basic: Choose lite or standard-1. Custom instance types require at least 1 vCPU, so you cannot reproduce basic with a custom instance.

Custom instance objects use camel case at runtime. Rename memory_mib to memoryMib and disk_mb to diskMb. Refer to Choose an instance size at runtime.

Move the application

  1. Add a new Durable Object class and Container application.

    You cannot move the existing application by changing its scheduling_policy to durable_object, because the scheduling policy cannot be changed after creation. You also cannot reuse the existing Durable Object class, because each Durable Object namespace attaches to one Container application.

    Instead, add all of the following to the Wrangler configuration:

    • A new SQLite-backed Durable Object class.
    • A Durable Object binding for the new class.
    • A new Container entry with "scheduling_policy": "durable_object", a different name, and class_name set to the new class.

    Leave the existing Container entry, Durable Object class, and binding unchanged so that you can route traffic back to them. Changing the existing entry's name or class_name can cause Wrangler to create a new application instead of updating the existing one.

    The following example uses exports. If the Worker uses the legacy migrations array, add a new migration with new_sqlite_classes: ["DurableSandbox"] instead.

    {
        "name": "sandbox-worker",
        "main": "src/index.ts",
        "compatibility_date": "2026-09-29",
        "containers": [
            // Existing application. Keep this entry unchanged.
            {
                "class_name": "Sandbox",
                "image": "./container/Dockerfile",
                "instance_type": "standard-2",
                "max_instances": 10,
            },
            // Replacement application.
            {
                "name": "sandbox-durable-object",
                "class_name": "DurableSandbox",
                "scheduling_policy": "durable_object",
                "images": {
                    "base": {
                        "dockerfile": "./container/Dockerfile",
                    },
                },
            },
        ],
        "durable_objects": {
            "bindings": [
                {
                    "name": "SANDBOX",
                    "class_name": "Sandbox",
                },
                {
                    "name": "DURABLE_SANDBOX",
                    "class_name": "DurableSandbox",
                },
            ],
        },
        "exports": {
            "Sandbox": {
                "type": "durable-object",
                "storage": "sqlite",
            },
            "DurableSandbox": {
                "type": "durable-object",
                "storage": "sqlite",
            },
        },
    }
    name = "sandbox-worker"
    main = "src/index.ts"
    compatibility_date = "2026-09-29"
    
    [[containers]]
    class_name = "Sandbox"
    image = "./container/Dockerfile"
    instance_type = "standard-2"
    max_instances = 10
    
    [[containers]]
    name = "sandbox-durable-object"
    class_name = "DurableSandbox"
    scheduling_policy = "durable_object"
    
    [containers.images.base]
    dockerfile = "./container/Dockerfile"
    
    [[durable_objects.bindings]]
    name = "SANDBOX"
    class_name = "Sandbox"
    
    [[durable_objects.bindings]]
    name = "DURABLE_SANDBOX"
    class_name = "DurableSandbox"
    
    [exports.Sandbox]
    type = "durable-object"
    storage = "sqlite"
    
    [exports.DurableSandbox]
    type = "durable-object"
    storage = "sqlite"
  2. Move startup configuration into the replacement class.

    Select the image and instance size when the replacement Durable Object starts its Container:

    src/index.jsjs
    import { DurableObject } from "cloudflare:workers";
    
    export class DurableSandbox extends DurableObject {
    	startContainer() {
    		if (this.ctx.container.running) {
    			return;
    		}
    
    		this.ctx.container.start({
    			image: this.ctx.container.images.base,
    			instance: "standard-2",
    			// Match the existing application's outbound access.
    			enableInternet: true,
    		});
    	}
    }
    src/index.tsts
    import { DurableObject } from "cloudflare:workers";
    
    export class DurableSandbox extends DurableObject {
        startContainer() {
            if (this.ctx.container.running) {
                return;
            }
    
            this.ctx.container.start({
                image: this.ctx.container.images.base,
                instance: "standard-2",
                // Match the existing application's outbound access.
                enableInternet: true,
            });
        }
    }

    Move other supported startup settings, such as env and entrypoint, into the same call. Set enableInternet to match the existing application. The Container class allows outbound Internet access unless you set enableInternet = false.

    ctx.container.start() returns before the Container is ready to accept requests. Add an application-specific readiness check before sending traffic to the Container.

  3. Deploy and validate the replacement application.

    Deploy both applications:

    npx wrangler deploy

    Start a replacement Container without changing production routing, for example through a test-only route that uses the DURABLE_SANDBOX binding. Confirm that the image, instance size, environment, entrypoint, network access, and readiness behavior match the existing application.

  4. Cut traffic over to the replacement namespace.

    Update the Worker routing logic to resolve Container IDs through the replacement Durable Object binding. The same name resolves to a different Durable Object in each namespace. A Durable Object in the replacement namespace cannot access storage from the old namespace.

    Choose the binding per logical Container, not per request. If requests for the same name can reach both bindings, such as during a percentage-based rollout or a gradual deployment, two Containers can run for one logical sandbox and their state can diverge. For example, record which namespace each sandbox uses and read that record when routing:

    // isMigrated() is application code that reads a per-sandbox record.
    const binding = (await isMigrated(sandboxName))
        ? env.DURABLE_SANDBOX
        : env.SANDBOX;
    const sandbox = binding.getByName(sandboxName);

    If Durable Object state must move, complete the application-specific transfer for each sandbox before routing it to the replacement namespace.

  5. Observe the replacement application.

    Keep the old application, class, and binding during the observation period. To roll back, route traffic to the old Durable Object binding again.

    Writes made after cutover stay in the replacement namespace. If the application accepts writes, plan how to reconcile that data before rolling back.

  6. Delete the old Container application.

    After the rollback period, remove the old Container entry from the Wrangler configuration and the old routing path from the Worker code. Deploy the updated Worker. Keep the old Durable Object class and binding until you no longer need its stored data.

    Removing the entry from Wrangler configuration does not delete the existing Container application. List the applications and copy the old application ID:

    npx wrangler containers list

    Delete the old application:

    npx wrangler containers delete <OLD_APPLICATION_ID>

    This command deletes the application and its Container instances.

    Delete the old Durable Object class only when its stored data is no longer needed. Deleting a Durable Object class permanently deletes its namespace and stored data. Refer to Durable Object class exports or Durable Object class migrations (legacy) for the applicable cleanup process.

Was this helpful?