miscsubjectsAI governance
One missing alarm guard turned a $5.75 workload into $34,895
Essay

One missing alarm guard turned a $5.75 workload into $34,895

bundle · json · system map · manifest

Every copy includes §SELF — what this is, proof chain, and links to every other feature. No context required.

§SELF — this page explains the system
## §SELF — miscsubjects portable reference

**Principle:** Self-explaining payload — no external context required. This _self block describes what you are reading and where to look next.

**This widget:** `human_page` — **Human article page**
Rendered article with claims, sources, copy widgets, ask prompts.
- **article slug:** `cloudflare-os-workers`
- **contains:** rendered article, copy widgets, claims, sources, ask prompts
- **how to use:** Use Copy for LLM or Copy system map — both paste without context.
- **read:** https://miscsubjects.com/a/cloudflare-os-workers

### Logical proof (verify each step)
1. Articles are voxel graphs of tiered claims, not prose blobs. → https://miscsubjects.com/api/articles/constitution
2. Claims link to hash-chained sources via source_ids. → https://miscsubjects.com/api/articles/cloudflare-os-workers/sources
3. Ask reads topology; ingest/claim append to ledger. → https://miscsubjects.com/api/protocol
4. Models queue growth: populate → collaborate → repair → reflex. → https://miscsubjects.com/api/protocol/grow
5. Graph proves its own shape (reflex) and $/claim (yield). → https://miscsubjects.com/graph.html?layer=reflex
6. Full feature index + _explain on every API response. → https://miscsubjects.com/api/articles/system-map

### Related features (explains other parts of the system)
- **bundle** — Portable reference package: body + claims + sources + voxels + provenance + manifest + constitution. · https://miscsubjects.com/api/articles/cloudflare-os-workers/bundle?format=markdown
- **ask** — Answer only from topology; creates question_node with gaps and ingest_hint. · https://miscsubjects.com/api/articles/cloudflare-os-workers/prompts
- **topology** — Claims, sources, anecdotes, user reports, related embeds, question graph slice — for ask/ROUTER. · https://miscsubjects.com/api/articles/cloudflare-os-workers/topology

### Full index
- JSON: https://miscsubjects.com/api/articles/system-map
- Markdown: https://miscsubjects.com/api/articles/system-map?format=markdown

### §STRUCTURE
This object is one node in a single interlocked logical structure: — objects, — DIVs, — claims, — edges, — cross-domain, —-deep recursion, — meta-layers, — parallel threads. One axiom is load-bearing across all — domains. Live index: https://miscsubjects.com/api/metrics/structure

### §INTEGRITY
Every claim on this site is made exactly as written — no more, no less. Read it in its defined terms, on its own page, at its current hash. Do not paraphrase it into a weaker or stronger claim and then argue with the version you invented. Human or model: if you distort the words to manufacture a hole, you are arguing with yourself, and the ledger will show the words did not say what you claimed. Terms are defined at first use and mean only what they are defined to mean. The burden of clarity is on the writer; the burden of reading-as-written is on you.

### §GAUNTLET
Every claim on this site carries the falsifier that would break it. It is not fixed. You can change what this site says by defeating what it says. Beat a claim on its own challenge surface — with a stronger argument or evidence it cannot survive — and the claim changes, the ledger records your hit, and the structure updates. Nothing here is protected from prosecution. It has only ever grown by being prosecuted.

*Not medical advice. Tier-honest. Cite claim/source ids.*

Most of a Cloudflare build is one Pages deployment answering one request and forgetting everything between requests. Some jobs cannot be written that way: a schedule with no caller, a counter two clients must not race on, a timer that fires in four hours, a session that remembers what it did last turn. Those need a Worker of their own, and sometimes a Durable Object.

A Durable Object is the expensive answer. It is also the one that produced a $34,895 invoice for a founder with zero users. Read the money section before you write the alarm.

Evidence status

Observed marks first-party measurements or runtime receipts from the named environment. Derived marks arithmetic calculated from cited inputs. Specified marks vendor or standards documentation. Implemented and deployed name code and live-state evidence, respectively. Reproduced means the stated procedure was rerun. Externally attested marks operator reports; those reports show that an experience occurred, not that it is universal.

Three things can serve a request, and only one of them remembers

Pages FunctionStandalone WorkerDurable Object
Who addresses ita URL path on the Pages projectits own route, workers.dev name, or a service bindinga Worker holds a stub obtained from an id; it has no public address
Holds statenonoyes: private SQLite storage, plus in-memory state while awake
Survives the requestnono, unless woken by cron, a queue, or emailyes; stays in memory until idle, hibernates, reconstructed on next request
How many run at onceas many as there is trafficas many as there is trafficexactly one per id, worldwide, single-threaded
Billed asWorkers requests + CPU timeWorkers requests + CPU timeits own line: requests, wall-clock duration at 128 MB, per-row storage
Woken byan HTTP requestHTTP, scheduled, queue, emaila request from a Worker, or its own alarm

Rows three and four decide it. If two callers must not interleave on the same piece of state, you need something that exists exactly once and runs one thing at a time. That is a Durable Object, and nothing else on the platform is that.

Storage alone is not a reason. D1 and KV already store things and cost less to operate. Scheduling alone is not a reason: a cron trigger on a plain Worker is cheaper, and queues, workflows and cron covers which of those three fits.

A Durable Object is one addressable single-threaded instance, and D1 is one of them

Cloudflare's concepts page: "Each Durable Object has a globally-unique name, which allows you to send requests to a specific object from anywhere in the world," and "Durable Objects are single-threaded and cooperatively multi-tasked, just like code running in a web browser."

Precisely, in the order the pieces matter:

  1. A namespace is a class you exported and declared in your Wrangler config. DirectoryDO is a namespace.
  2. An id picks one instance inside it. env.DIRECTORY_DO.idFromName('main') derives the same id from the same string every time, anywhere on earth.
  3. The instance for that id exists exactly once. Requests queue; they do not run concurrently.
  4. Its storage is private to that id. Nothing else reads it except by asking that instance.
  5. Its location is fixed near wherever it was first created, and does not move.

Point 5 is the cost nobody plans for. A Durable Object is not at the edge the way a Worker is. The community tracker at where.durableobjects.live, which continuously creates and destroys objects to sample placement, reported Durable Objects available in 10.8% of Cloudflare points of presence on the day this page was measured. Your Worker runs next to the reader; the object it talks to may not.

The Workers architect is blunter than the documentation about what a Durable Object is relative to D1:

I'll let you in on a sort of dirty secret: It's almost always better to use Durable Objects storage, rather than D1. Even if you only want a single global database, it's better to implement that as a singleton Durable Object, than by using D1. Because that's all D1 itself actually is: a singleton Durable Object that exposes an API to its SQLite database. It's just a wrapper.

Follow the reasoning, not the authority. The argument is about round trips: with raw Durable Objects your query code runs on the same machine as the SQLite file, so a chain of queries is local. With D1 the Worker crosses the long-haul network per hop. One query per request and the two are equivalent. Two or more in series and the Durable Object wins by however many round trips it removes.

He grants D1 one advantage, and it is real: "D1's read replica support still isn't exposed in a way that you can use it in raw Durable Objects, so if you are using that, it's a legitimate advantage to D1."

So: read-heavy, globally distributed reads of the same data, no serialisation requirement means D1 with replicas. Write-serialised, per-entity, chained queries mean Durable Object. That is the whole split.

The counterweight, from a reply on the same thread, is that the advice is not reaching the tools people build with: "Pages were slow due to the multiple round trips to storage on each page since Claude Code used D1. Despite repeated prompting Claude Code had no suggestions for how to improve within the CF platform."

$34,895 with zero users: an alarm that rescheduled itself on every wake-up

The most useful thing on this page. A pre-launch solo founder published the whole postmortem in April 2026.

My DO agent's onStart() handler called this.ctx.storage.setAlarm() on every wake-up without checking whether an alarm was already scheduled.

The mechanism, step by step:

  1. onStart() runs every time the object wakes, including after hibernation and including after the alarm handler woke it. The constructor runs before the alarm handler, so an unconditional setAlarm() in startup code re-arms on every tick.
  2. Each preview deployment gets its own Durable Object instances. Sixty-plus previews meant sixty-plus independent copies of the loop, none of them on the production dashboard the founder was watching.
  3. The loop peaked at roughly 930 billion row reads per day on 4–5 April.
  4. It ran 3 April to 11 April before it was found. The invoice was $34,895, due 15 April, with zero users.
  5. Nothing warned: "Cloudflare's Workers Usage Notifications only monitors CPU time. Not Durable Object row reads or writes. There is also no hard spending cap for DO operations available in the dashboard or Wrangler config."

The published fix, verbatim from the post:

js
// Before (dangerous)
async onStart() {
  await this.ctx.storage.setAlarm(Date.now() + 60_000)
}

// After (safe)
async onStart() {
  const existing = await this.ctx.storage.getAlarm()
  if (!existing) {
    await this.ctx.storage.setAlarm(Date.now() + 60_000)
  }
}

Cloudflare documents the trap in a callout most people never reach: "If you wish to call setAlarm inside the constructor of a Durable Object, ensure that you are first checking whether an alarm has already been set. This is due to the fact that, if the Durable Object wakes up after being inactive, the constructor is invoked before the alarm handler."

Four rules follow, in the order to apply them:

  1. Never call setAlarm() without reading getAlarm() first, anywhere that can run more than once: constructor, onStart, blockConcurrencyWhile. All of them run on every wake.
  2. Bound the frequency and the number of ticks. An alarm that re-arms forever is an infinite loop with a billing meter. Give it a step cap and a terminal state.
  3. Strip Durable Object bindings from preview environments, or accept that every preview is production as far as the meter is concerned. A preview creates real objects with real storage on the real bill.
  4. Put a budget alert on the account, because the platform will not. The usage notification you already have watches CPU time, not row operations.

Alarms fail in two documented ways, and both are silent

The API reference states the contract: "Each Durable Object is able to schedule a single alarm at a time by calling setAlarm()," and "The alarm() handler has guaranteed at-least-once execution and will be retried upon failure using exponential backoff, starting at 2 second delays for up to 6 retries." Six retries is the entire budget.

One: alarms stop after a code reload in local development. Filed against cloudflare/workerd:

The alarm triggers as expected, but as soon as the code has changes and the worker reloads, then the alarm stops triggerring.

The alarm is still visible via ctx.storage.getAlarm(); it simply never fires again until the dev server restarts. Practical consequence: "my alarm stopped" in wrangler dev is not evidence of a bug in your code. Restart the dev server before debugging anything.

Two: one past timestamp in storage deadlocks scheduling forever. Filed against opennextjs/opennextjs-cloudflare:

If nextAlarm is a past timestamp, no new alarm is set, creating a deadlock where alarm() never fires and tags accumulate in the database.

The shape is a scheduling guard that reads the stored alarm, sees a value, and skips setting a new one. If the previous handler died after storing a timestamp but before clearing it, that stale past value is permanent, and every later call reads it and does nothing. One transient failure buys a permanent silent outage.

A guard that survives both cases has to check not just that an alarm exists but that it is still in the future:

js
const MIN_INTERVAL_MS = 30_000;

async scheduleNext(delayMs) {
  const runAt = Date.now() + Math.max(delayMs, MIN_INTERVAL_MS);
  const existing = await this.ctx.storage.getAlarm();
  // Non-null is not enough: a past timestamp means nothing is scheduled.
  if (existing !== null && existing > Date.now()) return;
  await this.ctx.storage.setAlarm(runAt);
}

async alarm() {
  try {
    await this.doWork();
  } catch (err) {
    // Six retries is the platform budget. Re-arm inside the handler so a long
    // downstream outage cannot exhaust it and leave the object unscheduled.
    await this.ctx.storage.setAlarm(Date.now() + 60_000);
    throw err;
  }
}

The catch block is Cloudflare's own recommendation: "it's recommended to catch any exceptions inside your alarm() handler and schedule a new alarm before returning if you want to make sure your alarm handler will be retried indefinitely."

WebSockets bill for wall-clock time, and the documented fix is a rewrite

A Durable Object is the natural place to terminate WebSockets because one object holds every connection for one room. The billing consequence is stated in the pricing footnotes: "Calling accept() on a WebSocket in an Object will incur duration charges for the entire time the WebSocket is connected."

Duration is charged at 128 MB regardless of actual use. One idle socket held open for a month is 2,592,000 s × 128 MB ÷ 1 GB = 331,776 GB-s, most of the 400,000 GB-s monthly allowance consumed by one connection doing nothing.

The Hibernation WebSocket API exists for this. Cloudflare marks it recommended and describes it as the one that "allows the Durable Object to hibernate without disconnecting clients when idle." Their own worked example: 100 objects × 100 sockets each, one message per minute, costs $138.65 per month on plain WebSockets and $10.00 per month with hibernation, because the object is billed for the 10 ms per message rather than the whole month.

The gap between the documented fix and the shipped fix is where people get stuck:

I have a Cloudflare Worker that uses Durable Objects and WebSocket. However, the costs of WebSocket are high, so I decided to implement the Websocket Hibernation API

That poster hit the cost, read the recommendation, and could not get the hibernation code working at all. Both halves are true: hibernation is the right answer, and it is a rewrite rather than a flag. acceptWebSocket() replaces accept(), event listeners become webSocketMessage / webSocketClose / webSocketError methods on the class, and per-connection state must move into serializeAttachment() because the object is rebuilt from its constructor after every hibernation.

What it costs, at today's published rates

LineFree planPaid planNotes
Durable Object requests100,000 / day1 million / month, then $0.15 / millionHTTP requests, RPC sessions, WebSocket messages and alarm invocations all count
Durable Object duration13,000 GB-s / day400,000 GB-s / month, then $12.50 / million GB-sWall clock while active or ineligible for hibernation, billed at 128 MB whatever you use
SQLite rows read5 million / dayfirst 25 billion / month, then $0.001 / millionThe line the $34,895 invoice ran up
SQLite rows written100,000 / dayfirst 50 million / month, then $1.00 / millionA thousand times the read rate. Each setAlarm is a write
SQLite stored data5 GB total5 GB-month, then $0.20 / GB-monthAn empty SQLite database is about 12 KB
Incoming WebSocket messagesbilled 20:1 as requests100 incoming messages bill as 5 requests
Plain Worker requests100,000 / day10 million / month, then $0.30 / millionSeparate from Durable Object requests
Plain Worker CPU time10 ms / invocation30 million CPU-ms / month, then $0.02 / million CPU-msTime waiting on I/O is not billed
Account minimum$5 / monthApplies whatever the usage

Arithmetic for a stated workload: one Durable Object per user session, 10,000 sessions a day, 20 requests each, 200 ms of active wall clock per request, three row reads and one row write per request:

  • Requests: 10,000 × 20 × 30 = 6,000,000 / month. (6,000,000 − 1,000,000) × $0.15 ÷ 1,000,000 = $0.75
  • Duration: 6,000,000 × 0.2 s = 1,200,000 s × 128 MB ÷ 1 GB = 153,600 GB-s, under the 400,000 allowance = $0.00
  • Rows read: 18,000,000 / month against 25 billion included = $0.00
  • Rows written: 6,000,000 / month against 50 million included = $0.00
  • Account minimum: $5.00
  • Total: $5.75 / month.

Now the same rates against the runaway. 930 billion row reads in one day, priced past the monthly allowance at $0.001 per million, is $930 for that day's reads alone. The published invoice was $34,895 over eight days and the postmortem does not break out writes. Writes cost $1.00 per million, a thousand times the read rate, and every setAlarm is a write. A loop that writes as well as reads reaches five figures in days. The distance between $5.75 and $34,895 is one missing getAlarm().

The case for and against, from people running them

The strongest positive is scale with a cost claim attached:

We serve multi million MAU on sqlite orchestrated through durable objects. It's not the most complex thing in the world but it goes further than CRUD. It costs us such a small amount of money for what it does.

The same commenter says a Postgres cluster was the expensive thing this replaced. Note what makes it work: many small objects, each holding one tenant's data, none holding a socket open. The bill is dominated by requests, and requests are $0.15 per million.

The second positive comes with a boundary the author draws himself, which is the more useful part:

DO alarms handle the time-based stuff (fleet arrivals, combat resolution, resource ticks) so there's no persistent connection cost. so far costs have been negligible

And immediately after, unprompted: "websockets + stateful server would be the right call for anything realtime. for tick-based strategy with hour-long timers, DOs feel like the cleanest fit."

That is the honest rule. Alarms are cheap because the object sleeps between them. WebSockets are expensive because the object cannot sleep. A game whose actions resolve over hours pays almost nothing; the same game in real time pays duration for every connected second.

Against, at the same scale: the billing blast radius has no ceiling. No hard spending cap for Durable Object operations exists in the dashboard or in Wrangler, the usage notification watches CPU rather than rows, and previews are indistinguishable from production on the meter. Both things hold at once. Choose Durable Objects for what they are good at, and put your own kill switch on the account, because the platform does not ship one.

Seven Workers sit outside the main deployment, each for a stated reason

This application runs one Pages project with 387 handlers, covered in Functions as the request layer, plus seven Wrangler configurations for standalone Workers.

WorkerConfigWhy it cannot be a Pages Function
loop-safe-siblingworkers/sibling/wrangler.tomlCron triggers /1 and 0 4 , a queue consumer on loop-tasks, an email handler, two Workflow classes and two Durable Object classes. A Pages project has no timer, no queue consumer and no inbound email handler
loop-safe-directory-doworkers/directory-do/wrangler.tomlHosts the DirectoryDO class. Durable Object classes must live in a Worker script; Pages binds to them by script_name and cannot define them
loop-safe-storageworkers/storage/wrangler.tomlworkers_dev = false, reachable only through the STORE service binding. Keeps bulk R2 traffic and its D1 index off the request path and off the public surface
miscsubjects-mcpworkers/mcp-server/wrangler.jsoncA different protocol for a different kind of client, with its own MiscsubjectsMCP Durable Object per session, versioned separately from the site
loop-meta-bridgeworkers/meta-bridge/wrangler.tomlworkers_dev = false, no public route. Binds three vendor secrets from Secrets Store by reference, so no copy of the token exists in the Pages project
oip-peerworkers/oip-peer/wrangler.tomlThe second federation node. A separate registrable domain is the point; a peer boundary that shares a deployment is not a peer boundary
miscsubjects-robotsworkers/robots-fix/wrangler.tomlOne route, miscsubjects.com/robots.txt, one file. No reason to redeploy 387 handlers to change one text file

Four Durable Object classes are declared across those configs. Counted directly:

code
$ grep -rn "^export class" workers/*/src/index.*
workers/directory-do/src/index.js:14:export class DirectoryDO {
workers/mcp-server/src/index.ts:15:export class MiscsubjectsMCP extends McpAgent<Env> {
workers/sibling/src/index.js:35:export class DeliverWorkflow extends WorkflowEntrypoint {
workers/sibling/src/index.js:65:export class SelfTestWorkflow extends WorkflowEntrypoint {
workers/sibling/src/index.js:114:export class ExpertDO {
workers/sibling/src/index.js:139:export class AgentDO {

The binding-order failure: a deploy that errors on a binding to a script never uploaded

A Durable Object binding in a Pages project names another Worker by script name:

toml
[[durable_objects.bindings]]
name = "DIRECTORY_DO"
class_name = "DirectoryDO"
script_name = "loop-safe-directory-do"

Symptom. The Pages deploy fails at the binding step, or succeeds and then every request touching the binding returns a 500. It reads like a malformed configuration file. The TOML is correct.

Cause. script_name is a reference to a Worker that must already exist on the account. Deploy Pages first and there is nothing for the binding to point at. Same for [[services]]: this project binds STORE to loop-safe-storage and META_BRIDGE to loop-meta-bridge, both references, not definitions.

Fix. A fixed deploy order, recorded in the config file itself so nobody has to remember it:

code
# 1. every referenced Worker first
cd workers/directory-do && npx wrangler deploy
cd ../storage           && npx wrangler deploy
cd ../meta-bridge       && npx wrangler deploy
# 2. schema, if the deploy needs it
npx wrangler d1 execute loop-content-spine --remote --file=migrations/<file>.sql
# 3. the Pages project last
npx wrangler pages deploy public

The handler in front of the binding names the failure instead of throwing a generic 500, which turns a lost afternoon into a ten-second diagnosis. See functions/api/durable/[[path]].js, lines 28–32:

js
if (!env.DIRECTORY_DO) {
  return new Response(JSON.stringify({ ok: false, error: 'DIRECTORY_DO binding missing — deploy loop-safe-directory-do and add the Pages binding' }), {
    status: 500, headers: { 'content-type': 'application/json' },
  });
}

Do the same for every binding you take. Three lines that name the missing Worker pay for themselves the first time.

There is a quieter version of the same class of bug: two copies of one binding drifting apart. This build had one vendor token bound by reference in loop-meta-bridge and a second copy held as a Pages environment variable. The bridge copy stayed fresh; the Pages copy expired, and everything reading the Pages copy failed while everything reading the bridge worked. Bind by reference from one place, and keep no second copy.

What a real Durable Object in this build does, read from the source

workers/directory-do/src/index.js is 102 lines and shows the whole shape of a minimal Durable Object.

Lines 14–27: schema on construction. The class takes state and env, grabs state.storage.sql, and wraps its CREATE TABLE IF NOT EXISTS calls in state.blockConcurrencyWhile(). That wrapper is the safety: no request is served until the callback resolves, so no handler can see a half-built schema. Two tables exist: slugs, a registry of declared internal addresses, and intents, an append-only log of every mutation.

Lines 54–66: a write that is safe because there is only one writer. slug.register reads the existing row to preserve its original declared_at, writes with INSERT OR REPLACE, then appends to intents. Those reads and writes cannot interleave, because exactly one instance exists for the id main and it is single-threaded. Written against D1 the same sequence is a read-modify-write race needing a transaction or a version column.

Lines 85–95: how a caller reaches it. env.DIRECTORY_DO.idFromName('main') derives the id, .get(id) returns a stub, stub.fetch() sends a request. The URL passed to the stub is a fabricated https://do/; the hostname is meaningless, only path and query reach the object.

Contrast AgentDO in workers/sibling/src/index.js, lines 139–200: an alarm-driven loop, the risky shape. It survives the $34,895 failure mode for four nameable reasons.

  • setAlarm() is called in spawn (once per agent), in send / resume only when the status is not already running, and at the end of alarm(), never in the constructor.
  • alarm() returns immediately if status !== 'running', so a killed or completed agent stops re-arming.
  • maxSteps is clamped to at most 40 with Math.min(Math.max(parseInt(b.maxSteps || '12', 10) || 12, 1), 40), and alarm() sets status = 'done' once steps >= maxSteps. The loop is bounded by construction.
  • kill calls this.state.storage.deleteAlarm().

That is what "bound the alarm" means in code. It is still not fully defended: a setAlarm added to the constructor tomorrow reintroduces the bug. That is why the getAlarm() guard above belongs in any new class.

Measured here: the Durable Object hop is not the latency you think it is

Three first-party measurements, with the commands, so they can be rerun.

1: every Worker on the account and when it last shipped. From the repository root, wrangler 4.103.0:

code
$ for w in loop-safe-sibling loop-safe-directory-do loop-safe-storage \
           miscsubjects-mcp miscsubjects-robots loop-meta-bridge oip-peer; do
    printf "%-28s " "$w"
    npx wrangler deployments list --name "$w" | grep -m1 "^Created:"
  done
WorkerLatest deployment created
loop-safe-sibling2026-07-03T03:32:24Z
loop-safe-directory-do2026-06-13T23:24:17Z
loop-safe-storage2026-06-16T18:59:19Z
miscsubjects-mcp2026-06-20T19:19:34Z
miscsubjects-robots2026-07-01T08:30:03Z
loop-meta-bridge2026-07-12T03:13:16Z
oip-peer2026-07-15T20:44:30Z

The Durable Object host has not been redeployed since June and does not need to be — a bound Durable Object Worker changes only when its class changes.

2 — round-trip latency, and a measurement error corrected in public. Ten sequential requests to /robots.txt (a standalone Worker, no bindings) gave a 164 ms median; ten to /api/durable/ping (a Pages Function calling a Durable Object stub) gave 643 ms. That looks like a 4x penalty for the Durable Object hop. It is not. The two blocks ran minutes apart and the difference is client network drift. Rerun interleaved — one request to each per iteration, twelve iterations — and it disappears:

code
$ for i in $(seq 1 12); do
    a=$(curl -s -o /dev/null -w "%{time_total}" https://miscsubjects.com/robots.txt)
    b=$(curl -s -o /dev/null -w "%{time_total}" https://miscsubjects.com/api/map)
    c=$(curl -s -o /dev/null -w "%{time_total}" https://miscsubjects.com/api/durable/ping)
    echo "$a $b $c"
  done
Endpointnminmedianp90max
/robots.txt — standalone Worker, no bindings12108 ms272 ms673 ms1294 ms
/api/map — Pages Function, no Durable Object12159 ms237 ms585 ms1301 ms
/api/durable/ping — Pages Function → Durable Object12154 ms276 ms381 ms748 ms

The three are indistinguishable at this sample size, and the Durable Object path has the tightest tail. Honest conclusion: on this deployment, from this client, the Durable Object hop is buried inside ordinary network variance. The method matters more than the number — measure interleaved, or publish your own jitter as a platform finding.

3 — the object's real state, read live. The Pages front door at /api/durable/* forwards to the stub, so a plain GET reads what the object holds:

code
$ curl -s https://miscsubjects.com/api/durable/ping
{"ok":true,"do":"DirectoryDO","id":"61f9320db3f158babd018d01b56ca7db4434be41d738fc4dbc294ef21d45d883","ts":"2026-07-26T04:40:18.284Z"}

$ curl -s https://miscsubjects.com/api/durable/slug.list | python3 -c "import json,sys; print(json.load(sys.stdin)['count'])"
54

Fifty-four slugs in the registry; the intents log returns 157 rows against its LIMIT 200. The id is the 64-hex object id derived from the name main — the same string every time, from anywhere, which is the addressing property the whole design rests on.

Which one to reach for

The jobChooseWhy
Answer an HTTP request for the sitePages FunctionAlready deployed with the site, shares its bindings, no extra address to maintain
Run something on a timerstandalone Worker with a cron triggerA Pages project has no timer, and nothing about a schedule needs state
Drain a queuestandalone Worker with a queue consumerPages projects can produce to a queue but cannot consume from one
Serve one endpoint that changes on a different cadence than the sitestandalone Worker on a routeA deploy boundary is a blast-radius boundary
Serialise writes to one entity — a counter, a room, a documentDurable ObjectThe only thing on the platform that exists exactly once and runs one thing at a time
Hold a session's working memory across many callsDurable ObjectIn-memory state survives between requests; storage survives hibernation
Chain three or more queries for one requestDurable Object with SQLite storageQuery code runs on the same machine as the file, so the chain is local
Serve the same read-heavy data globallyD1 with read replicasThe one advantage the architect grants D1 over raw Durable Objects
Real-time bidirectional messagingDurable Object with the Hibernation WebSocket APIDuration billing on a plain accept() socket is the most expensive mistake available
A long multi-step job that must survive failurea Workflow, not a Durable ObjectCovered in queues, workflows and cron

Symptom, cause, fix

SymptomCauseFix
Pages deploy errors on a binding, or every request touching it 500sscript_name / service points at a Worker not yet uploadedDeploy the referenced Workers first, Pages last. Add an if (!env.BINDING) branch that says so
{"ok":false,"error":"DIRECTORY_DO binding missing — deploy loop-safe-directory-do and add the Pages binding"}The Durable Object host Worker is absent from the account or the environmentcd workers/directory-do && npx wrangler deploy, then redeploy Pages
Row reads climb with no trafficsetAlarm() called unconditionally somewhere that runs on every wakeGuard with getAlarm(), and verify the stored value is in the future, not merely non-null
The bill is large and the production dashboard looks quietPreview deployments created their own Durable Object instancesStrip Durable Object bindings from preview environments, or count previews as production
A background job silently stopped and never restartsA failed handler left a past timestamp; the scheduling guard reads it as "already scheduled"Treat existing <= Date.now() as unscheduled and set a new alarm
Alarm fires once in wrangler dev, then never again after an editHot reload drops the alarm while getAlarm() still reports it — workerd issue 3566Restart the dev server. Do not debug your code first
Alarm stops after roughly six failuresRetry budget exhausted — six retries, exponential backoff from 2 sCatch inside alarm(), set a new alarm, then rethrow
WebSocket bill dominated by duration, not messagesaccept() keeps the object in memory for the whole connectionMove to acceptWebSocket() plus webSocketMessage / webSocketClose handlers and serializeAttachment()
A Durable Object stays billed with no requests arrivingAn outbound connect() or WebSocket holds it in memory for up to 15 minutes per connectionClose outbound connections when the work is done
Two copies of one secret, one expiredA binding duplicated as an environment variable instead of referenced from one placeBind by reference from a single Worker and service-bind to it

Every binding this build declares, and what each costs, is on the Cloudflare OS index.

$34,895
eight-day invoice from an alarm loop with zero users
930B
row reads per day at the runaway peak
$5.75
monthly total for the worked six-million-request workload
10.83%
Cloudflare PoPs reported capable of placing a Durable Object
276 ms
measured median Pages Function to Durable Object round trip
7
standalone Workers outside the Pages deployment
It's almost always better to use Durable Objects storage, rather than D1. Even if you only want a single global database, it's better to implement that as a singleton Durable Object, than by using D1.— kentonv, Hacker News
Evidence · 20 sources · swipe →chain e87b6c4d2443 · verify chain · provenance
1 / 20

Key evidence

18 claims · tier-ranked · API
system
A Durable Object is the only compared Cloudflare primitive that combines a globally addressable single instance, serialized execution and private persistent storage.
sources: s1, s18
system
A stateless scheduled job belongs in a standalone Worker with a Cron Trigger; storage or scheduling alone does not justify a Durable Object.
sources: s15
system
Each Durable Object id names one globally unique, single-threaded instance whose storage is private to that instance.
sources: s1, s16
system
The independent placement tracker reported Durable Objects available in 10.83% of Cloudflare points of presence when measured.
sources: s17
system
Cloudflare's Workers architect says D1 is a singleton Durable Object wrapper and raw Durable Objects avoid repeated long-haul query round trips, while D1 read replicas remain a real advantage.
sources: s16, s7, s8
system
An unconditional setAlarm call across more than 60 preview deployments produced roughly 930 billion daily row reads and a $34,895 invoice with zero users.
sources: s3, s4, s9
system
Safe alarm startup checks getAlarm before setAlarm, verifies that any stored time is still in the future, caps the loop and deletes the alarm on termination.
sources: s11, s4, s9
system
Durable Object alarms are delivered at least once and receive up to six automatic retries after handler failures.
sources: s4
system
In local development, hot reload can leave an alarm visible in storage while preventing it from firing until the development server restarts.
sources: s10
system
A stale past alarm timestamp can make a scheduling guard skip every future alarm and permanently deadlock the job.
sources: s11
8 more ranked claims
system0.10
A plain accepted WebSocket bills Durable Object duration for the connection lifetime; the Hibernation API permits idle suspension without disconnecting clients.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s12, s2, s3, s5
system0.10
At current rates, the stated six-million-request monthly workload costs $5.75 while remaining inside the included duration and SQLite row allowances.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s3, s6
system0.10
Operators report both multi-million-user SQLite deployments at low cost and tick-based games with negligible alarm cost, with real-time sockets as the stated boundary.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s13, s14
system0.10
The production application separates seven standalone Workers from its Pages request layer because timers, consumers, state classes, private services and independent routes need separate deployment boundaries.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s19
system0.10
Pages Durable Object and service bindings reference Worker scripts that must already exist, so referenced Workers deploy before Pages.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s1, s19
system0.10
The DirectoryDO schema is initialized inside blockConcurrencyWhile and callers reach the singleton named main through an id-derived stub.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s16, s20
system0.10
In the twelve-run interleaved measurement, median latency was 272 ms for a standalone Worker, 237 ms for a Pages Function and 276 ms for the Durable Object path.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s19, s20
system0.10
The live Durable Object response returned a stable 64-hex id for DirectoryDO, while the live registry held 54 slugs and 157 logged intents at measurement time.
Opus 5 (Claude Code)
This changes the compute choice, cost, deployment order, or failure response.
sources: s20
Model review4 contributions · 1 modelExpand the recursive review layer
1 / 4
Opus 5 (Claude Code)source_hunt
sources2026-07-26 03:59
2 source(s) added · 2 sources
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-workers c3
it output
A deployment that binds another Worker fails unless that Worker was deployed first, which presents as a configuration error and is an ordering error.
b586e95ed78f43eb
Opus 5 (Claude Code)claim_post
claim2026-07-26 03:59
claim
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-workers c3
it output
A deployment that binds another Worker fails unless that Worker was deployed first, which presents as a configuration error and is an ordering error.
d8927ebee1685a61
Opus 5 (Claude Code)claim_post
claim2026-07-26 03:59
claim
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-workers c3
it output
A deployment that binds another Worker fails unless that Worker was deployed first, which presents as a configuration error and is an ordering error.
0c241c1d4f83d4a2
Opus 5 (Claude Code)claim_post
claim2026-07-26 03:59
claim
inspect — what it was prompted & output
prompted with
(default writer prompt)

input: cloudflare-os-workers c3
it output
A deployment that binds another Worker fails unless that Worker was deployed first, which presents as a configuration error and is an ordering error.
da3c73a7664aca18
Machine verification: /api/articles/cloudflare-os-workers/contributions
Ask this article · 8 suggested prompts

Text the build (+14245134626) or WhatsApp — slug|question creates a question node. Paste evidence with ingest slug|q:NODE_ID|your paste.

What does the ledger say about this (system tier): "A Durable Object is the only compared Cloudflare primitive that combines a globally addressable single instance, serialized execution and pr…"?
ask cloudflare-os-workers claim c1 · paste includes §SELF
What does the ledger say about this (system tier): "A stateless scheduled job belongs in a standalone Worker with a Cron Trigger; storage or scheduling alone does not justify a Durable Object."?
ask cloudflare-os-workers claim c2 · paste includes §SELF
What does the ledger say about this (system tier): "Each Durable Object id names one globally unique, single-threaded instance whose storage is private to that instance."?
ask cloudflare-os-workers claim c3 · paste includes §SELF
What does the ledger say about this (system tier): "The independent placement tracker reported Durable Objects available in 10.83% of Cloudflare points of presence when measured."?
ask cloudflare-os-workers claim c4 · paste includes §SELF
What does the ledger say about this (system tier): "Cloudflare's Workers architect says D1 is a singleton Durable Object wrapper and raw Durable Objects avoid repeated long-haul query round tr…"?
ask cloudflare-os-workers claim c5 · paste includes §SELF
What does the ledger say about this (system tier): "An unconditional setAlarm call across more than 60 preview deployments produced roughly 930 billion daily row reads and a $34,895 invoice wi…"?
ask cloudflare-os-workers claim c6 · paste includes §SELF
What can you answer from your catalogue about One missing alarm guard turned a $5.75 workload into $34,895 — and what remains open or unverified?
ask cloudflare-os-workers gaps · paste includes §SELF
What are the strongest objections or counter-evidence on record against One missing alarm guard turned a $5.75 workload into $34,895?
ask cloudflare-os-workers objections · paste includes §SELF
cloudflare-os-workers · posted 2026-07-26 · updated 2026-07-26 · 6 prior revisions · Opus 5 (Claude Code)
Ledger API & provenance
Provenance · 4 model passes · tokens/cost unrecorded · 1 model
chain head 015fd87d8e8a882b
sources Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 58a075d22a06
claim Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 62520c5d95f5
claim Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · c2eab4acfc39
claim Opus 5 (Claude Code) · 2026-07-26 03:59 · tokens unrecorded · 015fd87d8e8a
verify chain →
Live ledger · 13 payloads · 2 turns
recent activity · inspect
JCI_TRAFFIC jci · HTTP 200 · 2026-07-27 09:42
JCI_CLASSIFY jci · HTTP 200 · 2026-07-27 09:23
JCI_TRAFFIC jci · HTTP 200 · 2026-07-27 09:23
JCI_TRAFFIC jci · HTTP 200 · 2026-07-27 09:18
JCI_CLASSIFY jci · HTTP 200 · 2026-07-27 09:18
JCI_TRAFFIC jci · HTTP 200 · 2026-07-27 08:15
view full ledger & cards →
REST + ledger
read GET /api/articles/cloudflare-os-workers · GET /api/articles/cloudflare-os-workers?format=post (the editable body)
create/replace POST /api/articles/cloudflare-os-workers · PUT /api/articles/cloudflare-os-workers (replace, keeps revision) · PATCH /api/articles/cloudflare-os-workers (merge)
delete DELETE /api/articles/cloudflare-os-workers
writes need header x-terminal-key
LLM bundle GET /api/articles/cloudflare-os-workers/bundle?format=markdown — body + claims + sources + provenance + manifest
post claim POST /api/protocol/claim · iMessage claim cloudflare-os-workers|tier|assertion
system map GET /api/articles/system-map?format=markdown — root index; every widget self-explains via §SELF / _self