Skip to content
sproutboatDocs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Durable Object lifecycle

When an idle Durable Object leaves memory, what comes back, what keeps one awake, and how to run one object per customer.

An object that has nothing to do leaves memory and is built again by its next request, RPC call, alarm or WebSocket message. This is how one app can hold thousands of objects, one per customer, in a small amount of memory. It applies to apps with compatibility_date 2026-11-21 or later. Older apps keep every object in memory for as long as the process runs, as before.

What lives where

State What is in memory How it gets there
Awake The instance, its fields, its input gate, an open SQLite handle Any event: a request, an RPC call, an alarm, a WebSocket message
Evicted Nothing The object has been idle for 10 seconds, or the app is at its limit of awake objects and this was the least recently used
Kept always Its storage, its SQL database file (one clean file once the object is evicted), its alarm, its hibernatable WebSockets Eviction does not touch any of it

Evicted objects cost disk, not memory. An app with 500,000 customers holds 500,000 database files and only the active objects in memory.

What comes back

The next event runs your constructor again, then the event. blockConcurrencyWhile in the constructor holds every event until it settles, so the first event of a wake sees your initialised fields. What did not survive:

  • Fields on this, module-level variables read through the object, and anything else only in memory. Write what matters to storage when it changes.
  • Pending timers and callbacks. An object with a pending timer is not evicted at all (below).

Objects have no onSleep or similar hook. Cloudflare has none either, and an object can leave between two statements of your code, so a hook could not run in time to help.

An evicted instance cannot write. If a promise your handler left running wakes up after its object was evicted and calls storage.put, it rejects with This Durable Object instance was evicted from memory. instead of writing as a second copy of the object. Await what you start, or give it to state.waitUntil.

What keeps an object awake

An object stays in memory while any of these is true:

Rule Detail
An event is running A request, RPC call, alarm or WebSocket message, from its start until it returns
A waitUntil promise is pending Until it settles. A task longer than 25 seconds stops holding the object at 25 seconds (Cloudflare: 15 minutes)
blockConcurrencyWhile or a storage.transaction is open Until it finishes
A timer is pending A setTimeout or setInterval your object armed. A setInterval you never clear keeps the object awake forever. Use an alarm for anything that should happen later, because an alarm survives eviction and a timer does not
A fetch is in flight One you started and did not await, until it settles
A WebSocket you accepted with accept(), a WebSocket you opened, or a connect() socket Until it closes
A streamed response is being sent Until the stream ends

A hibernatable WebSocket (state.acceptWebSocket) does not keep an object awake on Sproutboat Cloud: the connection stays open while the object is gone, and the next message builds it again. An automatic response set with setWebSocketAutoResponse is answered without building it. In a standalone binary nothing holds the connection outside the process, so an open hibernatable socket keeps its object in memory there.

If a pinned object stays awake for more than 60 seconds, the app log names its class once.

Keep constructors cheap

The constructor runs on every wake, so its cost is the wake latency of every first request. Check a schema version and return. Do not load a table into memory or run a migration in full on every wake. Measured on a 32 MiB test app, waking an object whose database file exists takes about 2 ms at the median and under 10 ms at the 99th percentile, and an idle instance holds about 3 to 4 KiB.

export class Org {
  constructor(state) {
    this.state = state;
    state.blockConcurrencyWhile(async () => {
      // IF NOT EXISTS is the whole schema check; PRAGMA user_version is not available in object SQL.
      state.storage.sql.exec("CREATE TABLE IF NOT EXISTS events (ts INTEGER, name TEXT, props TEXT)");
    });
  }
}

Alarms

An alarm into an evicted object builds it, runs the constructor, and then calls alarm(). alarmInfo.retryCount counts the failed runs of that alarm and survives eviction.

Alarms of different objects can run at the same time (up to ten at once), so do not depend on their order.

Jitter per-object alarms. If every customer’s object sets an alarm for midnight, all of them are due in the same second. Each one is built, and the app can only hold so many at once. Spread them by adding a delay derived from the object id, for example a few minutes of spread, and the work arrives as a stream the app can keep up with.

Too many objects at once

The app holds at most a fixed number of awake objects (see the table below). A request for an object that is not awake and finds no idle object to replace waits up to five seconds for a slot. If none opens, that call fails alone with an error whose overloaded property is true, the same error Cloudflare returns for an object that cannot take more requests:

try {
  return await stub.fetch(request);
} catch (error) {
  if (error.overloaded) return new Response("Try again shortly", { status: 503, headers: { "retry-after": "2" } });
  throw error;
}

Do not retry an overloaded error in a tight loop. More than 1,024 events queued behind one object fail the same way. Two objects that call each other each need a slot, so a chain of calls deeper than the limit ends in the same error after the wait, not in a deadlock.

Settings

Setting Default Sproutboat Cloud Standalone binary
Idle time before eviction 10 seconds (0.1 to 600; 0 keeps every object) Set per app by the platform SB_DO_IDLE_MS, in milliseconds
Most awake objects 16 per MiB of the app’s memory limit, at least 64, at most 4,096 (512 at 32 MiB) Set per app by the platform SB_DO_MAX; 4,096 unless a memory limit is set
Wait for a slot before overloaded 5 seconds Set per app by the platform SB_DO_WAIT_MS, in milliseconds

A standalone binary evicts the same way. SB_DO_IDLE_MS=0 keeps every object resident, which is the setting to use when the process has memory to spare and you want the old behaviour. SB_DO_STATS=1 prints a counters line to stderr whenever it changes.

Differences from Cloudflare

  • One idle timeout. Cloudflare evicts a hibernateable object after 10 seconds and one that is not after 70 to 140 seconds. Here an object that is pinned stays until the thing that pins it ends, and every other idle object leaves after the idle time.
  • A function an object returned over RPC throws after that object was evicted. On Cloudflare it keeps the object alive.
  • waitUntil holds an object for at most 25 seconds.
  • The limit on awake objects is per app and set in memory terms; Cloudflare bills and limits per isolate.

One object per customer

For an app that keeps one object per customer organisation (events in the object’s SQLite database, daily rollups in an alarm):

  • Start with 64 MiB. In a test with 5,000 objects and 200 requests a second over them (20 hot, the rest spread), a 32 MiB app answered every request and never held more than 13 MiB of heap, but the files it touched count toward the memory limit, so it ran at the limit and saw occasional stalls: the 99th percentile was 38 ms for a warm object and 108 ms for a cold one, with a worst case of 3 seconds. At 64 MiB the same load had a 99th percentile of 7 ms and 22 ms and nothing slower than 64 ms.
  • Keep the 10 second idle time. A longer time holds more objects awake for no gain, because a wake is a few milliseconds (a median of 2 ms in the test).
  • Create the schema with IF NOT EXISTS in the constructor and nothing else.
  • Set the rollup and retention alarm with a per-object jitter. All 5,000 alarms due in the same second ran exactly once each, while the same traffic kept running.
  • Keep the shared data (users, billing, which customer maps to which object) in D1, and only the customer’s own rows in the object.
  • Expect cold requests to be a few milliseconds slower than warm ones. Handle overloaded as above.

Was this page helpful?