Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Test Durable Object eviction with new cloudflare:test helpers
description: evictDurableObject and evictAllDurableObjects let you test how Durable Objects behave across evictions in your Vitest tests.
products:
- durable-objects
Comment thread
apeacock1991 marked this conversation as resolved.
- workers
date: 2026-06-25
---

The `@cloudflare/vitest-pool-workers` package now includes `evictDurableObject` and `evictAllDurableObjects` test helpers, exported from `cloudflare:test`.

These helpers let you test how a Durable Object behaves across evictions, simulating the production lifecycle where an idle Durable Object can be evicted from memory.

For more context, refer to [Lifecycle of a Durable Object](/durable-objects/concepts/durable-object-lifecycle/).

```ts
import { evictDurableObject, evictAllDurableObjects } from "cloudflare:test";
import { env } from "cloudflare:workers";

const id = env.COUNTER.idFromName("my-counter");
const stub = env.COUNTER.get(id);

// Evict the Durable Object instance pointed to by a specific stub
await evictDurableObject(stub);

// Close WebSockets instead of hibernating them
await evictDurableObject(stub, { webSockets: "close" });

// Evict all currently-running Durable Objects in evictable namespaces
await evictAllDurableObjects();
```

These helpers are available in `@cloudflare/vitest-pool-workers@0.16.20` and later.

Learn more in the [Test APIs reference](/workers/testing/vitest-integration/test-apis/#durable-objects) and the [Testing Durable Objects guide](/durable-objects/examples/testing-with-durable-objects/#testing-eviction).
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ export class Counter extends DurableObject<Env> {
});
}

// In-memory only. This field lives on the instance and is not persisted
// to storage, so it is reset whenever the Durable Object is evicted and
// reconstructed.
cachedHits = 0;

recordHit(): number {
return ++this.cachedHits;
}

getHits(): number {
return this.cachedHits;
}

async increment(name: string = "default"): Promise<number> {
this.ctx.storage.sql.exec(
`INSERT INTO counters (name, value) VALUES (?, 1)
Expand Down Expand Up @@ -329,7 +342,7 @@ describe("Direct Durable Object access", () => {

// List all IDs in the namespace
const ids = await listDurableObjectIds(env.COUNTER);
expect(ids.length).toBe(2);
expect(ids.length).toBeGreaterThanOrEqual(2);
expect(ids.some((id) => id.equals(id1))).toBe(true);
expect(ids.some((id) => id.equals(id2))).toBe(true);
});
Expand Down Expand Up @@ -444,6 +457,171 @@ export class Counter extends DurableObject {
```
</TypeScriptExample>

### Testing eviction

Use `evictDurableObject()` to evict a Durable Object instance during tests. Eviction tears down the instance to reset its in-memory state. This lets you test how your Durable Object recovers state from storage after being evicted.

By default, hibernatable WebSockets are hibernated rather than closed, and eviction waits up to 30 seconds for in-flight requests to drain before tearing down the instance.

The following test sets both in-memory state (`cachedHits`) and durable storage (the counter value), evicts the Durable Object, and verifies that the in-memory state is wiped while the stored count survives:

<TypeScriptExample filename="test/eviction.test.ts">
```ts
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";

describe("Durable Object eviction", () => {
it("wipes in-memory state but preserves storage across eviction", async () => {
const id = env.COUNTER.idFromName("evict-test");
const stub = env.COUNTER.get(id);

// Persist a value to SQLite storage
await stub.increment();
await stub.increment();
expect(await stub.getCount()).toBe(2);

// Set in-memory only state, which is not persisted to storage
await stub.recordHit();
await stub.recordHit();
expect(await stub.getHits()).toBe(2);

// Evict the Durable Object. The in-memory instance is torn down,
// but durable storage is preserved.
await evictDurableObject(stub);

// In-memory state is wiped: the reconstructed instance starts fresh
expect(await stub.getHits()).toBe(0);

// Durable storage survives: the persisted count is read back
expect(await stub.getCount()).toBe(2);
});
});
```
</TypeScriptExample>

#### Testing WebSocket behavior across eviction

You can control what happens to hibernatable WebSockets when a Durable Object is evicted by passing the `options` parameter:

- `{ webSockets: "hibernate" }` (the default) hibernates WebSockets so they can resume after eviction.
- `{ webSockets: "close" }` closes WebSockets during eviction.

The following example uses a Durable Object that accepts WebSocket connections with the [hibernatable WebSockets API](/durable-objects/best-practices/websockets/):

<TypeScriptExample filename="src/websocket-server.ts">
```ts
import { DurableObject } from "cloudflare:workers";

export class WebSocketServer extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
const [client, server] = Object.values(new WebSocketPair());

// Accept the WebSocket as hibernatable so it can survive eviction
this.ctx.acceptWebSocket(server);

return new Response(null, { status: 101, webSocket: client });
}

webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
// Echo the received message back to the client
ws.send(message);
}

webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
// Handle WebSocket close events
}
}
```
</TypeScriptExample>

Add a binding and migration for the Durable Object in your Wrangler configuration, alongside the existing `COUNTER` binding:

<WranglerConfig>
```jsonc
{
"durable_objects": {
"bindings": [
{ "name": "WEBSOCKET_SERVER", "class_name": "WebSocketServer" }
]
},
"migrations": [
{ "tag": "v2", "new_sqlite_classes": ["WebSocketServer"] }
]
}
```
</WranglerConfig>

With the default options, hibernatable WebSockets remain open across eviction, so messages still round-trip afterwards. Passing `{ webSockets: "close" }` closes them instead:

<TypeScriptExample filename="test/eviction-websockets.test.ts">
```ts
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, it, expect } from "vitest";

describe("WebSocket eviction behavior", () => {
it("hibernates WebSockets across eviction by default", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-test");
const stub = env.WEBSOCKET_SERVER.get(id);

const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();

// Hibernatable WebSockets are hibernated, not closed
await evictDurableObject(stub);

// Messages still round-trip after eviction wakes the Durable Object
const message = new Promise<string>((resolve) => {
socket.addEventListener("message", (event) => {
resolve(event.data as string);
});
});
socket.send("after-eviction");
expect(await message).toBe("after-eviction");
socket.close(1000, "done");
});

it("closes WebSockets when requested", async () => {
const id = env.WEBSOCKET_SERVER.idFromName("ws-close-test");
const stub = env.WEBSOCKET_SERVER.get(id);

const response = await stub.fetch("https://example.com", {
headers: { Upgrade: "websocket" },
});
const socket = response.webSocket;
if (!socket) throw new Error("Expected WebSocket response");
socket.accept();

const closed = new Promise<CloseEvent>((resolve) => {
socket.addEventListener("close", (event) => resolve(event));
});

// Close WebSockets instead of hibernating them
await evictDurableObject(stub, { webSockets: "close" });
expect(await closed).toBeDefined();
});
});
```
</TypeScriptExample>

To evict all currently-running Durable Objects at once (for example, to reset state between tests without deleting persisted data), use `evictAllDurableObjects()`:

```ts
import { evictAllDurableObjects } from "cloudflare:test";
import { afterEach } from "vitest";

afterEach(async () => {
await evictAllDurableObjects();
});
```

For more details on the eviction helpers, including the `DurableObjectEvictionOptions` interface, refer to the [Test APIs reference](/workers/testing/vitest-integration/test-apis/#durable-objects).

## Running tests

Run your tests with:
Expand Down
1 change: 1 addition & 0 deletions src/content/docs/workers/testing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ However, if you don't use Vitest, both [Miniflare's API](/workers/testing/minif
| Direct access to Durable Objects | ✅ | ❌ | ❌ |
| Run Durable Object alarms immediately | ✅ | ❌ | ❌ |
| List Durable Objects | ✅ | ❌ | ❌ |
| Test Durable Object eviction | ✅ | ❌ | ❌ |
| Testing service Workers | ❌ | ✅ | ✅ |

<Render file="testing-pages-functions" product="workers" />
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,48 @@ The Workers Vitest integration provides runtime helpers for writing tests. Some

* Immediately runs and removes the Durable Object pointed to by `stub`'s alarm if one is scheduled. Returns `true` if an alarm ran, and `false` otherwise. Note this can only be used with `stub`s pointing to Durable Objects defined in the `main` Worker.

* <code>evictDurableObject(stub:DurableObjectStub, options?:DurableObjectEvictionOptions)</code>: Promise\<void>

* Evicts the currently-running Durable Object pointed to by `stub`, tearing down its instance to reset in-memory state. By default, hibernatable WebSockets are hibernated rather than closed, and eviction waits up to 30 seconds for in-flight requests to drain.

<br/>

Useful for testing how a Durable Object behaves across evictions, such as recovering state from storage or resuming hibernated WebSockets.

<br/>

Rejects if `stub` is not a Durable Object stub, if the target Durable Object is not currently running, or if its namespace has eviction prevented. Note this can only be used with `stub`s pointing to Durable Objects defined in the `main` Worker.

<br/>

```ts
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { it, expect } from "vitest";

it("preserves stored data across eviction", async () => {
const id = env.COUNTER.idFromName("evict-test");
const stub = env.COUNTER.get(id);

// Each request increments and persists the count to storage
expect(await (await stub.fetch("https://example.com")).text()).toBe("1");
expect(await (await stub.fetch("https://example.com")).text()).toBe("2");

// Evict the Durable Object. The in-memory instance is torn down,
// but durable storage is preserved.
await evictDurableObject(stub);

// The next request reconstructs the instance and reads the persisted count
expect(await (await stub.fetch("https://example.com")).text()).toBe("3");
});
```

* The `DurableObjectEvictionOptions` interface controls eviction behavior:

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `webSockets` | `"close" \| "hibernate"` | `"hibernate"` | Controls what happens to hibernatable WebSockets when evicting a Durable Object. With `"hibernate"`, WebSockets are hibernated and can resume after eviction. With `"close"`, WebSockets are closed. |

* <code>listDurableObjectIds(namespace:DurableObjectNamespace)</code>: Promise\<DurableObjectId\[]>

* Gets the IDs of all objects that have been created in the `namespace`. Respects per-file storage isolation, meaning objects created in a different test file will not be returned.
Expand All @@ -226,6 +268,55 @@ The Workers Vitest integration provides runtime helpers for writing tests. Some
});
```

* <code>reset()</code>: Promise\<void>

* Deletes all data from all attached bindings. This is useful for resetting state between test blocks.

<br/>

```ts
import { reset } from "cloudflare:test";
import { afterEach } from "vitest";

afterEach(async () => {
await reset();
});
```

* <code>abortAllDurableObjects()</code>: Promise\<void>

* Resets all Durable Object instances. Unlike `reset()`, this does not delete persisted data. This forcibly tears down all running Durable Object instances, discarding in-memory state without waiting for in-flight requests to drain.

<br/>

```ts
import { abortAllDurableObjects } from "cloudflare:test";
import { afterEach } from "vitest";

afterEach(async () => {
await abortAllDurableObjects();
});
```

* <code>evictAllDurableObjects(options?:DurableObjectEvictionOptions)</code>: Promise\<void>

* Evicts all currently-running Durable Objects in evictable namespaces. Unlike `abortAllDurableObjects()`, eviction is graceful: hibernatable WebSockets are hibernated rather than closed by default, and eviction waits up to 30 seconds for in-flight requests to drain. In-memory state is reset by tearing down each instance.

<br/>

Non-running or idle Durable Objects are skipped, and namespaces with eviction prevented are respected. Accepts the same [`DurableObjectEvictionOptions`](#durable-objects) as `evictDurableObject()`.

<br/>

```ts
import { evictAllDurableObjects } from "cloudflare:test";
import { afterEach } from "vitest";

afterEach(async () => {
await evictAllDurableObjects();
});
```



### D1
Expand Down