From d0a2c86e0b94eb75c6c0a5fc59a451fb83561a1b Mon Sep 17 00:00:00 2001 From: iglesiasbrandon <5313116+iglesiasbrandon@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:27:06 -0400 Subject: [PATCH 1/4] [Workers] Document Durable Objects preShutdown hook (WIP) --- src/content/docs/durable-objects/api/base.mdx | 61 +++++++++++++++++++ .../rules-of-durable-objects.mdx | 53 ++++++++++++++++ .../concepts/durable-object-lifecycle.mdx | 15 ++++- .../working-without-shutdown-hooks.mdx | 10 +-- 4 files changed, 133 insertions(+), 6 deletions(-) diff --git a/src/content/docs/durable-objects/api/base.mdx b/src/content/docs/durable-objects/api/base.mdx index e604f565c89..35c7aef9ca0 100644 --- a/src/content/docs/durable-objects/api/base.mdx +++ b/src/content/docs/durable-objects/api/base.mdx @@ -16,6 +16,7 @@ import { Type, MetaInfo, TypeScriptExample, + Badge, } from "~/components"; The `DurableObject` base class is an abstract class which all Durable Objects inherit from. This base class provides a set of optional methods, frequently referred to as handler methods, which can respond to events, for example a `webSocketMessage` when using the [WebSocket Hibernation API](/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). To provide a concrete example, here is a Durable Object `MyDurableObject` which extends `DurableObject` and implements the fetch handler to return "Hello, World!" to the calling Worker. @@ -306,6 +307,65 @@ class MyDurableObject(DurableObject): +### `preShutdown` + +- preShutdown(info ) + : | + - Called by the system before some planned shutdowns, giving the Durable Object a chance to run best-effort cleanup, such as a final checkpoint to storage. + - `preShutdown()` is not guaranteed to run. It does not run if a Durable Object shuts down unexpectedly, for example after a crash. Use it to reduce how often you checkpoint state, not to replace incremental writes. Refer to [Shutdown notifications](/durable-objects/concepts/durable-object-lifecycle/#shutdown-notifications) for the shutdown causes that trigger this handler. + - Once `preShutdown()` starts, the shutdown cannot be canceled. No new requests, RPC calls, alarms, or WebSocket messages are delivered to this instance. + - A request that arrives while `preShutdown()` is running waits for a new instance to start once the shutdown finishes. + - The handler has a 10-second execution budget. If it throws, rejects, or exceeds the budget, the runtime logs the failure and continues with the shutdown. + - Storage writes issued from `preShutdown()`, including any issued before a timeout, are saved before the next instance's constructor runs. + - This method can be `async`. You do not need `blockConcurrencyWhile()` inside `preShutdown()`, because no new events are delivered while the handler runs. + - `preShutdown()` must be idempotent, since your own code can also call it directly. + +#### Parameters + +- `info` - an object describing why the shutdown is happening: + - `reason` - one of `"inactive"` (the Durable Object was idle) or `"codeUpdated"` (a new version of your Worker was deployed). Additional reason values may be added in future releases. + +#### Return values + +- None. + +#### Example + + + + +```ts +export class MyDurableObject extends DurableObject { + events: string[] = []; + dirty = false; + + async recordEvent(event: string) { + this.events.push(event); + this.dirty = true; + } + + async preShutdown(info: PreShutdownInfo) { + if (this.dirty) { + await this.ctx.storage.put("events", this.events); + } + } +} +``` + + + + +```python +from workers import DurableObject + +class MyDurableObject(DurableObject): + async def preShutdown(self, info): + if self.dirty: + await self.ctx.storage.put("events", self.events) +``` + + + ## Properties ### `ctx` @@ -321,3 +381,4 @@ class MyDurableObject(DurableObject): - [Use WebSockets](/durable-objects/best-practices/websockets/) for WebSocket handler best practices. - [Alarms API](/durable-objects/api/alarms/) for scheduling future work. - [RPC methods](/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) for type-safe method calls. +- [Lifecycle of a Durable Object](/durable-objects/concepts/durable-object-lifecycle/#shutdown-notifications) for when `preShutdown()` runs relative to eviction and code updates. diff --git a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx index 7c296be16b1..77bfd87c6b0 100644 --- a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx +++ b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx @@ -1443,6 +1443,59 @@ export class ChatRoom extends DurableObject { +Use [`preShutdown()`](/durable-objects/api/base/#preshutdown) to reduce how often you checkpoint buffered state, such as batched agent messages or metrics, not to replace incremental writes: + + +```ts +import { DurableObject } from "cloudflare:workers"; + +export interface Env { + AGENT: DurableObjectNamespace; +} + +export class Agent extends DurableObject { + conversation: string[] = []; + dirty = false; + + // ✅ Good: Buffer in memory, flush periodically and on shutdown + async addMessage(message: string) { + this.conversation.push(message); + this.dirty = true; + + // Checkpoint on a relaxed interval, not on every message + if (!(await this.ctx.storage.getAlarm())) { + await this.ctx.storage.setAlarm(Date.now() + 30_000); + } + } + + async alarm() { + await this.checkpoint(); + } + + // preShutdown() is best-effort and may not run, so the periodic + // alarm above still owns durability. This handler only shortens + // the gap before eviction. + async preShutdown() { + await this.checkpoint(); + } + + private async checkpoint() { + if (this.dirty) { + await this.ctx.storage.put("conversation", this.conversation); + this.dirty = false; + } + } + + // 🔴 Bad: Relying on preShutdown() as the only durability mechanism + async addMessageRisky(message: string) { + this.conversation.push(message); + // No periodic checkpoint — data written since the last checkpoint + // may be lost if the object shuts down unexpectedly. + } +} +``` + + ## Anti-patterns to avoid ### Do not use a single Durable Object as a global singleton diff --git a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx index 3f26619bf43..8aed6643389 100644 --- a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx +++ b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx @@ -8,7 +8,7 @@ products: - durable-objects --- -import { Render } from "~/components"; +import { Render, Badge } from "~/components"; This section describes the lifecycle of a [Durable Object](/durable-objects/concepts/what-are-durable-objects/). @@ -103,6 +103,17 @@ It is important to ensure that any services using Durable Objects are designed t When your Durable Object code is updated, your Worker and Durable Objects are released globally in an eventually consistent manner. This will cause a Durable Object to shut down, with the behavior described above. Updates can also create a situation where a request reaches a new version of your Worker in one location, and calls to a Durable Object still running a previous version elsewhere. Refer to [Code updates](/durable-objects/platform/known-issues/#code-updates) for more information about handling this scenario. -### Working without shutdown hooks +### Shutdown notifications + +A Durable Object class can define a [`preShutdown()`](/durable-objects/api/base/#preshutdown) handler to run best-effort cleanup work, such as a final checkpoint to storage, before a shutdown. Cloudflare invokes `preShutdown()` for some, but not all, of the shutdown causes listed above: + +| Shutdown cause | `preShutdown()` invoked | +| --- | --- | +| Lack of requests to an object (eviction due to inactivity) | Yes | +| New Worker deployment with a code update | Yes | +| Cloudflare updates to the Workers runtime system | No | +| Workers runtime decisions on where to host objects | No | + +`preShutdown()` is not guaranteed to run, even for the causes marked "Yes" above — for example, the object may shut down unexpectedly before the handler completes. Design your Durable Object to write important state incrementally, and use `preShutdown()` only to reduce how often you need to checkpoint, not as a substitute for durable writes. diff --git a/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx b/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx index e9a31c50c9f..bed0a499496 100644 --- a/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx +++ b/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx @@ -2,11 +2,13 @@ {} --- -Durable Objects may shut down at any time due to deployments, inactivity, or runtime decisions. Rather than relying on shutdown hooks (which are not provided), design your application to write state incrementally. +import { Badge } from "~/components"; -Shutdown hooks or lifecycle callbacks that run before shutdown are not provided because Cloudflare cannot guarantee these hooks would execute in all cases, and external software may rely too heavily on these (unreliable) hooks. +Durable Objects may shut down at any time due to deployments, inactivity, or runtime decisions. Design your application to write state incrementally rather than relying only on a shutdown notification. -Instead of relying on shutdown hooks, you can regularly write to storage to recover gracefully from shutdowns. +Durable Objects provide a best-effort [`preShutdown()`](/durable-objects/api/base/#preshutdown) handler that runs before some shutdowns, such as eviction after inactivity or a code update. Use `preShutdown()` to reduce how often you checkpoint state, not to replace incremental writes. Cloudflare cannot guarantee `preShutdown()` runs in every case, and external software should not rely too heavily on it. + +Instead of relying only on a shutdown handler, regularly write to storage to recover gracefully from shutdowns. For example, if you are processing a stream of data and need to save your progress, write your position to storage as you go rather than waiting to persist it at the end: @@ -23,4 +25,4 @@ async processData(data) { While this may feel unintuitive, Durable Object storage writes are fast and synchronous, so you can persist state with minimal performance concerns. -This approach ensures your Durable Object can safely resume from any point, even if it shuts down unexpectedly. +This approach ensures your Durable Object can safely resume from any point, even if it shuts down unexpectedly. Refer to [`preShutdown`](/durable-objects/api/base/#preshutdown) for details on the best-effort shutdown handler. From ae2a3895ef510c91f4f45061bbce9956a62d51d5 Mon Sep 17 00:00:00 2001 From: iglesiasbrandon <5313116+iglesiasbrandon@users.noreply.github.com> Date: Mon, 14 Sep 2026 09:54:35 -0400 Subject: [PATCH 2/4] [DO] Document shutdown hook --- src/content/docs/durable-objects/api/base.mdx | 78 +++++++++---------- .../rules-of-durable-objects.mdx | 67 ++++++++-------- .../concepts/durable-object-lifecycle.mdx | 60 +++++++++++--- .../traces/spans-and-attributes.mdx | 4 + .../working-without-shutdown-hooks.mdx | 34 ++++---- 5 files changed, 141 insertions(+), 102 deletions(-) diff --git a/src/content/docs/durable-objects/api/base.mdx b/src/content/docs/durable-objects/api/base.mdx index 35c7aef9ca0..8ee7b3f0560 100644 --- a/src/content/docs/durable-objects/api/base.mdx +++ b/src/content/docs/durable-objects/api/base.mdx @@ -16,7 +16,6 @@ import { Type, MetaInfo, TypeScriptExample, - Badge, } from "~/components"; The `DurableObject` base class is an abstract class which all Durable Objects inherit from. This base class provides a set of optional methods, frequently referred to as handler methods, which can respond to events, for example a `webSocketMessage` when using the [WebSocket Hibernation API](/durable-objects/best-practices/websockets/#durable-objects-hibernation-websocket-api). To provide a concrete example, here is a Durable Object `MyDurableObject` which extends `DurableObject` and implements the fetch handler to return "Hello, World!" to the calling Worker. @@ -307,23 +306,37 @@ class MyDurableObject(DurableObject): -### `preShutdown` +### `[shutdown]` -- preShutdown(info ) +:::note[Beta] +Shutdown hooks are in Beta. +::: + +Import the `shutdown` well-known symbol from `cloudflare:workers`. Define the hook with a computed method name, as shown in the example. + +- [shutdown](info) : | - - Called by the system before some planned shutdowns, giving the Durable Object a chance to run best-effort cleanup, such as a final checkpoint to storage. - - `preShutdown()` is not guaranteed to run. It does not run if a Durable Object shuts down unexpectedly, for example after a crash. Use it to reduce how often you checkpoint state, not to replace incremental writes. Refer to [Shutdown notifications](/durable-objects/concepts/durable-object-lifecycle/#shutdown-notifications) for the shutdown causes that trigger this handler. - - Once `preShutdown()` starts, the shutdown cannot be canceled. No new requests, RPC calls, alarms, or WebSocket messages are delivered to this instance. - - A request that arrives while `preShutdown()` is running waits for a new instance to start once the shutdown finishes. - - The handler has a 10-second execution budget. If it throws, rejects, or exceeds the budget, the runtime logs the failure and continues with the shutdown. - - Storage writes issued from `preShutdown()`, including any issued before a timeout, are saved before the next instance's constructor runs. - - This method can be `async`. You do not need `blockConcurrencyWhile()` inside `preShutdown()`, because no new events are delivered while the handler runs. - - `preShutdown()` must be idempotent, since your own code can also call it directly. + - Runs before idle eviction or a reset caused by a code update. Use this handler for best-effort clean shutdown logic, such as a final storage checkpoint. + - The hook is not guaranteed to run. It does not run after `ctx.abort()`, broken input or output gates, resource limit termination, supervisor session loss, or a crash. Runtime process reloads and drains also do not trigger the hook. + - Once the hook starts, shutdown cannot be canceled. The instance receives no new requests, RPC calls, alarms, or WebSocket messages. + - A racing request waits for teardown and storage flush. The request then reaches the successor instance. + - During code updates, existing in-flight events may continue while the hook awaits. Your hook must account for changes made by those events. + - The runtime awaits the returned value, whether it is a promise or not. The handler has a 10-second wall-clock budget. + - After resolution, rejection, or timeout, the runtime drains issued storage writes. + - The runtime logs rejections and timeouts, then continues shutdown. It does not retry the hook. + - The successor constructor starts after the predecessor's storage flush completes. + - The hook does not reset the inactivity timer. + - The hook runs before hibernatable WebSockets are hibernated. You can send a final message with `ws.send()`. + - During Beta, the hook runs for root actors only. Actor facets do not receive it. + - Local development follows the same behavior as production. + - The hook has no per-request `AsyncLocalStorage` context. + - The symbol is not exposed on a Durable Object stub. Therefore, callers cannot invoke the hook over RPC. + - Your code can call the symbol directly after importing it. Keep the hook idempotent and reentrant, or do not call it directly. #### Parameters -- `info` - an object describing why the shutdown is happening: - - `reason` - one of `"inactive"` (the Durable Object was idle) or `"codeUpdated"` (a new version of your Worker was deployed). Additional reason values may be added in future releases. +- `info` - an object describing why shutdown is happening: + - `reason` - `"inactive"` for idle eviction or `"codeUpdated"` for a code update. Handle unknown values because more reasons may be added. #### Return values @@ -331,41 +344,22 @@ class MyDurableObject(DurableObject): #### Example - - - + ```ts -export class MyDurableObject extends DurableObject { - events: string[] = []; - dirty = false; - - async recordEvent(event: string) { - this.events.push(event); - this.dirty = true; - } +import { DurableObject, shutdown } from "cloudflare:workers"; - async preShutdown(info: PreShutdownInfo) { - if (this.dirty) { - await this.ctx.storage.put("events", this.events); - } +export class MyDurableObject extends DurableObject { + async [shutdown](info) { + console.log(`Shutting down: ${info.reason}`); + await this.ctx.storage.put("lastShutdown", { + reason: info.reason, + timestamp: Date.now(), + }); } } ``` - - -```python -from workers import DurableObject - -class MyDurableObject(DurableObject): - async def preShutdown(self, info): - if self.dirty: - await self.ctx.storage.put("events", self.events) -``` - - - ## Properties ### `ctx` @@ -381,4 +375,4 @@ class MyDurableObject(DurableObject): - [Use WebSockets](/durable-objects/best-practices/websockets/) for WebSocket handler best practices. - [Alarms API](/durable-objects/api/alarms/) for scheduling future work. - [RPC methods](/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/) for type-safe method calls. -- [Lifecycle of a Durable Object](/durable-objects/concepts/durable-object-lifecycle/#shutdown-notifications) for when `preShutdown()` runs relative to eviction and code updates. +- [Lifecycle of a Durable Object](/durable-objects/concepts/durable-object-lifecycle/#shutdown-notifications) for when `[shutdown]()` runs relative to eviction and code updates. diff --git a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx index b2addddbe48..f7cd7c19a30 100644 --- a/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx +++ b/src/content/docs/durable-objects/best-practices/rules-of-durable-objects.mdx @@ -1443,54 +1443,53 @@ export class ChatRoom extends DurableObject { -Use [`preShutdown()`](/durable-objects/api/base/#preshutdown) to reduce how often you checkpoint buffered state, such as batched agent messages or metrics, not to replace incremental writes: +Use [`[shutdown]()`](/durable-objects/api/base/#shutdown) for a final best-effort checkpoint. Combine it with relaxed count-based or time-based checkpoints during normal requests. + +The following counter accepts the loss of increments since the previous checkpoint after an unexpected shutdown. It checkpoints every 100 increments and again during clean shutdown: ```ts -import { DurableObject } from "cloudflare:workers"; +import { DurableObject, shutdown } from "cloudflare:workers"; export interface Env { - AGENT: DurableObjectNamespace; + COUNTER: DurableObjectNamespace; } -export class Agent extends DurableObject { - conversation: string[] = []; - dirty = false; - - // ✅ Good: Buffer in memory, flush periodically and on shutdown - async addMessage(message: string) { - this.conversation.push(message); - this.dirty = true; - - // Checkpoint on a relaxed interval, not on every message - if (!(await this.ctx.storage.getAlarm())) { - await this.ctx.storage.setAlarm(Date.now() + 30_000); - } - } +export class Counter extends DurableObject { + private count: number; - async alarm() { - await this.checkpoint(); + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ctx.storage.sql.exec( + "CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY, value INTEGER NOT NULL)" + ); + this.ctx.storage.sql.exec( + "INSERT OR IGNORE INTO counter (id, value) VALUES (1, 0)" + ); + this.count = this.ctx.storage.sql + .exec<{ value: number }>("SELECT value FROM counter WHERE id = 1") + .one().value; } - // preShutdown() is best-effort and may not run, so the periodic - // alarm above still owns durability. This handler only shortens - // the gap before eviction. - async preShutdown() { - await this.checkpoint(); + increment() { + this.count++; + if (this.count % 100 === 0) { + this.checkpoint(); + } + return this.count; } - private async checkpoint() { - if (this.dirty) { - await this.ctx.storage.put("conversation", this.conversation); - this.dirty = false; - } + async [shutdown](info) { + console.log(`Shutting down: ${info.reason}`); + this.checkpoint(); + await this.ctx.storage.put("lastShutdownReason", info.reason); } - // 🔴 Bad: Relying on preShutdown() as the only durability mechanism - async addMessageRisky(message: string) { - this.conversation.push(message); - // No periodic checkpoint — data written since the last checkpoint - // may be lost if the object shuts down unexpectedly. + private checkpoint() { + this.ctx.storage.sql.exec( + "UPDATE counter SET value = ? WHERE id = 1", + this.count + ); } } ``` diff --git a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx index 8aed6643389..a8c9497d489 100644 --- a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx +++ b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx @@ -8,7 +8,7 @@ products: - durable-objects --- -import { Render, Badge } from "~/components"; +import { TypeScriptExample } from "~/components"; This section describes the lifecycle of a [Durable Object](/durable-objects/concepts/what-are-durable-objects/). @@ -93,8 +93,8 @@ Durable Objects will occasionally shut down and objects are restarted, which wil When a Durable Object is shut down, the object instance is automatically restarted and new requests are routed to the new instance. In-flight requests are handled as follows: -- **HTTP & RPC requests**: In-flight requests are allowed to finish if they do not access a Durable Object's storage. If a request attempts to access a Durable Object's storage, it will be stopped immediately and return an error to maintain Durable Objects global uniqueness property. When the Worker runtime system is being updated, in-flight requests have up to 30 seconds to complete. -- **WebSocket connections**: WebSocket requests are terminated automatically during shutdown. This is so that the new instance can take over the connection as soon as possible. +- **HTTP & RPC requests**: In-flight requests are allowed to finish if they do not access a Durable Object's storage. If a request attempts to access a Durable Object's storage, it will be stopped immediately and return an error to maintain Durable Objects global uniqueness property. When the Worker runtime system is being updated, in-flight requests have up to 30 seconds to complete. +- **WebSocket connections**: Standard WebSocket connections are terminated during shutdown. WebSockets accepted with the [WebSocket Hibernation API](/durable-objects/best-practices/websockets/) remain connected when an object is evicted due to inactivity. - **Other invocations (email, cron)**: Other invocations are treated similarly to HTTP requests. It is important to ensure that any services using Durable Objects are designed to handle the possibility of a Durable Object being shut down. @@ -105,15 +105,51 @@ When your Durable Object code is updated, your Worker and Durable Objects are re ### Shutdown notifications -A Durable Object class can define a [`preShutdown()`](/durable-objects/api/base/#preshutdown) handler to run best-effort cleanup work, such as a final checkpoint to storage, before a shutdown. Cloudflare invokes `preShutdown()` for some, but not all, of the shutdown causes listed above: +A Durable Object can define a [`[shutdown]()`](/durable-objects/api/base/#shutdown) hook. Use it for best-effort clean shutdown logic and final checkpoints. -| Shutdown cause | `preShutdown()` invoked | -| --- | --- | -| Lack of requests to an object (eviction due to inactivity) | Yes | -| New Worker deployment with a code update | Yes | -| Cloudflare updates to the Workers runtime system | No | -| Workers runtime decisions on where to host objects | No | + +```ts +import { DurableObject, shutdown } from "cloudflare:workers"; -`preShutdown()` is not guaranteed to run, even for the causes marked "Yes" above — for example, the object may shut down unexpectedly before the handler completes. Design your Durable Object to write important state incrementally, and use `preShutdown()` only to reduce how often you need to checkpoint, not as a substitute for durable writes. +export class Counter extends DurableObject { + async [shutdown](info) { + console.log(`Shutting down: ${info.reason}`); + await this.ctx.storage.put("lastShutdown", Date.now()); + } +} +``` + + +:::note[Beta] +Shutdown hooks are in Beta. +::: + +The runtime invokes the hook for these shutdown causes: + +| Shutdown cause | `[shutdown]()` invoked | +| ---------------------------------------------------------- | ---------------------- | +| Lack of requests to an object (eviction due to inactivity) | Yes | +| New Worker deployment with a code update | Yes | +| Runtime process reload or drain | No | +| `ctx.abort()` | No | +| Broken input or output gate | No | +| CPU or memory limit termination | No | +| Supervisor session loss or crash | No | + +For runtime process reloads and drains, in-flight requests retain the existing drain behavior described above, but `[shutdown]()` is not invoked. + +`[shutdown]()` is not guaranteed to run. Persist important changes incrementally. SQLite storage operations such as `sql.exec()` are synchronous, while key-value storage operations are strongly consistent and complete asynchronously. Use the hook for best-effort clean shutdown work that differs from regular request handling. + +Once the hook starts, the shutdown is committed. The instance receives no new events. A racing request waits for teardown and storage flush, then reaches the successor instance. + +During code updates, existing in-flight events may continue while the hook awaits. Account for concurrent changes when checkpointing state. + +The runtime awaits the hook for up to 10 seconds. It drains issued storage writes after resolution, rejection, or timeout, then continues shutdown without retrying the hook. + +The hook runs before hibernatable WebSockets are hibernated. This ordering permits a final `ws.send()` call. During Beta, the hook runs on root actors, but not actor facets. + +The symbol is not exposed on a Durable Object stub, so callers cannot invoke it over RPC. For the complete contract, refer to [`[shutdown]()`](/durable-objects/api/base/#shutdown). + +When [Workers tracing](/workers/observability/traces/) is enabled, the shutdown invocation produces a span with a `cloudflare.handler_type` value of `shutdown`. - +For an example that combines regular checkpoints with a final shutdown checkpoint, refer to [Design for unexpected shutdowns](/durable-objects/best-practices/rules-of-durable-objects/#design-for-unexpected-shutdowns). diff --git a/src/content/docs/workers/observability/traces/spans-and-attributes.mdx b/src/content/docs/workers/observability/traces/spans-and-attributes.mdx index a3141c936f2..b743af3d64b 100644 --- a/src/content/docs/workers/observability/traces/spans-and-attributes.mdx +++ b/src/content/docs/workers/observability/traces/spans-and-attributes.mdx @@ -163,6 +163,10 @@ Cloudflare Workers provides automatic tracing instrumentation **out of the box** - `cloudflare.scheduled_time` +#### [Durable Object shutdown hook](/durable-objects/api/base/#shutdown) + +A shutdown invocation produces a tracing span, like a constructor invocation. Its `cloudflare.handler_type` value is `shutdown`. + --- ### [D1](/d1/) diff --git a/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx b/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx index bed0a499496..afd501637c3 100644 --- a/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx +++ b/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx @@ -2,27 +2,33 @@ {} --- -import { Badge } from "~/components"; +import { TypeScriptExample } from "~/components"; Durable Objects may shut down at any time due to deployments, inactivity, or runtime decisions. Design your application to write state incrementally rather than relying only on a shutdown notification. -Durable Objects provide a best-effort [`preShutdown()`](/durable-objects/api/base/#preshutdown) handler that runs before some shutdowns, such as eviction after inactivity or a code update. Use `preShutdown()` to reduce how often you checkpoint state, not to replace incremental writes. Cloudflare cannot guarantee `preShutdown()` runs in every case, and external software should not rely too heavily on it. +Use the best-effort [`[shutdown]()`](/durable-objects/api/base/#shutdown) hook for clean shutdown logic. The hook runs before idle eviction and code-update resets, but it is not guaranteed to run. -Instead of relying only on a shutdown handler, regularly write to storage to recover gracefully from shutdowns. +Persist important changes during normal request processing. SQLite storage operations such as `sql.exec()` are synchronous, while key-value storage operations are strongly consistent and complete asynchronously. -For example, if you are processing a stream of data and need to save your progress, write your position to storage as you go rather than waiting to persist it at the end: +For example, persist each event as it arrives rather than buffering events in memory until shutdown: -```js -// Good: Write progress as you go -async processData(data) { - data.forEach(async (item, index) => { - await this.processItem(item); - // Save progress frequently - await this.ctx.storage.put("lastProcessedIndex", index); - }); + +```ts +import { DurableObject, shutdown } from "cloudflare:workers"; + +export class EventRecorder extends DurableObject { + async recordEvent(eventId: string, payload: string) { + await this.ctx.storage.put(`event:${eventId}`, payload); + } + + async [shutdown](info) { + // Record shutdown metadata, but do not rely on this write for progress. + await this.ctx.storage.put("lastShutdownReason", info.reason); + } } ``` + -While this may feel unintuitive, Durable Object storage writes are fast and synchronous, so you can persist state with minimal performance concerns. +This approach ensures that recorded events survive an unexpected shutdown. Refer to [`[shutdown]()`](/durable-objects/api/base/#shutdown) for hook details. -This approach ensures your Durable Object can safely resume from any point, even if it shuts down unexpectedly. Refer to [`preShutdown`](/durable-objects/api/base/#preshutdown) for details on the best-effort shutdown handler. +``` From 48adb5c4f17e52e3a05f79a6f6070ba3e3970d86 Mon Sep 17 00:00:00 2001 From: Brandon Iglesias Date: Mon, 21 Sep 2026 17:09:54 -0400 Subject: [PATCH 3/4] Apply batched suggestions from code review Co-authored-by: Vy Ton --- .../docs/durable-objects/concepts/durable-object-lifecycle.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx index a8c9497d489..a34bba82091 100644 --- a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx +++ b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx @@ -130,7 +130,7 @@ The runtime invokes the hook for these shutdown causes: | ---------------------------------------------------------- | ---------------------- | | Lack of requests to an object (eviction due to inactivity) | Yes | | New Worker deployment with a code update | Yes | -| Runtime process reload or drain | No | +| Workers runtime updates | No | | `ctx.abort()` | No | | Broken input or output gate | No | | CPU or memory limit termination | No | From b999bf980b31134d0d517ad22fcbe360bfa498d5 Mon Sep 17 00:00:00 2001 From: iglesiasbrandon <5313116+iglesiasbrandon@users.noreply.github.com> Date: Wed, 23 Sep 2026 17:03:24 -0400 Subject: [PATCH 4/4] [DO] Address review feedback on shutdown hook docs --- src/content/docs/durable-objects/api/base.mdx | 12 ++--- .../concepts/durable-object-lifecycle.mdx | 49 ++++++++++--------- .../working-without-shutdown-hooks.mdx | 2 +- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/content/docs/durable-objects/api/base.mdx b/src/content/docs/durable-objects/api/base.mdx index 8ee7b3f0560..53206d76dc7 100644 --- a/src/content/docs/durable-objects/api/base.mdx +++ b/src/content/docs/durable-objects/api/base.mdx @@ -317,17 +317,17 @@ Import the `shutdown` well-known symbol from `cloudflare:workers`. Define the ho - [shutdown](info) : | - Runs before idle eviction or a reset caused by a code update. Use this handler for best-effort clean shutdown logic, such as a final storage checkpoint. - - The hook is not guaranteed to run. It does not run after `ctx.abort()`, broken input or output gates, resource limit termination, supervisor session loss, or a crash. Runtime process reloads and drains also do not trigger the hook. + - The hook is not guaranteed to run. It does not run after `ctx.abort()`, broken input or output gates, resource limit termination, loss of connectivity to the object's host, or a crash. Runtime process reloads and drains also do not trigger the hook. - Once the hook starts, shutdown cannot be canceled. The instance receives no new requests, RPC calls, alarms, or WebSocket messages. - - A racing request waits for teardown and storage flush. The request then reaches the successor instance. - - During code updates, existing in-flight events may continue while the hook awaits. Your hook must account for changes made by those events. + - A racing request waits for teardown and any pending storage writes to complete. The request then reaches the new instance. + - During code updates, the hook starts immediately and may run concurrently with in-flight requests already being processed; those requests continue until the hook's returned promise resolves. Account for changes made by those events. - The runtime awaits the returned value, whether it is a promise or not. The handler has a 10-second wall-clock budget. - - After resolution, rejection, or timeout, the runtime drains issued storage writes. + - After the hook finishes, throws an error, or times out, any storage writes it issued are still allowed to complete. - The runtime logs rejections and timeouts, then continues shutdown. It does not retry the hook. - - The successor constructor starts after the predecessor's storage flush completes. + - The new instance's constructor starts after the previous instance's storage writes complete. - The hook does not reset the inactivity timer. - The hook runs before hibernatable WebSockets are hibernated. You can send a final message with `ws.send()`. - - During Beta, the hook runs for root actors only. Actor facets do not receive it. + - During Beta, [Durable Object Facets](/dynamic-workers/usage/durable-object-facets/) are not supported. - Local development follows the same behavior as production. - The hook has no per-request `AsyncLocalStorage` context. - The symbol is not exposed on a Durable Object stub. Therefore, callers cannot invoke the hook over RPC. diff --git a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx index a34bba82091..77225133237 100644 --- a/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx +++ b/src/content/docs/durable-objects/concepts/durable-object-lifecycle.mdx @@ -105,6 +105,10 @@ When your Durable Object code is updated, your Worker and Durable Objects are re ### Shutdown notifications +:::note[Beta] +Shutdown hooks are in Beta. +::: + A Durable Object can define a [`[shutdown]()`](/durable-objects/api/base/#shutdown) hook. Use it for best-effort clean shutdown logic and final checkpoints. @@ -120,36 +124,35 @@ export class Counter extends DurableObject { ``` -:::note[Beta] -Shutdown hooks are in Beta. -::: - -The runtime invokes the hook for these shutdown causes: +`info` describes why shutdown is happening. `info.reason` is `"inactive"` for idle eviction or `"codeUpdated"` for a code update — handle unknown values, since more reasons may be added. Refer to [`[shutdown]()` parameters](/durable-objects/api/base/#shutdown) for the full reference. -| Shutdown cause | `[shutdown]()` invoked | -| ---------------------------------------------------------- | ---------------------- | -| Lack of requests to an object (eviction due to inactivity) | Yes | -| New Worker deployment with a code update | Yes | -| Workers runtime updates | No | -| `ctx.abort()` | No | -| Broken input or output gate | No | -| CPU or memory limit termination | No | -| Supervisor session loss or crash | No | +The runtime invokes the hook for the shutdown causes described above: -For runtime process reloads and drains, in-flight requests retain the existing drain behavior described above, but `[shutdown]()` is not invoked. +| Shutdown cause | `[shutdown]()` invoked | +| ------------------------------------------------------------ | ---------------------- | +| Lack of requests to an object (eviction due to inactivity) | Yes | +| New Worker deployment with a code update | Yes | +| Workers runtime updates | No | +| Workers runtime decisions on where to host objects | No | -`[shutdown]()` is not guaranteed to run. Persist important changes incrementally. SQLite storage operations such as `sql.exec()` are synchronous, while key-value storage operations are strongly consistent and complete asynchronously. Use the hook for best-effort clean shutdown work that differs from regular request handling. +The hook also does not run for abnormal terminations, since these bypass the storage-healthy shutdown path required to safely run cleanup logic: -Once the hook starts, the shutdown is committed. The instance receives no new events. A racing request waits for teardown and storage flush, then reaches the successor instance. +- `ctx.abort()` +- A broken input or output gate +- A CPU or memory limit termination +- Loss of connectivity to the object's host, or a crash -During code updates, existing in-flight events may continue while the hook awaits. Account for concurrent changes when checkpointing state. +For runtime updates, in-flight requests retain the existing drain behavior described above, but `[shutdown]()` is not invoked. -The runtime awaits the hook for up to 10 seconds. It drains issued storage writes after resolution, rejection, or timeout, then continues shutdown without retrying the hook. - -The hook runs before hibernatable WebSockets are hibernated. This ordering permits a final `ws.send()` call. During Beta, the hook runs on root actors, but not actor facets. +`[shutdown]()` is not guaranteed to run. Persist important changes incrementally. SQLite storage operations such as `sql.exec()` are synchronous, while key-value storage operations are strongly consistent and complete asynchronously. Use the hook for best-effort clean shutdown work that differs from regular request handling. -The symbol is not exposed on a Durable Object stub, so callers cannot invoke it over RPC. For the complete contract, refer to [`[shutdown]()`](/durable-objects/api/base/#shutdown). +### Shutdown behavior -When [Workers tracing](/workers/observability/traces/) is enabled, the shutdown invocation produces a span with a `cloudflare.handler_type` value of `shutdown`. +- Once the hook starts, the shutdown is committed. The instance receives no new events. A racing request waits for teardown and any pending storage writes to complete, then reaches the new instance. +- During code updates, the hook starts immediately and may run concurrently with in-flight requests already being processed; those requests continue until the hook's returned promise resolves. Account for concurrent changes when checkpointing state. +- The hook has up to 10 seconds to run. Whether it finishes, throws an error, or times out, any storage writes it issued are still allowed to complete, and the runtime does not retry the hook. +- The hook runs before hibernatable WebSockets are hibernated. This ordering permits a final `ws.send()` call. During Beta, [Durable Object Facets](/dynamic-workers/usage/durable-object-facets/) are not supported. +- The symbol is not exposed on a Durable Object stub, so callers cannot invoke it over RPC. For the complete contract, refer to [`[shutdown]()`](/durable-objects/api/base/#shutdown). +- When [Workers tracing](/workers/observability/traces/) is enabled, the shutdown invocation produces a span with a `cloudflare.handler_type` value of `shutdown`. For an example that combines regular checkpoints with a final shutdown checkpoint, refer to [Design for unexpected shutdowns](/durable-objects/best-practices/rules-of-durable-objects/#design-for-unexpected-shutdowns). diff --git a/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx b/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx index afd501637c3..fa1e385077b 100644 --- a/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx +++ b/src/content/partials/durable-objects/working-without-shutdown-hooks.mdx @@ -8,7 +8,7 @@ Durable Objects may shut down at any time due to deployments, inactivity, or run Use the best-effort [`[shutdown]()`](/durable-objects/api/base/#shutdown) hook for clean shutdown logic. The hook runs before idle eviction and code-update resets, but it is not guaranteed to run. -Persist important changes during normal request processing. SQLite storage operations such as `sql.exec()` are synchronous, while key-value storage operations are strongly consistent and complete asynchronously. +Persist important changes during normal request processing rather than deferring them to shutdown. SQLite storage operations such as `sql.exec()` complete synchronously, and key-value storage writes issued without an intervening `await` are automatically coalesced into a single atomic transaction — so writing incrementally does not add meaningful overhead compared to batching writes until shutdown. For example, persist each event as it arrives rather than buffering events in memory until shutdown: