Skip to content

Durable Object class exports

The exports field in your Wrangler configuration file is the declarative way to manage Durable Object class lifecycle. You declare each Durable Object class your Worker exports — along with whether it is live, deleted, renamed, or transferred — and Cloudflare reconciles your declaration against the namespaces that have already been provisioned for your Worker.

This page covers how to:

  • Create a new Durable Object class.
  • Delete a Durable Object class and its data.
  • Rename a Durable Object class.
  • Transfer a Durable Object class between Workers.

How exports works

When you deploy a Worker that declares exports, Cloudflare compares three sources of truth:

  1. Your code — the set of Durable Object classes the Worker actually exports.
  2. Your exports configuration — what you have declared about each class.
  3. Provisioned state — the Durable Object namespaces that already exist for this Worker.

A class that appears only in your code is ignored until you declare it in exports; Cloudflare does not provision a namespace implicitly. Other disagreements, such as a live entry whose class is missing from code or a provisioned namespace with no matching entry, are surfaced as structured errors or, where the intent is unambiguous, actions that Cloudflare applies for you.

A minimal exports block declares each Durable Object class as a live entry:

JSONC
{
"durable_objects": {
"bindings": [
{
"name": "MY_DURABLE_OBJECT",
"class_name": "MyDurableObject",
},
],
},
"exports": {
"MyDurableObject": {
"type": "durable-object",
"storage": "sqlite",
},
},
}

To declare a destructive operation — deleting, renaming, or transferring a class — you change the entry's state to a tombstone variant. The class name remains the same; the value tells Cloudflare what to do with the existing namespace.

OperationstateRequired fields
Define a new class (default)"created" (or omitted)storage
Delete a class"deleted"(none)
Rename a class"renamed"renamed_to
Transfer a class to another Worker"transferred"transferred_to
Receive a transfer from another Worker"expecting-transfer"storage, transfer_from

The rest of this page describes each operation in detail and links to the exports configuration reference for the full schema.

Define a Durable Object class

To define a new Durable Object class, add an entry to exports keyed by the class name and set storage to "sqlite".

  1. Add the class to your Worker code and export it:

    src/index.ts
    import { DurableObject } from "cloudflare:workers";
    export class MyDurableObject extends DurableObject {
    // ...
    }
  2. Add a binding for the class (if your Worker needs to access it through env) and declare the class in exports:

    JSONC
    {
    "durable_objects": {
    "bindings": [
    {
    "name": "MY_DURABLE_OBJECT",
    "class_name": "MyDurableObject"
    }
    ]
    },
    "exports": {
    "MyDurableObject": {
    "type": "durable-object",
    "storage": "sqlite"
    }
    }
    }
  3. Deploy the Worker:

    Terminal window
    npx wrangler deploy

Cloudflare provisions a namespace for the class the first time you deploy. On subsequent deploys with the same entry, no namespace changes are made — the entry simply confirms that the class is still live.

Delete a Durable Object class

Replace the live entry with a deleted tombstone to retire a Durable Object class. Deleting a class removes its namespace and all of its stored data permanently — this is not a soft delete.

  1. Remove the class from your Worker code.

  2. Remove the binding for the class from durable_objects.bindings.

  3. Change the exports entry for the class to a deleted tombstone:

    JSONC
    {
    "exports": {
    "OldDurableObject": {
    "type": "durable-object",
    "state": "deleted"
    }
    }
    }
  4. Deploy the Worker:

    Terminal window
    npx wrangler deploy

A deleted tombstone has two preconditions enforced at deploy time:

  • The class must not be present in your Worker code. Cloudflare will not delete a namespace whose class is still being shipped, because runtime bindings would resolve to a deleted namespace.
  • No other Worker in your account may bind to the namespace. If another Worker still binds to the class, the deploy is rejected with tombstone_delete_blocked_by_external_bindings and the list of referencing scripts is returned. Redeploy those Workers without the binding first, then re-run your deploy.

Once the namespace has been deleted, the tombstone becomes stale. Cloudflare reports this in the reconciliation output and lists the entry in removable_entries so you can safely remove it from exports.

Rename a Durable Object class

Renaming a Durable Object class moves stored data from one class to another within the same Worker. Renaming requires:

  • A renamed tombstone keyed by the old class name, with renamed_to pointing at the new class name.
  • A live entry for the new class name in the same exports map (so the data has somewhere to land).

For a brand-new deploy where the old class is no longer in your code, a single deploy is enough:

JSONC
{
"exports": {
"OldName": {
"type": "durable-object",
"state": "renamed",
"renamed_to": "NewName"
},
"NewName": {
"type": "durable-object",
"storage": "sqlite"
}
}
}

After the rename applies, the namespace's class name is updated to NewName and runtime bindings that reference the new class name resolve to the same data.

Avoid downtime during a rename

A Durable Object class rename involves two updates that are not perfectly atomic at the runtime layer: the namespace's class pointer and the Worker code that exports the class. During a deploy rollout, one update may be visible before the other for a few seconds. To avoid runtime errors during this window, use a three-deploy rename:

  1. Alias the new name to the old class. Add the new class name to your code as the canonical class, and re-export it under the old name so existing instances keep resolving:

    src/index.ts
    import { DurableObject } from "cloudflare:workers";
    export class NewName extends DurableObject {
    // ...
    }
    export { NewName as OldName };

    Leave exports unchanged. Deploy.

  2. Apply the rename while the alias is still in place. Update exports to add the renamed tombstone and the new live entry:

    JSONC
    {
    "exports": {
    "OldName": {
    "type": "durable-object",
    "state": "renamed",
    "renamed_to": "NewName"
    },
    "NewName": {
    "type": "durable-object",
    "storage": "sqlite"
    }
    }
    }

    Deploy. Cloudflare applies the rename and surfaces a tombstone_class_still_in_code info notice — this is expected and confirms the safe rollout pattern.

  3. Remove the alias. Once the previous deploy has fully rolled out, remove the OldName alias from your code:

    src/index.ts
    import { DurableObject } from "cloudflare:workers";
    export class NewName extends DurableObject {
    // ...
    }

    Leave exports unchanged. Deploy. The OldName tombstone is now stale and Cloudflare lists it in removable_entries — you can delete the entry from exports in your next config edit.

The renamed_to target must:

  • Be a valid JavaScript identifier and differ from the source class name.
  • Appear as a live entry (state: "created" or omitted) in the same exports map. Cloudflare rejects a renamed_to value that names another tombstone or is missing entirely.
  • Not collide with an existing namespace under the same name on this Worker. If a namespace already exists under the target name, delete it via its own deleted tombstone in a prior deploy first.

Transfer a Durable Object class between Workers

Transferring moves an existing Durable Object namespace from one Worker (the source) to another Worker (the target) in the same account. Because two Workers must coordinate, transfer is a multi-deploy flow.

The target Worker declares an expecting-transfer entry that names the source Worker. The source Worker declares a transferred tombstone that names the target Worker. The actual handoff commits when the source Worker's deploy lands.

The recommended sequence is four deploys:

  1. Source Worker — initial state. The source declares MyDO as a live class. Nothing else has changed yet.

    JSONC
    {
    "exports": {
    "MyDO": { "type": "durable-object", "storage": "sqlite" }
    }
    }
  2. Target Worker — receive the transfer. The target adds MyDO to its code and declares an expecting-transfer entry naming the source Worker. Do not add a durable_objects.bindings entry for MyDO on the target yet — Cloudflare does not route self-referencing bindings through the source's namespace during this phase.

    JSONC
    {
    "exports": {
    "MyDO": {
    "type": "durable-object",
    "state": "expecting-transfer",
    "storage": "sqlite",
    "transfer_from": "source-worker"
    }
    }
    }

    Deploy the target. Cloudflare records a pending transfer and emits a Transfer pending notice in the reconciliation output.

  3. Source Worker — commit the transfer. Change the source's MyDO entry to a transferred tombstone naming the target Worker. Leave the class in the source code for now:

    JSONC
    {
    "exports": {
    "MyDO": {
    "type": "durable-object",
    "state": "transferred",
    "transferred_to": "target-worker"
    }
    }
    }

    Deploy the source. Cloudflare matches the pending transfer and atomically reassigns the namespace to the target Worker. The reconciliation output reports Transferred (committed): MyDO → target-worker.

    If the source Worker still needs to access MyDO after the transfer commits, update its durable_objects.bindings entry to point at the target Worker with script_name:

    JSONC
    {
    "durable_objects": {
    "bindings": [
    {
    "name": "MY_DURABLE_OBJECT",
    "class_name": "MyDO",
    "script_name": "target-worker"
    }
    ]
    }
    }

    If the source Worker no longer needs to access MyDO, remove the binding when you remove MyDO from the source Worker's code.

  4. Target Worker — bind the class. Once the source's deploy has fully rolled out, redeploy the target with a durable_objects.bindings entry for MyDO. The binding now resolves to the namespace on the target Worker.

    JSONC
    {
    "durable_objects": {
    "bindings": [
    {
    "name": "MY_DURABLE_OBJECT",
    "class_name": "MyDO"
    }
    ]
    },
    "exports": {
    "MyDO": { "type": "durable-object", "storage": "sqlite" }
    }
    }

After the handoff has rolled out everywhere, you can remove MyDO from the source Worker's code and delete the transferred tombstone from the source's exports map. While other Workers in the account still bind to MyDO on the source, the reconciliation output lists them in referencing_scripts; redeploy each of them with bindings re-pointed at the target before removing the source tombstone.

Cancel a pending transfer

A pending transfer persists until the source Worker commits with a transferred tombstone, or the target Worker cancels by removing the expecting-transfer entry. To cancel, redeploy the target without the entry (or replace it with a normal live entry). Cloudflare deletes the pending record and the source Worker keeps its namespace.

Transfer constraints

  • Both Workers must live in the same Cloudflare account.
  • Cross-dispatch-namespace transfers are not supported. The source and target Workers must both be in the same dispatch-namespace context (or both be outside any dispatch namespace).
  • A target Worker can hold only one pending phase-1 hint per class at a time. To redirect a pending transfer to a different source, cancel the current pending transfer first.

Storage backends

Live entries (state: "created" and state: "expecting-transfer") must declare a storage value:

  • "sqlite" selects the SQLite storage backend. This is the recommended and only path for new namespaces. SQLite-backed namespaces support SQL, Point-in-Time Recovery, and a higher per-object storage limit.
  • "legacy-kv" selects the key-value storage backend. Cloudflare only accepts this value for namespaces that were already provisioned with key-value storage (typically Workers that started on the legacy migrations array before SQLite was the default). You cannot create a new key-value-backed namespace through exports.

Storage type is immutable once a namespace exists. Cloudflare rejects an exports change that switches a provisioned namespace from sqlite to legacy-kv or vice versa with the error storage_type_mismatch. To genuinely change storage backends, delete the namespace via a deleted tombstone and re-provision it under the new backend (with full data loss in between).

Reading the reconciliation output

When a deployment applies a Durable Object class lifecycle change, wrangler deploy prints a Durable Object exports reconciliation block. The block also appears when reconciliation produces warnings, info notices, or removable entries. Wrangler omits the block when nothing changed and there are no notices.

A typical output looks like:

Durable Object exports reconciliation:
Created: ChatRoom
Renamed: OldRoom → ChatRoom
Transferred (committed): Widget → target-worker
Transfer pending: Incoming ← source-worker
Info:
[tombstone_class_still_in_code] OldRoom: Tombstone of type 'renamed' applied. Class 'OldRoom' is still exported in code; this is the supported pattern for zero-downtime rename rollouts.
[stale_tombstone] OldGone: Tombstone of type 'deleted' for class 'OldGone' has no effect (no namespace exists with this class name). Safe to remove from `exports`.
Safe to remove from `exports`: OldGone

The block has four sections:

  • Action lines (Created, Updated, Deleted, Renamed, Transferred, Transfer pending) report the changes Cloudflare applied during this deploy.
  • Warnings (yellow) flag conditions that should be investigated but do not block the deploy. The set of warning scenarios is reserved for future use; current Cloudflare deploys do not emit them.
  • Info (dimmed) reports non-blocking notices: stale tombstones that no longer apply, and tombstones applied while the source class is still in code (the supported pattern for zero-downtime rollouts).
  • Safe to remove from exports lists tombstone entries that are stale and have no other Workers in the account binding to the source class. You can delete these entries from your exports map on your next config edit.

Info entries can include a referencing_scripts list — other Workers in the account whose bindings still resolve to the affected namespace. Redeploy those Workers with bindings re-pointed at the new class name before removing the tombstone, or you will orphan their bindings.

If the deploy fails, the output shows a structured error per class:

✘ [orphaned_provisioned_namespace] class 'Bar': A namespace exists for 'Bar' but no `exports` entry declares it.
Suggestion: add an `exports` entry for 'Bar', or add a `deleted` tombstone to remove the namespace.
Referencing scripts: worker-foo, worker-bar

All class-level errors from a single deploy are reported together so you can fix them in one round trip. Refer to the error reference for the full list of error scenarios.

Stale tombstones and cleanup

A tombstone becomes stale when applying it produces no state change — for example, a deleted tombstone for a class whose namespace has already been removed, or a renamed tombstone after the rename has already landed.

Cloudflare emits a stale_tombstone info notice on every deploy where a tombstone is stale, until you remove the entry from exports. The notice is intentionally repeated so you do not forget to clean up.

For renamed and transferred tombstones, Cloudflare also enumerates other Workers in your account whose durable_objects.bindings entries still reference the source class name. These appear as referencing_scripts on the info notice. While referencing_scripts is non-empty, removing the tombstone could orphan those bindings — redeploy the referencing Workers with bindings re-pointed at the new class first.

The top-level "Safe to remove from exports" line in the reconciliation output names every stale tombstone whose referencing_scripts is empty. Use that list as the authoritative "you can delete these now" hint.

Environments, dispatch namespaces, and previews

exports can be specified at the top level of your Wrangler configuration file and overridden per environment:

JSONC
{
// top-level exports apply to the default environment
"exports": {
"MyDO": { "type": "durable-object", "storage": "sqlite" }
},
"env": {
"staging": {
// override for the staging environment
"exports": {
"MyDO": { "type": "durable-object", "storage": "sqlite" }
}
}
}
}

If exports is only declared at the top level, named environments inherit the same value. Each environment maintains its own provisioned namespaces, so tombstones apply only within the environment they are declared in.

Preview deployments and dispatch namespaces follow the same rules. Cross-dispatch-namespace transfers are not supported — the source and target Workers in a transferred / expecting-transfer pair must both live in the same dispatch-namespace context (or both be outside any dispatch namespace).

exports configuration reference

The exports field is a map keyed by Durable Object class name. Each value is an object whose fields depend on state:

  • type string required

    • For Durable Object class entries, set this to "durable-object".
  • state string optional

    • The lifecycle state. One of "created" (the default, omit to use), "deleted", "renamed", "transferred", or "expecting-transfer".
  • storage string conditional

    • Required when state is "created" or "expecting-transfer". One of "sqlite" (recommended, the only valid value for new namespaces) or "legacy-kv" (only for existing key-value-backed namespaces). Forbidden on tombstone states.
  • renamed_to string conditional

    • Required when state is "renamed". The destination class name. Must be a valid JavaScript identifier, differ from the source class name, and appear as a live entry in the same exports map.
  • transferred_to string conditional

    • Required when state is "transferred". The name of the target Worker that will receive the namespace.
  • transfer_from string conditional

    • Required when state is "expecting-transfer". The name of the source Worker the namespace is being transferred from.

The following table lists the allowed and forbidden fields for each state:

stateRequiredForbidden
"created" (default)storagerenamed_to, transferred_to, transfer_from
"deleted"(none)storage, renamed_to, transferred_to, transfer_from
"renamed"renamed_tostorage, transferred_to, transfer_from
"transferred"transferred_tostorage, renamed_to, transfer_from
"expecting-transfer"storage, transfer_fromrenamed_to, transferred_to

Error reference

When a deploy fails reconciliation, Cloudflare returns one error per class along with a structured scenario tag, a human-readable message, and where applicable a suggestion and referencing_scripts list:

ScenarioMeaningHow to fix
provisioned_class_missing_from_configA namespace exists for a class that the Worker code still exports, but exports has no entry for it.Add a live entry (state: "created") to keep the namespace, or add a tombstone (deleted, renamed, or transferred) to retire it.
config_export_not_in_codeA live entry declares a class that the Worker code does not export.Add the class to your code, or replace the entry with a tombstone.
config_references_nonexistent_classA live entry declares a class that is neither in code nor provisioned.Remove the entry, or add the class to your Worker code.
orphaned_provisioned_namespaceA namespace exists for a class that is neither in code nor declared.Add a tombstone for the class, or add the class back to code and exports.
invalid_exportAn exports entry is structurally invalid — for example, requesting legacy-kv for a new namespace, or a class that maps to more than one provisioned namespace.Correct the entry: use "storage": "sqlite" for new namespaces, or resolve the duplicate with a deleted or renamed tombstone.
tombstone_delete_class_still_in_codeA deleted tombstone names a class that is still exported in code.Remove the class from your code first, then deploy the tombstone.
tombstone_delete_blocked_by_external_bindingsAnother Worker in the account binds to the namespace being deleted.Redeploy the referencing Workers without the binding, then re-run your deploy.
tombstone_renamed_to_occupiedA renamed_to target collides with an existing namespace under that name.Delete the colliding namespace via its own deleted tombstone in a prior deploy.
transferred_pending_not_foundA transferred tombstone has no matching expecting-transfer entry on the target.Deploy the target Worker first with an expecting-transfer entry naming this Worker.
transferred_target_missingThe target Worker named by transferred_to no longer exists.Update transferred_to to a valid target, or remove the tombstone.
transferred_target_mismatchThe transferred_to value does not match the target recorded by the pending transfer.Set transferred_to to the pending transfer's target, or have the target cancel its expecting-transfer entry before redirecting.
transferred_source_in_dispatch_namespaceThe source Worker declaring the transferred tombstone is in a dispatch namespace; cross-dispatch transfer is not supported.Perform the transfer within a single dispatch-namespace context.
transferred_target_in_dispatch_namespaceThe target of a transferred tombstone is in a dispatch namespace; cross-dispatch transfer is not supported.Cancel the pending transfer by having the target redeploy without expecting-transfer, or keep both Workers in the same context.
phase_one_transfer_after_commit_mismatchThe target's namespace was transferred from a different source than the one declared.Remove the expecting-transfer entry — the transfer cannot be redirected after commit.
phase_one_transfer_target_class_provisionedThe target Worker already owns a namespace for the class.Replace the expecting-transfer entry with a normal live entry, or delete the existing namespace first.
phase_one_transfer_duplicateAnother expecting-transfer hint is already in flight for the same class.Cancel the existing pending transfer first by removing or replacing its entry.
phase_one_transfer_source_missingThe source Worker named by transfer_from does not exist in the account.Correct the source Worker name.
phase_one_transfer_source_namespace_missingThe source Worker has no namespace for the class.Make sure the source Worker has the class deployed before the target declares expecting-transfer.
phase_one_transfer_source_in_dispatch_namespaceThe source Worker is in a dispatch namespace; cross-dispatch transfer is not supported.Move the transfer within a single dispatch-namespace context.
phase_one_transfer_target_in_dispatch_namespaceThe target Worker is in a dispatch namespace; cross-dispatch transfer is not supported.Move the transfer within a single dispatch-namespace context.
storage_type_mismatchThe declared storage value does not match the provisioned namespace's storage backend.Storage backends cannot be changed in place. Delete the namespace and re-provision it under the new backend if you genuinely need to switch.
free_tier_requires_sqliteThe account's plan only supports SQLite-backed namespaces, but the entry requests legacy-kv.Use "storage": "sqlite".

Constraints and limitations

  • exports and migrations are mutually exclusive. A Worker configuration that contains both fields is rejected at validation. Once a Worker has been deployed with exports, subsequent deploys must continue to use exports (or neither, which reconciles against an empty config and is usually a mistake).
  • wrangler versions upload does not apply lifecycle changes. Just like the legacy migrations array, Durable Object lifecycle changes can only be applied via wrangler deploy. If your Wrangler configuration contains exports entries, wrangler versions upload fails fast with an actionable error. Refer to Deployment management - Durable Object migrations.
  • Gradual deployments are not supported with exports. Lifecycle changes are atomic at the Cloudflare control plane and cannot be rolled out gradually. Refer to Gradual deployments with Durable Objects - Durable Object class lifecycle changes.
  • Rollbacks cannot cross a lifecycle change. You cannot roll back to a version deployed before an exports-driven lifecycle change. Refer to Rollbacks - Bindings.
  • Storage backends are immutable once provisioned. You cannot change a namespace's storage value in place; delete and re-provision instead.

Migrate from the legacy migrations flow

Existing Workers using the migrations array can move to exports without any data migration. The provisioned namespaces remain in place; only the configuration shape changes.

To determine a class's existing storage backend, trace it to the migration that originally created it. Classes introduced through new_sqlite_classes use sqlite, while classes introduced through new_classes use legacy-kv.

  1. Identify the live Durable Object classes your Worker exports today. Each class with an active namespace becomes a live exports entry.
  2. Replace the migrations array with an exports map. For each existing class, add an entry keyed by the class name with "type": "durable-object". Match the existing storage backend with "storage": "sqlite" or "storage": "legacy-kv".
  3. Remove the migrations array from your configuration.
  4. Deploy.

Side-by-side, the equivalent of a typical migrations history is:

JSONC
{
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["ChatRoom"] }
]
}
JSONC
{
"exports": {
"ChatRoom": { "type": "durable-object", "storage": "sqlite" }
}
}

Future lifecycle changes (deletes, renames, transfers) happen entirely through exports — there is no equivalent of the tag field and no need to keep historical entries. The current state of your exports map is the source of truth.