cloudflare / cloudflare/actors

[Bug] Alarms execute before @Persist properties are hydrated on cold start

Open
#121 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
422
Forks
28
PR merge metrics
No merged PRs in 30d

Description

**Describe the bug**

When an Actor wakes up from hibernation/eviction (triggered by an alarm scheduled far enough in the future), the alarm callback executes before the `@Persist` properties have been hydrated from storage.

If the alarm callback relies on any `@Persist` property, it reads `null/undefined`, leading to silent failures on tasks like longer interval cron jobs.

While debugging this, I noticed it might be related to the initialization order of `blockConcurrencyWhile` locks on cold start. It seems like `this.alarms = new Alarms(...)` runs and schedules the alarms before the Actor constructor queues the lock to `_initializePersistedProperties()`.

**To Reproduce**

Steps to reproduce the behavior:

1. Create a basic Hono worker and Actor that schedules a task far enough in the future to force a Durable Object eviction (for example, 2 min):
- Commands used:
```sh
pnpm create hono@latest my-actor --template cloudflare-workers
cd my-actor
pnpm add @cloudflare/actors
```
- Add this to `wrangler.jsonc`:

```jsonc
{
"migrations": [
{
"new_sqlite_classes": ["MyActor"],
"tag": "v1"
}
],
"durable_objects": {
"bindings": [
{
"class_name": "MyActor",
"name": "MyActor"
}
]
}
}
```

```typescript
import { Actor, Persist } from "@cloudflare/actors";
import { Hono } from "hono";
import { logger } from "hono/logger";

export class MyActor extends Actor {
@Persist
myConfig: string | null = null;

async scheduleTask() {
this.myConfig = "hydrated_value";
// Schedule an alarm 2 minutes in the future (long enough to force DO eviction)
await this.alarms.schedule(120, "myAlarmCallback");
console.log("Scheduled!");
}
async myAlarmCallback() {
// BUG: When the DO wakes up, this logs "null" instead of "hydrated_value"
console.log("Alarm fired. Config is:", this.myConfig);
}
}

const app = new Hono<{ Bindings: CloudflareBindings }>();
app.use(logger());

app.get("/", async (c) => {
const exampleActor = MyActor.get("example");

const date = new Date();
console.log(`Current time: ${date.toLocaleTimeString()}`);

await exampleActor.scheduleTask();

date.setMinutes(date.getMinutes() + 2);

console.log(`Check back at: ${date.toLocaleTimeString()}`);
return c.text(`Schedule started`);
});

export default app;
```

2. Trigger the / route using wrangler dev.
3. Wait 2 minutes for the alarm to trigger (ensure the DO goes idle and gets evicted).
4. Check logs: The logs will print Alarm fired. Config is: `null` instead of `hydrated_value`.

**Expected behavior**
When the alarm triggers a callback on a sleeping Actor, the `@Persist` properties should be fully hydrated from the SQLite table before the user's callback function is invoked. In the example `hydrated_value` should have been printed.

**Screenshots**: N/A

**Information:**

- Actor Version: 0.0.1-beta.6
- Wrangler: 4.4.0 (I've tried deleting the `.wrangler` folder and updating wrangler to latest version(4.86.0) but faced the same issue)
- Node.js: v24.15.0
- Typescript: 6.0.3

**Additional context**
Currently, my only workaround is to manually query `this.sql` for the `_actor_persist` table inside the alarm handler to get the data I need.

Contributor guide

Open the contributing guide

Research direction

Start with the Actor constructor, the `this.alarms = new Alarms(...)` initialization, and `_initializePersistedProperties()`, then reproduce the cold-start behavior with the provided Hono worker and two-minute alarm. Trace the initialization order when the Actor is evicted and awakened. Done means the alarm callback sees the hydrated `@Persist` value instead of `null` or `undefined`.

Written by the indexing model from the issue text.

Assessment

Tech stack
sqlite, typescript
Domain
backend, databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.