Skip to content

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

Open

Description

@aceducky

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:
pnpm create hono@latest my-actor --template cloudflare-workers
cd my-actor
pnpm add @cloudflare/actors
  • Add this to wrangler.jsonc:
{
  "migrations": [
    {
      "new_sqlite_classes": ["MyActor"],
      "tag": "v1"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "class_name": "MyActor",
        "name": "MyActor"
      }
    ]
  }
}
import { Actor, Persist } from "@cloudflare/actors";
import { Hono } from "hono";
import { logger } from "hono/logger";

export class MyActor extends Actor<CloudflareBindings> {
  @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;
  1. Trigger the / route using wrangler dev.
  2. Wait 2 minutes for the alarm to trigger (ensure the DO goes idle and gets evicted).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions