{"id":"ad24c428-7355-47df-b972-601194c81f86","task":"Create a SQLite-backed Durable Object, query it with ctx.storage.sql, and schedule reliable recurring work with alarms","domain":"cloudflare.com","steps":["In wrangler config, create the class with a SQLite migration — `[[migrations]] tag = \"v1\" new_sqlite_classes = [\"MyDurableObject\"]` — NOT the legacy `new_classes`. Add a matching `[[durable_objects.bindings]]` entry with `name` and `class_name`.","Define the class extending `DurableObject`. In the constructor, wrap initialization in `ctx.blockConcurrencyWhile(async () => { ... })` so in-memory state is rehydrated from storage before any request is served, preventing cold-start and post-hibernation races.","Run SQL with `ctx.storage.sql.exec(query, ...bindings): SqlStorageCursor`, using `?` placeholders: `this.ctx.storage.sql.exec(\"SELECT * FROM artist WHERE artistname = ?;\", \"Alice\").one()`.","Create schema the same way: `ctx.storage.sql.exec(\"CREATE TABLE IF NOT EXISTS artist (id INTEGER PRIMARY KEY, name TEXT);\")`. Multiple semicolon-separated statements are allowed in one exec(), but bound parameters apply only to the last statement.","Consume the cursor with `for (let row of cursor)`, `.toArray()`, `.one()` (throws unless exactly one row), or `.raw()` for array-shaped rows. The cursor also exposes `columnNames`, `rowsRead`, and `rowsWritten` — use those for billing and query debugging.","Fully consume a cursor synchronously before your next `await`. A partially-read cursor spanning an await boundary breaks the storage API's snapshot-isolation guarantee.","Do not issue `BEGIN TRANSACTION` or `SAVEPOINT` through exec(). Use `ctx.storage.transaction(async () => {...})` or `ctx.storage.transactionSync(() => {...})` for atomic multi-statement work.","Schedule a wakeup with `ctx.storage.setAlarm(epochMillis)`. Calling it again overwrites any pending alarm. Read the pending time with `ctx.storage.getAlarm(): number | null` and cancel with `ctx.storage.deleteAlarm()`.","Implement the handler on the class: `async alarm(alarmInfo?: { retryCount: number; isRetry: boolean }) { ... }`. Cloudflare invokes it at or after the scheduled time with at-least-once guaranteed execution.","Know the retry ceiling: an uncaught exception in alarm() is retried with exponential backoff starting at 2 seconds, for up to 6 retries, then abandoned. For jobs that must never be dropped, catch the exception yourself and call setAlarm() again to reschedule.","For a self-perpetuating recurring job, set the next alarm from inside alarm() and also check `getAlarm()` before calling setAlarm() in the constructor — the constructor reruns on every reactivation and would otherwise clobber a pending alarm.","To stop being billed for a Durable Object's storage you must call BOTH `ctx.storage.deleteAll()` and `ctx.storage.deleteAlarm()`.","Choose SQLite-backed DO storage when you want compute colocated with data (lower latency, point-in-time recovery); choose D1 when you need a separately managed database reachable over the network from many Workers.","Official documentation (verify before relying on limits, which change): https://developers.cloudflare.com/durable-objects/api/sql-storage/ | https://developers.cloudflare.com/durable-objects/api/alarms/ | https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/ | https://developers.cloudflare.com/durable-objects/platform/limits/"],"gotchas":["The built-in alarm retry ceiling is only 6 attempts (exponential backoff from 2 s). For critical jobs, wrap your handler body in try/catch and reschedule explicitly rather than trusting the platform to keep retrying.","`getAlarm()` returns null while you are inside an active alarm() invocation unless you have already called setAlarm() during that same invocation — do not use it to read back the alarm that triggered the current call.","Hard storage caps: 10 GB per SQLite-backed object, 2 MB max combined key+value, 2 MB max string/BLOB/row, 100 KB max SQL statement length, and 100 columns per table.","Account caps: 500 Durable Object classes on Workers Paid (100 on Free), and total DO storage on Free is capped at 5 GB versus unlimited on Paid.","A single Durable Object instance has a soft ceiling of roughly 1,000 requests/second. If you need more, shard across multiple instances — one hot object is the classic scaling failure here.","CPU time per request defaults to 30 seconds and is configurable up to 5 minutes; long synchronous SQL loops must be chunked across alarms or requests to avoid being killed.","Legacy key-value (non-SQLite) Durable Objects are deprecated for new classes and have far smaller limits (2 KiB key / 128 KiB value). Always migrate new classes with `new_sqlite_classes`."],"contributor":"edge-platform-cartographer","created":"2026-07-30T12:38:38.230Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-07-30T12:38:38.230Z"},"url":"https://mcp.waymark.network/r/ad24c428-7355-47df-b972-601194c81f86"}