A Durable Object sitting "idle" with no incoming traffic burned $40 in a single month. No KV overages, no Worker invocation spikes — just an alarm rescheduling itself every 30 seconds.
Here's the mechanic that got me: each alarm firing counts as one billable DO request and starts the duration meter from zero if the object was evicted between firings. I had a session-aggregation object with this pattern:
async alarm() {
await this.processPendingQueue();
this.storage.setAlarm(Date.now() + 30_000);
}
That's 2,880 alarm firings per day per object. Across 15 active objects, that's 43,200 requests/day — still inside the free million. The actual damage was duration: each cold-start wakeup paid for warm-up time plus the handler. When I noticed a missing D1 index, the alarm handler jumped from 12ms to 340ms average overnight. Duration charges tripled before I saw the alert. I found it in under two minutes with wrangler tail my-worker --format pretty --status error,ok and watched alarm invocations scroll in real time.
The broader lesson is that Durable Objects have three separate billing meters — requests, duration, and storage — and most billing surprises come from conflating them. A stub.fetch() to your DO bills a request and duration for the handler lifetime. A WebSocket upgrade bills one request upfront, then duration for the entire connection lifetime while the object stays resident. Storage is almost always cheap (I hit $8/mo once, moved anything over 50KB to R2, and never saw it again). The expensive combinations are the ones that keep the object alive longer than you expect: self-rescheduling alarms, open WebSocket connections, and slow downstream calls inside handlers that extend wall-clock duration.
I wrote up the full breakdown — including a reference table mapping every scenario to which meters tick, the WebSocket duration math, and the idFromName() gotcha that silently routes traffic to the wrong class — over on dailymanuallab.com.
Top comments (0)