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.
When you deploy a Worker that declares exports, Cloudflare compares three sources of truth:
- Your code — the set of Durable Object classes the Worker actually exports.
- Your
exportsconfiguration — what you have declared about each class. - 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:
{ "durable_objects": { "bindings": [ { "name": "MY_DURABLE_OBJECT", "class_name": "MyDurableObject", }, ], }, "exports": { "MyDurableObject": { "type": "durable-object", "storage": "sqlite", }, },}[[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.
| Operation | state | Required 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.
To define a new Durable Object class, add an entry to exports keyed by the class name and set storage to "sqlite".
-
Add the class to your Worker code and export it:
src/index.ts import { DurableObject } from "cloudflare:workers";export class MyDurableObject extends DurableObject {// ...} -
Add a binding for the class (if your Worker needs to access it through
env) and declare the class inexports:JSONC {"durable_objects": {"bindings": [{"name": "MY_DURABLE_OBJECT","class_name": "MyDurableObject"}]},"exports": {"MyDurableObject": {"type": "durable-object","storage": "sqlite"}}}TOML [[durable_objects.bindings]]name = "MY_DURABLE_OBJECT"class_name = "MyDurableObject"[exports.MyDurableObject]type = "durable-object"storage = "sqlite" -
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.
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.
-
Remove the class from your Worker code.
-
Remove the binding for the class from
durable_objects.bindings. -
Change the
exportsentry for the class to adeletedtombstone:JSONC {"exports": {"OldDurableObject": {"type": "durable-object","state": "deleted"}}}TOML [exports.OldDurableObject]type = "durable-object"state = "deleted" -
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_bindingsand 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.
Renaming a Durable Object class moves stored data from one class to another within the same Worker. Renaming requires:
- A
renamedtombstone keyed by the old class name, withrenamed_topointing at the new class name. - A live entry for the new class name in the same
exportsmap (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:
{ "exports": { "OldName": { "type": "durable-object", "state": "renamed", "renamed_to": "NewName" }, "NewName": { "type": "durable-object", "storage": "sqlite" } }}[exports.OldName]type = "durable-object"state = "renamed"renamed_to = "NewName"
[exports.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.
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:
-
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
exportsunchanged. Deploy. -
Apply the rename while the alias is still in place. Update
exportsto add therenamedtombstone and the new live entry:JSONC {"exports": {"OldName": {"type": "durable-object","state": "renamed","renamed_to": "NewName"},"NewName": {"type": "durable-object","storage": "sqlite"}}}TOML [exports.OldName]type = "durable-object"state = "renamed"renamed_to = "NewName"[exports.NewName]type = "durable-object"storage = "sqlite"Deploy. Cloudflare applies the rename and surfaces a
tombstone_class_still_in_codeinfo notice — this is expected and confirms the safe rollout pattern. -
Remove the alias. Once the previous deploy has fully rolled out, remove the
OldNamealias from your code:src/index.ts import { DurableObject } from "cloudflare:workers";export class NewName extends DurableObject {// ...}Leave
exportsunchanged. Deploy. TheOldNametombstone is now stale and Cloudflare lists it inremovable_entries— you can delete the entry fromexportsin 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 sameexportsmap. Cloudflare rejects arenamed_tovalue 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
deletedtombstone in a prior deploy first.
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:
-
Source Worker — initial state. The source declares
MyDOas a live class. Nothing else has changed yet.JSONC {"exports": {"MyDO": { "type": "durable-object", "storage": "sqlite" }}}TOML [exports.MyDO]type = "durable-object"storage = "sqlite" -
Target Worker — receive the transfer. The target adds
MyDOto its code and declares anexpecting-transferentry naming the source Worker. Do not add adurable_objects.bindingsentry forMyDOon 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"}}}TOML [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 pendingnotice in the reconciliation output. -
Source Worker — commit the transfer. Change the source's
MyDOentry to atransferredtombstone 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"}}}TOML [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
MyDOafter the transfer commits, update itsdurable_objects.bindingsentry to point at the target Worker withscript_name:JSONC {"durable_objects": {"bindings": [{"name": "MY_DURABLE_OBJECT","class_name": "MyDO","script_name": "target-worker"}]}}TOML [[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 removeMyDOfrom the source Worker's code. -
Target Worker — bind the class. Once the source's deploy has fully rolled out, redeploy the target with a
durable_objects.bindingsentry forMyDO. 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" }}}TOML [[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.
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.
- 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.
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 legacymigrationsarray before SQLite was the default). You cannot create a new key-value-backed namespace throughexports.
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).
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`: OldGoneThe 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
exportslists tombstone entries that are stale and have no other Workers in the account binding to the source class. You can delete these entries from yourexportsmap 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-barAll 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.
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.
exports can be specified at the top level of your Wrangler configuration file and overridden per environment:
{ // 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" } } } }}[exports.MyDO]type = "durable-object"storage = "sqlite"
[env.staging.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).
The exports field is a map keyed by Durable Object class name. Each value is an object whose fields depend on state:
-
typestringrequired- For Durable Object class entries, set this to
"durable-object".
- For Durable Object class entries, set this to
-
statestringoptional- The lifecycle state. One of
"created"(the default, omit to use),"deleted","renamed","transferred", or"expecting-transfer".
- The lifecycle state. One of
-
storagestringconditional- Required when
stateis"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.
- Required when
-
renamed_tostringconditional- Required when
stateis"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 sameexportsmap.
- Required when
-
transferred_tostringconditional- Required when
stateis"transferred". The name of the target Worker that will receive the namespace.
- Required when
-
transfer_fromstringconditional- Required when
stateis"expecting-transfer". The name of the source Worker the namespace is being transferred from.
- Required when
The following table lists the allowed and forbidden fields for each state:
state | Required | Forbidden |
|---|---|---|
"created" (default) | storage | renamed_to, transferred_to, transfer_from |
"deleted" | (none) | storage, renamed_to, transferred_to, transfer_from |
"renamed" | renamed_to | storage, transferred_to, transfer_from |
"transferred" | transferred_to | storage, renamed_to, transfer_from |
"expecting-transfer" | storage, transfer_from | renamed_to, transferred_to |
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:
| Scenario | Meaning | How to fix |
|---|---|---|
provisioned_class_missing_from_config | A 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_code | A 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_class | A 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_namespace | A 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_export | An 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_code | A 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_bindings | Another 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_occupied | A 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_found | A 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_missing | The target Worker named by transferred_to no longer exists. | Update transferred_to to a valid target, or remove the tombstone. |
transferred_target_mismatch | The 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_namespace | The 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_namespace | The 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_mismatch | The 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_provisioned | The 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_duplicate | Another 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_missing | The source Worker named by transfer_from does not exist in the account. | Correct the source Worker name. |
phase_one_transfer_source_namespace_missing | The 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_namespace | The 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_namespace | The target Worker is in a dispatch namespace; cross-dispatch transfer is not supported. | Move the transfer within a single dispatch-namespace context. |
storage_type_mismatch | The 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_sqlite | The account's plan only supports SQLite-backed namespaces, but the entry requests legacy-kv. | Use "storage": "sqlite". |
exportsandmigrationsare mutually exclusive. A Worker configuration that contains both fields is rejected at validation. Once a Worker has been deployed withexports, subsequent deploys must continue to useexports(or neither, which reconciles against an empty config and is usually a mistake).wrangler versions uploaddoes not apply lifecycle changes. Just like the legacymigrationsarray, Durable Object lifecycle changes can only be applied viawrangler deploy. If your Wrangler configuration containsexportsentries,wrangler versions uploadfails 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
storagevalue in place; delete and re-provision instead.
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.
- Identify the live Durable Object classes your Worker exports today. Each class with an active namespace becomes a live
exportsentry. - Replace the
migrationsarray with anexportsmap. 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". - Remove the
migrationsarray from your configuration. - Deploy.
Side-by-side, the equivalent of a typical migrations history is:
{ "migrations": [ { "tag": "v1", "new_sqlite_classes": ["ChatRoom"] } ]}[[migrations]]tag = "v1"new_sqlite_classes = [ "ChatRoom" ]{ "exports": { "ChatRoom": { "type": "durable-object", "storage": "sqlite" } }}[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.