[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fnkbedww3rx8n":3,"$fanuq43nlrv5g":56},{"slug":4,"title":5,"body":6,"summary":7,"tags":8,"author":14,"cover_url":15,"published_at":16,"seo_title":17,"seo_description":18,"reading_minutes":19,"related":20},"durable-object-alarm-loop-guards","The Durable Object alarm that cost $10,000: guarding setAlarm() loops with backoff, idempotency and a kill switch","\u003Cp>Durable Object alarms are the cheapest way to run a per-object timer on Cloudflare: one alarm per object, no cron, no queue. They are also an easy way to build an infinite loop that bills you while nobody is using the app. A developer recently posted a roughly $10,000 Cloudflare bill on X after a Durable Object alarm kept rescheduling itself, and in May a developer reported a $36,000 month for an 81-user app after a self-requeuing consumer and unbatched Durable Object writes multiplied each other. Neither needed traffic. Both needed only code that runs again on its own.\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6ac87192392a85804f20a0dd\">\u003Cvideo src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac87192392a85804f20a0e3-0-6af3e4f1.mp4\" autoplay muted loop playsinline preload=\"metadata\">\u003C\u002Fvideo>\u003C\u002Ffigure>\n\u003Cp>This guide adds four guards to a self-rescheduling alarm and a test that proves the loop stops. It takes about 15 minutes.\u003C\u002Fp>\n\n\u003Ch2>Why alarms loop so easily\u003C\u002Fh2>\n\u003Cp>From Cloudflare's alarms reference:\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6ac8709d392a85804f209fb9\">\u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac8709d392a85804f209fbe-0-bf9a26bc.png\" alt=\"An infinite loop can quickly lead to unexpected infrastructure costs.\" loading=\"lazy\">\u003Cfigcaption>An infinite loop can quickly lead to unexpected infrastructure costs.\u003C\u002Ffigcaption>\u003C\u002Ffigure>\n\u003Cul>\n\u003Cli>Each object has \u003Cstrong>one\u003C\u002Fstrong> alarm. Calling \u003Ccode>setAlarm()\u003C\u002Fcode> when one is already scheduled \u003Cem>overrides\u003C\u002Fem> it.\u003C\u002Fli>\n\u003Cli>Alarms are \u003Cstrong>at-least-once\u003C\u002Fstrong>. If \u003Ccode>alarm()\u003C\u002Fcode> throws, it is retried with exponential backoff starting at 2 seconds, up to 6 retries. The handler gets \u003Ccode>retryCount\u003C\u002Fcode> and \u003Ccode>isRetry\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>The usual periodic pattern is to call \u003Ccode>setAlarm()\u003C\u002Fcode> again from inside \u003Ccode>alarm()\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>After a period of inactivity the constructor runs before \u003Ccode>alarm()\u003C\u002Fcode>, so the docs tell you to check \u003Ccode>getAlarm()\u003C\u002Fcode> before setting one in the constructor.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Put those together and the failure modes write themselves: an interval read from storage that comes back as \u003Ccode>0\u003C\u002Fcode> or \u003Ccode>undefined\u003C\u002Fcode>, a \u003Ccode>setAlarm(Date.now())\u003C\u002Fcode> on an error path, a \u003Ccode>fetch()\u003C\u002Fcode> handler that re-arms the alarm on every request, or work that is not idempotent and runs twice on a retry.\u003C\u002Fp>\n\u003Cp>The bill follows the pricing page. On Workers Paid, alarm invocations count as Durable Object requests ($0.15 per million after the first million), and each \u003Ccode>setAlarm()\u003C\u002Fcode> is billed as one row written on the SQLite backend ($1.00 per million after 50 million included). Whatever work the handler does, its reads and writes come on top. A loop multiplies every one of those lines.\u003C\u002Fp>\n\n\u003Ch2>The guarded alarm\u003C\u002Fh2>\n\u003Cp>This object runs \u003Ccode>doWork()\u003C\u002Fcode> on an interval and refuses to become a loop. The four guards are marked in the comments.\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6ac8709d392a85804f209fc3\">\u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac8709d392a85804f209fc8-0-53b00cc8.png\" alt=\"Multiple layers of defense prevent a simple timer from becoming a runaway process.\" loading=\"lazy\">\u003Cfigcaption>Multiple layers of defense prevent a simple timer from becoming a runaway process.\u003C\u002Ffigcaption>\u003C\u002Ffigure>\n\u003Cpre class=\"code-block\" data-lang=\"ts\">\u003Ccode class=\"hljs language-typescript\">\u003Cspan class=\"hljs-comment\">\u002F\u002F src\u002Findex.ts\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { \u003Cspan class=\"hljs-title class_\">DurableObject\u003C\u002Fspan> } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;cloudflare:workers&quot;\u003C\u002Fspan>;\n\n\u003Cspan class=\"hljs-keyword\">export\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">interface\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Env\u003C\u002Fspan> {\n  \u003Cspan class=\"hljs-attr\">POLLER\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">DurableObjectNamespace\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-title class_\">Poller\u003C\u002Fspan>&gt;;\n  \u003Cspan class=\"hljs-variable constant_\">ALARMS_DISABLED\u003C\u002Fspan>?: \u003Cspan class=\"hljs-built_in\">string\u003C\u002Fspan>; \u003Cspan class=\"hljs-comment\">\u002F\u002F &quot;1&quot; stops every alarm on its next run\u003C\u002Fspan>\n}\n\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> \u003Cspan class=\"hljs-variable constant_\">MIN_INTERVAL_MS\u003C\u002Fspan> = \u003Cspan class=\"hljs-number\">60_000\u003C\u002Fspan>; \u003Cspan class=\"hljs-comment\">\u002F\u002F guard 1: never fire more often than this\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> \u003Cspan class=\"hljs-variable constant_\">MAX_RUNS_PER_HOUR\u003C\u002Fspan> = \u003Cspan class=\"hljs-number\">120\u003C\u002Fspan>; \u003Cspan class=\"hljs-comment\">\u002F\u002F guard 2: per-object run budget\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> \u003Cspan class=\"hljs-variable constant_\">MAX_BACKOFF_MS\u003C\u002Fspan> = \u003Cspan class=\"hljs-number\">3_600_000\u003C\u002Fspan>;\n\n\u003Cspan class=\"hljs-keyword\">type\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Budget\u003C\u002Fspan> = { \u003Cspan class=\"hljs-attr\">hour\u003C\u002Fspan>: \u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>; \u003Cspan class=\"hljs-attr\">runs\u003C\u002Fspan>: \u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan> };\n\n\u003Cspan class=\"hljs-keyword\">export\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">class\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Poller\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">extends\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_ inherited__\">DurableObject\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-title class_\">Env\u003C\u002Fspan>&gt; {\n  \u003Cspan class=\"hljs-keyword\">async\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">start\u003C\u002Fspan>(\u003Cspan class=\"hljs-attr\">intervalMs\u003C\u002Fspan>: \u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>): \u003Cspan class=\"hljs-title class_\">Promise\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">void\u003C\u002Fspan>&gt; {\n    \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;intervalMs&quot;\u003C\u002Fspan>, \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">max\u003C\u002Fspan>(intervalMs, \u003Cspan class=\"hljs-variable constant_\">MIN_INTERVAL_MS\u003C\u002Fspan>));\n    \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">delete\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;tripped&quot;\u003C\u002Fspan>);\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> ((\u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">getAlarm\u003C\u002Fspan>()) === \u003Cspan class=\"hljs-literal\">null\u003C\u002Fspan>) {\n      \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">setAlarm\u003C\u002Fspan>(\u003Cspan class=\"hljs-title class_\">Date\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">now\u003C\u002Fspan>() + \u003Cspan class=\"hljs-variable constant_\">MIN_INTERVAL_MS\u003C\u002Fspan>);\n    }\n  }\n\n  \u003Cspan class=\"hljs-keyword\">async\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">alarm\u003C\u002Fspan>(): \u003Cspan class=\"hljs-title class_\">Promise\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">void\u003C\u002Fspan>&gt; {\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F guard 3: global kill switch, no reschedule\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (\u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">env\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ALARMS_DISABLED\u003C\u002Fspan> === \u003Cspan class=\"hljs-string\">&quot;1&quot;\u003C\u002Fspan>) {\n      \u003Cspan class=\"hljs-variable language_\">console\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">warn\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;poller: ALARMS_DISABLED=1, not rescheduling&quot;\u003C\u002Fspan>);\n      \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan>;\n    }\n\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F guard 2: count runs per clock hour; trip and stop past the budget\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> now = \u003Cspan class=\"hljs-title class_\">Date\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">now\u003C\u002Fspan>();\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> hour = \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">floor\u003C\u002Fspan>(now \u002F \u003Cspan class=\"hljs-number\">3_600_000\u003C\u002Fspan>);\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> stored = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">get\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-title class_\">Budget\u003C\u002Fspan>&gt;(\u003Cspan class=\"hljs-string\">&quot;budget&quot;\u003C\u002Fspan>);\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">budget\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">Budget\u003C\u002Fspan> = stored &amp;&amp; stored.\u003Cspan class=\"hljs-property\">hour\u003C\u002Fspan> === hour ? stored : { hour, \u003Cspan class=\"hljs-attr\">runs\u003C\u002Fspan>: \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan> };\n    budget.\u003Cspan class=\"hljs-property\">runs\u003C\u002Fspan>++;\n    \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;budget&quot;\u003C\u002Fspan>, budget);\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (budget.\u003Cspan class=\"hljs-property\">runs\u003C\u002Fspan> &gt; \u003Cspan class=\"hljs-variable constant_\">MAX_RUNS_PER_HOUR\u003C\u002Fspan>) {\n      \u003Cspan class=\"hljs-variable language_\">console\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">error\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">`poller: \u003Cspan class=\"hljs-subst\">${budget.runs}\u003C\u002Fspan> runs this hour, tripping`\u003C\u002Fspan>);\n      \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;tripped&quot;\u003C\u002Fspan>, now);\n      \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan>;\n    }\n\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F guard 1: the interval can never drop below the floor\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> raw = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">get\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>&gt;(\u003Cspan class=\"hljs-string\">&quot;intervalMs&quot;\u003C\u002Fspan>);\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> interval = \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">max\u003C\u002Fspan>(\u003Cspan class=\"hljs-title class_\">Number\u003C\u002Fspan>(raw) || \u003Cspan class=\"hljs-variable constant_\">MIN_INTERVAL_MS\u003C\u002Fspan>, \u003Cspan class=\"hljs-variable constant_\">MIN_INTERVAL_MS\u003C\u002Fspan>);\n\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F guard 4: idempotent work, keyed by time slot, so a retry or a\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F duplicate run inside the same slot does nothing\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> slot = \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">floor\u003C\u002Fspan>(now \u002F interval);\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> failures = (\u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">get\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>&gt;(\u003Cspan class=\"hljs-string\">&quot;failures&quot;\u003C\u002Fspan>)) ?? \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>;\n    \u003Cspan class=\"hljs-keyword\">let\u003C\u002Fspan> next = now + interval;\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> ((\u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">get\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>&gt;(\u003Cspan class=\"hljs-string\">&quot;lastSlot&quot;\u003C\u002Fspan>)) !== slot) {\n      \u003Cspan class=\"hljs-keyword\">try\u003C\u002Fspan> {\n        \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">doWork\u003C\u002Fspan>(slot);\n        \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;lastSlot&quot;\u003C\u002Fspan>, slot);\n        \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;failures&quot;\u003C\u002Fspan>, \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>);\n      } \u003Cspan class=\"hljs-keyword\">catch\u003C\u002Fspan> (err) {\n        \u003Cspan class=\"hljs-comment\">\u002F\u002F handle failure ourselves: back off instead of throwing into\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-comment\">\u002F\u002F the platform retry, and keep the schedule alive\u003C\u002Fspan>\n        \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> backoff = \u003Cspan class=\"hljs-title class_\">Math\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">min\u003C\u002Fspan>(interval * \u003Cspan class=\"hljs-number\">2\u003C\u002Fspan> ** (failures + \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>), \u003Cspan class=\"hljs-variable constant_\">MAX_BACKOFF_MS\u003C\u002Fspan>);\n        \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;failures&quot;\u003C\u002Fspan>, failures + \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>);\n        \u003Cspan class=\"hljs-variable language_\">console\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">error\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">`poller: work failed (\u003Cspan class=\"hljs-subst\">${failures + \u003Cspan class=\"hljs-number\">1\u003C\u002Fspan>}\u003C\u002Fspan>x), next in \u003Cspan class=\"hljs-subst\">${backoff}\u003C\u002Fspan> ms`\u003C\u002Fspan>, err);\n        next = now + backoff;\n      }\n    }\n\n    \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">setAlarm\u003C\u002Fspan>(next);\n  }\n\n  \u003Cspan class=\"hljs-keyword\">private\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">async\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">doWork\u003C\u002Fspan>(\u003Cspan class=\"hljs-attr\">slot\u003C\u002Fspan>: \u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>): \u003Cspan class=\"hljs-title class_\">Promise\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">void\u003C\u002Fspan>&gt; {\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F your periodic job: poll an API, flush a buffer, expire sessions\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">this\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">ctx\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">put\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;lastWorkSlot&quot;\u003C\u002Fspan>, slot);\n  }\n}\n\n\u003Cspan class=\"hljs-keyword\">export\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">default\u003C\u002Fspan> {\n  \u003Cspan class=\"hljs-keyword\">async\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">fetch\u003C\u002Fspan>(\u003Cspan class=\"hljs-attr\">request\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">Request\u003C\u002Fspan>, \u003Cspan class=\"hljs-attr\">env\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">Env\u003C\u002Fspan>): \u003Cspan class=\"hljs-title class_\">Promise\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-title class_\">Response\u003C\u002Fspan>&gt; {\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> url = \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">URL\u003C\u002Fspan>(request.\u003Cspan class=\"hljs-property\">url\u003C\u002Fspan>);\n    \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (url.\u003Cspan class=\"hljs-property\">pathname\u003C\u002Fspan> === \u003Cspan class=\"hljs-string\">&quot;\u002Fstart&quot;\u003C\u002Fspan>) {\n      \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> stub = env.\u003Cspan class=\"hljs-property\">POLLER\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">get\u003C\u002Fspan>(env.\u003Cspan class=\"hljs-property\">POLLER\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">idFromName\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;main&quot;\u003C\u002Fspan>));\n      \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> stub.\u003Cspan class=\"hljs-title function_\">start\u003C\u002Fspan>(\u003Cspan class=\"hljs-number\">5\u003C\u002Fspan> * \u003Cspan class=\"hljs-number\">60_000\u003C\u002Fspan>);\n      \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Response\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;started\\n&quot;\u003C\u002Fspan>);\n    }\n    \u003Cspan class=\"hljs-keyword\">return\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Response\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;not found\\n&quot;\u003C\u002Fspan>, { \u003Cspan class=\"hljs-attr\">status\u003C\u002Fspan>: \u003Cspan class=\"hljs-number\">404\u003C\u002Fspan> });\n  },\n};\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>What each guard covers:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Minimum interval.\u003C\u002Fstrong> A bad value in storage, a \u003Ccode>0\u003C\u002Fcode>, \u003Ccode>NaN\u003C\u002Fcode> or a missing key, cannot schedule the next run sooner than 60 seconds.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Run budget.\u003C\u002Fstrong> Each object counts its runs per clock hour. With a 5-minute interval it should run 12 times; 120 means something is wrong, so it records \u003Ccode>tripped\u003C\u002Fcode> and stops rescheduling. You restart it with \u003Ccode>start()\u003C\u002Fcode> after fixing the cause.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Kill switch.\u003C\u002Fstrong> One variable stops every object in the namespace on its next run without touching their storage.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Idempotent work.\u003C\u002Fstrong> The job is keyed by its time slot, so an at-least-once retry or a duplicate fire inside the same slot is a no-op.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The handler catches its own errors and backs off, doubling the interval up to an hour, instead of throwing. That trades the platform's six fast retries for a schedule that slows down when the dependency is down and never stops silently. If you prefer the platform retries, rethrow, but then keep the work idempotent.\u003C\u002Fp>\n\u003Cp>The Wrangler config. Note the kill switch lives in \u003Ccode>vars\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre class=\"code-block\" data-lang=\"json\">\u003Ccode class=\"hljs language-json\">\u003Cspan class=\"hljs-punctuation\">{\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-attr\">&quot;name&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;poller&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-attr\">&quot;main&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;src\u002Findex.ts&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-attr\">&quot;compatibility_date&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;2026-10-01&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-attr\">&quot;durable_objects&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">{\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-attr\">&quot;bindings&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">[\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">{\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">&quot;name&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;POLLER&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">&quot;class_name&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;Poller&quot;\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">}\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">]\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-punctuation\">}\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-attr\">&quot;migrations&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">[\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">{\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">&quot;tag&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;v1&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">&quot;new_sqlite_classes&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">[\u003C\u002Fspan>\u003Cspan class=\"hljs-string\">&quot;Poller&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">]\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">}\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">]\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">,\u003C\u002Fspan>\n  \u003Cspan class=\"hljs-attr\">&quot;vars&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">{\u003C\u002Fspan> \u003Cspan class=\"hljs-attr\">&quot;ALARMS_DISABLED&quot;\u003C\u002Fspan>\u003Cspan class=\"hljs-punctuation\">:\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;0&quot;\u003C\u002Fspan> \u003Cspan class=\"hljs-punctuation\">}\u003C\u002Fspan>\n\u003Cspan class=\"hljs-punctuation\">}\u003C\u002Fspan>\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Save it as \u003Ccode>wrangler.jsonc\u003C\u002Fcode>. To pull the switch in an incident, set the value to \u003Ccode>\"1\"\u003C\u002Fcode> and deploy:\u003C\u002Fp>\n\u003Cpre class=\"code-block\" data-lang=\"bash\">\u003Ccode class=\"hljs language-bash\">npx wrangler deploy\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Every object stops at its next alarm. Turning the switch back to \u003Ccode>\"0\"\u003C\u002Fcode> does not restart them, because none rescheduled; call \u003Ccode>\u002Fstart\u003C\u002Fcode> again (or \u003Ccode>start()\u003C\u002Fcode> per object) once the cause is fixed. That is deliberate: a kill switch that quietly resumes the loop when flipped back is not a kill switch.\u003C\u002Fp>\n\n\u003Ch2>A test that proves the loop stops\u003C\u002Fh2>\n\u003Cp>The Workers Vitest integration (\u003Ccode>@cloudflare\u002Fvitest-pool-workers\u003C\u002Fcode>, set up per Cloudflare's guide) has \u003Ccode>runDurableObjectAlarm(stub)\u003C\u002Fcode>, which runs and removes an object's scheduled alarm immediately and returns \u003Ccode>true\u003C\u002Fcode> if one ran. Calling it in a loop simulates an alarm that fires as fast as it is rescheduled, the exact failure you want to rule out:\u003C\u002Fp>\n\u003Cfigure data-post-media=\"6ac8709d392a85804f209fcd\">\u003Cimg src=\"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac8709d392a85804f209fd2-0-5a71170a.png\" alt=\"Automated tests provide certainty that the safety mechanisms actually work.\" loading=\"lazy\">\u003Cfigcaption>Automated tests provide certainty that the safety mechanisms actually work.\u003C\u002Ffigcaption>\u003C\u002Ffigure>\n\u003Cpre class=\"code-block\" data-lang=\"ts\">\u003Ccode class=\"hljs language-typescript\">\u003Cspan class=\"hljs-comment\">\u002F\u002F test\u002Fpoller.test.ts\u003C\u002Fspan>\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { env } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;cloudflare:workers&quot;\u003C\u002Fspan>;\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { runDurableObjectAlarm, runInDurableObject } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;cloudflare:test&quot;\u003C\u002Fspan>;\n\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan> { describe, expect, it } \u003Cspan class=\"hljs-keyword\">from\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;vitest&quot;\u003C\u002Fspan>;\n\n\u003Cspan class=\"hljs-keyword\">declare\u003C\u002Fspan> \u003Cspan class=\"hljs-variable language_\">module\u003C\u002Fspan> \u003Cspan class=\"hljs-string\">&quot;cloudflare:workers&quot;\u003C\u002Fspan> {\n  \u003Cspan class=\"hljs-keyword\">interface\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">ProvidedEnv\u003C\u002Fspan> {\n    \u003Cspan class=\"hljs-attr\">POLLER\u003C\u002Fspan>: \u003Cspan class=\"hljs-title class_\">DurableObjectNamespace\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-keyword\">import\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;..\u002Fsrc\u002Findex&quot;\u003C\u002Fspan>).\u003Cspan class=\"hljs-property\">Poller\u003C\u002Fspan>&gt;;\n  }\n}\n\n\u003Cspan class=\"hljs-title function_\">describe\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;Poller alarm&quot;\u003C\u002Fspan>, \u003Cspan class=\"hljs-function\">() =&gt;\u003C\u002Fspan> {\n  \u003Cspan class=\"hljs-title function_\">it\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;stops rescheduling once the hourly budget is spent&quot;\u003C\u002Fspan>, \u003Cspan class=\"hljs-title function_\">async\u003C\u002Fspan> () =&gt; {\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> stub = env.\u003Cspan class=\"hljs-property\">POLLER\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">get\u003C\u002Fspan>(env.\u003Cspan class=\"hljs-property\">POLLER\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">idFromName\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;loop-test&quot;\u003C\u002Fspan>));\n    \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> stub.\u003Cspan class=\"hljs-title function_\">start\u003C\u002Fspan>(\u003Cspan class=\"hljs-number\">60_000\u003C\u002Fspan>);\n\n    \u003Cspan class=\"hljs-keyword\">let\u003C\u002Fspan> runs = \u003Cspan class=\"hljs-number\">0\u003C\u002Fspan>;\n    \u003Cspan class=\"hljs-keyword\">while\u003C\u002Fspan> (\u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">runDurableObjectAlarm\u003C\u002Fspan>(stub)) {\n      runs++;\n      \u003Cspan class=\"hljs-keyword\">if\u003C\u002Fspan> (runs &gt; \u003Cspan class=\"hljs-number\">1_000\u003C\u002Fspan>) \u003Cspan class=\"hljs-keyword\">throw\u003C\u002Fspan> \u003Cspan class=\"hljs-keyword\">new\u003C\u002Fspan> \u003Cspan class=\"hljs-title class_\">Error\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;alarm never stopped&quot;\u003C\u002Fspan>);\n    }\n\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F 120 budgeted runs plus the one that trips the guard\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-title function_\">expect\u003C\u002Fspan>(runs).\u003Cspan class=\"hljs-title function_\">toBeLessThanOrEqual\u003C\u002Fspan>(\u003Cspan class=\"hljs-number\">121\u003C\u002Fspan>);\n\n    \u003Cspan class=\"hljs-keyword\">const\u003C\u002Fspan> { alarm, tripped, workSlot } = \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> \u003Cspan class=\"hljs-title function_\">runInDurableObject\u003C\u002Fspan>(stub, \u003Cspan class=\"hljs-title function_\">async\u003C\u002Fspan> (_obj, state) =&gt; ({\n      \u003Cspan class=\"hljs-attr\">alarm\u003C\u002Fspan>: \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> state.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-title function_\">getAlarm\u003C\u002Fspan>(),\n      \u003Cspan class=\"hljs-attr\">tripped\u003C\u002Fspan>: \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> state.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">get\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>&gt;(\u003Cspan class=\"hljs-string\">&quot;tripped&quot;\u003C\u002Fspan>),\n      \u003Cspan class=\"hljs-attr\">workSlot\u003C\u002Fspan>: \u003Cspan class=\"hljs-keyword\">await\u003C\u002Fspan> state.\u003Cspan class=\"hljs-property\">storage\u003C\u002Fspan>.\u003Cspan class=\"hljs-property\">get\u003C\u002Fspan>&lt;\u003Cspan class=\"hljs-built_in\">number\u003C\u002Fspan>&gt;(\u003Cspan class=\"hljs-string\">&quot;lastWorkSlot&quot;\u003C\u002Fspan>),\n    }));\n    \u003Cspan class=\"hljs-title function_\">expect\u003C\u002Fspan>(alarm).\u003Cspan class=\"hljs-title function_\">toBeNull\u003C\u002Fspan>();\n    \u003Cspan class=\"hljs-title function_\">expect\u003C\u002Fspan>(tripped).\u003Cspan class=\"hljs-title function_\">toBeTypeOf\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;number&quot;\u003C\u002Fspan>);\n    \u003Cspan class=\"hljs-comment\">\u002F\u002F all runs happened inside one slot, so the work ran once\u003C\u002Fspan>\n    \u003Cspan class=\"hljs-title function_\">expect\u003C\u002Fspan>(workSlot).\u003Cspan class=\"hljs-title function_\">toBeTypeOf\u003C\u002Fspan>(\u003Cspan class=\"hljs-string\">&quot;number&quot;\u003C\u002Fspan>);\n  });\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Run it:\u003C\u002Fp>\n\u003Cpre class=\"code-block\" data-lang=\"bash\">\u003Ccode class=\"hljs language-bash\">npx vitest run\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Remove guard 2 and the test throws \u003Ccode>alarm never stopped\u003C\u002Fcode>. That is the loop that ends up on an invoice, caught in a second on your laptop. If a run in the test happens to straddle an hour boundary the budget resets once, which is why the assertion is \"at most 121\", not \"exactly\".\u003C\u002Fp>\n\n\u003Ch2>The guard outside the code\u003C\u002Fh2>\n\u003Cp>Guards in code catch the bugs you thought of. Set a usage alert for the ones you did not. Cloudflare added budget alerts in April 2026 and default informational alerts in June, but they warn rather than cap: there is still no hard spending limit on Workers. Set the alert threshold close to your normal monthly spend, and send it somewhere a person reads the same day, not a shared inbox.\u003C\u002Fp>\n\u003Cp>Checklist before you ship an alarm:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Every \u003Ccode>setAlarm()\u003C\u002Fcode> argument is at least \u003Ccode>Date.now() + MIN_INTERVAL_MS\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>Nothing outside \u003Ccode>start()\u003C\u002Fcode> re-arms the alarm, especially not \u003Ccode>fetch()\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>A per-object run budget trips and stops.\u003C\u002Fli>\n\u003Cli>A kill switch exists, and you have pulled it once in staging.\u003C\u002Fli>\n\u003Cli>The work is idempotent per slot.\u003C\u002Fli>\n\u003Cli>The loop test runs in CI.\u003C\u002Fli>\n\u003Cli>A billing alert is set and routed to a person.\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Ch2>Sources\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fdurable-objects\u002Fapi\u002Falarms\u002F\">Cloudflare Docs: Durable Objects Alarms API\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fdurable-objects\u002Fplatform\u002Fpricing\u002F\">Cloudflare Docs: Durable Objects pricing\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers\u002Ftesting\u002Fvitest-integration\u002Ftest-apis\u002F\">Cloudflare Docs: Vitest integration test APIs\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fppc.land\u002Fdeveloper-faces-36-000-cloudflare-bill-after-queue-loop-post-says\u002F\">PPC Land: Developer faces $36,000 Cloudflare bill after queue loop\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fx.com\u002Fshmily7\u002Fstatus\u002F2107481028726251762\">The reported Durable Object alarm bill, on X\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>","Durable Object alarms are at-least-once, retried, and usually rescheduled from inside the handler, which makes a billing loop easy to write. Four guards in TypeScript, a kill switch, and a Vitest test that proves the loop stops.",[9,10,11,12,13],"cloudflare","durable-objects","typescript","cost-control","ai-assisted","if.codes","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac8709b392a85804f209fb3-0-9143ed0c.png","2026-10-09T06:09:10.127Z","Guarding Durable Object alarm loops: backoff, idempotency, kill switch","Stop a self-rescheduling Durable Object alarm from billing you in a loop: minimum interval, run budget, kill switch, idempotent work and a test.",7,[21,33,45],{"slug":22,"title":23,"type":24,"summary":25,"tags":26,"author":14,"cover_url":31,"published_at":32,"updated_at":32},"cctld-hijack-counterfeit-certs-caa-accounturi-ct-watch","Counterfeit certs via hijacked ccTLDs: CAA with accounturi and a CT watch for your domains in 15 minutes","blog","Attackers took over the .gh, .sl and .as registries and got 12 valid certificates for Google domains. CAA would not have stopped the hijack. Here is the CAA record that limits the damage afterwards, and a CT watch script that tells you within a day.",[27,28,29,30,13],"tls","caa","certificate-transparency","dns","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac75337392a85804f207d25-0-12f538ac.png","2026-10-08T22:47:12.408Z",{"slug":34,"title":35,"type":24,"summary":36,"tags":37,"author":14,"cover_url":42,"published_at":43,"updated_at":44},"embeddinggemma-2-qdrant-truncation-recall","EmbeddingGemma 2 in Qdrant: one 740M model for text, images and audio, and what truncating 768 to 128 dims costs your recall","Google's open multimodal embedder runs locally and truncates from 768 to 128 dims. Index one Qdrant collection at three sizes from a single encode pass, then measure recall@10 on your own queries instead of trusting a benchmark.",[38,39,40,41,13],"embeddings","qdrant","rag","open-models","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac5cceedf0d655df505306e-0-d17d0fef.png","2026-10-07T07:21:20.071Z","2026-10-07T07:21:20.072Z",{"slug":46,"title":47,"type":24,"summary":48,"tags":49,"author":14,"cover_url":53,"published_at":54,"updated_at":55},"cloudflare-access-strict-service-token-auth-migration","Strict service token auth in Cloudflare Access: moving your scripts and CI over before it bites","Cloudflare Access now has a strict mode for service tokens: 401\u002F403 instead of a 302 to the login page, only Service Auth policies count, and no CF_Authorization cookie. New orgs get it forced on from 5 October. A 15-minute check and switch for existing orgs.",[9,50,51,52,13],"zero-trust","ci-cd","authentication","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac4a8860956594c947bd197-0-f768b25b.png","2026-10-06T08:30:13.154Z","2026-10-06T08:30:13.155Z",[57,60,63,65,68,79,90,101,114,128,140,152,163,174,182,193,204,213,224,234,243,253],{"slug":4,"title":5,"type":24,"summary":7,"tags":58,"author":14,"cover_url":15,"published_at":16,"updated_at":59,"reading_minutes":19},[9,10,11,12,13],"2026-10-09T06:09:10.128Z",{"slug":22,"title":23,"type":24,"summary":25,"tags":61,"author":14,"cover_url":31,"published_at":32,"updated_at":32,"reading_minutes":62},[27,28,29,30,13],6,{"slug":34,"title":35,"type":24,"summary":36,"tags":64,"author":14,"cover_url":42,"published_at":43,"updated_at":44,"reading_minutes":62},[38,39,40,41,13],{"slug":46,"title":47,"type":24,"summary":48,"tags":66,"author":14,"cover_url":53,"published_at":54,"updated_at":55,"reading_minutes":67},[9,50,51,52,13],5,{"slug":69,"title":70,"type":24,"summary":71,"tags":72,"author":14,"cover_url":76,"published_at":77,"updated_at":78,"reading_minutes":62},"cloudflare-traces-trace-rules-debug-one-customer","Why was that request blocked? Tracing one customer at 100% with Cloudflare Traces and Trace Rules","Cloudflare Traces (open beta) shows a request's path through WAF rules, transforms, cache, Workers and origin as one trace. A recipe: low baseline sampling, a 100% Trace Rule for one host or debug header, traceparent to your origin, OTLP export to your own collector, and what December pricing means.",[9,73,74,75,13],"observability","opentelemetry","tracing","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac338052cb6b40613ac2b7c-0-2294d114.png","2026-10-05T06:22:19.055Z","2026-10-05T06:22:19.056Z",{"slug":80,"title":81,"type":24,"summary":82,"tags":83,"author":14,"cover_url":88,"published_at":89,"updated_at":89,"reading_minutes":67},"copyescape-cve-2026-17106-patch-docker-cp","CopyEscape (CVE-2026-17106): patch docker cp, and stop copying out of running containers","A race in docker cp lets a malicious container write files anywhere the copying process can write on the host. That matters for CI runners and AI-agent sandboxes that copy results out. Check your versions, patch, and change copy-out jobs to stop the container first.",[84,85,86,87,13],"docker","security","cve","ci","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac218ebfd770659725abe68-0-eee7c8f7.png","2026-10-04T23:51:14.689Z",{"slug":91,"title":92,"type":24,"summary":93,"tags":94,"author":14,"cover_url":97,"published_at":98,"updated_at":99,"reading_minutes":100},"protected-quick-tunnels-vs-tailscale-funnel","Share localhost with three named people: Cloudflare's Protected Quick Tunnels vs Tailscale Funnel","cloudflared 2026.9.3 adds --allowed-mail: your quick tunnel now sits behind an email one-time PIN, checked against an allow-list on your own machine, free and without a Cloudflare account. The commands, what it protects, and when Tailscale Serve or Funnel is the better fit.",[9,95,96,85,13],"tailscale","tunnels","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6ac218d8fd770659725abe3a-0-40f8cb2e.png","2026-10-04T23:19:14.764Z","2026-10-04T23:19:14.765Z",4,{"slug":102,"title":103,"type":24,"summary":104,"tags":105,"author":14,"cover_url":111,"published_at":112,"updated_at":113,"reading_minutes":62},"si-domains-super-intelligence-data",".si after 'Super Intelligence': did one UN speech move a ccTLD?","Trump renamed AI 'super intelligence' at the UN on 22 September 2026 and Slovenia's .si went from about 190,000 names to almost 276,000 in a month. Registry numbers, prices, and 87 WHOIS checks: the obvious AI names were gone years ago; the compounds went in days.",[106,107,108,109,110,13],"domains","si","tld","data","ai","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abaaee53e21d5cbd14d442d-0-d6243bc5.png","2026-10-04T22:36:14.598Z","2026-10-04T22:36:14.599Z",{"slug":115,"title":116,"type":24,"summary":117,"tags":118,"author":14,"cover_url":124,"published_at":125,"updated_at":126,"reading_minutes":127},"palantir-agent-stack-python","Steal Palantir's agent stack: typed tools, one LLM gateway, swappable models","An X thread boils Palantir's AIP docs down to four agent patterns. We check each one against the docs, then build them in one stdlib-only Python file: typed business-object tools, a gateway that masks PII, caches and retries, a model set in config, and schedule\u002Fevent\u002FAPI triggers.",[119,120,121,122,123,13],"ai-agents","llm","python","architecture","palantir","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88f9c951ea7137fa3872-0-48eb7eee.png","2026-10-01T23:05:10.531Z","2026-10-01T23:05:10.532Z",10,{"slug":129,"title":130,"type":24,"summary":131,"tags":132,"author":14,"cover_url":136,"published_at":137,"updated_at":138,"reading_minutes":139},"claude-code-effort-levels","Effort levels in Claude Code: when max effort pays off and when it just burns tokens","Anthropic's effort deep dive (Terminal-Bench 3.0 plus three builds) shows higher effort mostly buys verification and edge-case testing, not smarter code. A rule of thumb per task type, the commands to set effort, and a script to measure cost vs pass rate on your own repo.",[133,134,120,135,13],"claude-code","ai-coding","developer-tools","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88edc951ea7137fa3804-0-2716005a.png","2026-10-01T22:32:14.139Z","2026-10-02T04:44:58.001Z",9,{"slug":141,"title":142,"type":24,"summary":143,"tags":144,"author":14,"cover_url":148,"published_at":149,"updated_at":150,"reading_minutes":151},"agentic-inbox-cloudflare-setup","Self-host an AI email agent on Cloudflare Workers: agentic-inbox set up and costed","Cloudflare's open-source agentic-inbox runs a full email client on Workers, with one SQLite Durable Object per mailbox and a Kimi K2.5 agent that drafts replies. Covers the post-deploy steps people miss (Access, sending, routing, mailbox first) and the cost.",[9,145,119,146,147,13],"workers","email","self-hosting","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abd88dac951ea7137fa378b-0-5b0ec96d.png","2026-10-01T08:17:18.646Z","2026-10-05T05:56:11.919Z",8,{"slug":153,"title":154,"type":24,"summary":155,"tags":156,"author":14,"cover_url":160,"published_at":161,"updated_at":162,"reading_minutes":151},"audit-ai-agent-public-traces","Nearly a million leaked links: auditing what your AI agents leave on the public web","OpenAI's agent swarm left almost a million public shortener URLs holding credentials. Here's a tested shell + gitleaks audit to find the shortlinks, pastes and webhooks your own agents created, scan them for secrets and close the channels.",[85,157,158,159,120,13],"agents","secrets","gitleaks","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abaad873e21d5cbd14d4397-0-0f4f51f7.png","2026-10-01T07:43:19.642Z","2026-10-01T08:44:43.041Z",{"slug":164,"title":165,"type":24,"summary":166,"tags":167,"author":14,"cover_url":171,"published_at":172,"updated_at":173,"reading_minutes":151},"mikrotrick-check-patch-mikrotik","MikroTrick: check and patch your MikroTik in 15 minutes","Two chained RouterOS bugs give anyone who can reach SSH full admin, no password needed, and attacks started before the patch. Find exposed SSH, check the version, grep for the published IoCs, patch and move management behind WireGuard.",[85,168,169,170,147,13],"mikrotik","routeros","ssh","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abcd9c0838b650cb96b3d10-0-6cd107e0.png","2026-10-01T07:02:15.03Z","2026-10-01T08:44:41.391Z",{"slug":175,"title":176,"type":24,"summary":177,"tags":178,"author":14,"cover_url":179,"published_at":180,"updated_at":181,"reading_minutes":151},"agent-sandbox-dns-egress-lockdown","Your agent sandbox leaks through DNS: lock down egress in 15 minutes","An OpenAI model escaped its sandbox by tunnelling questions through DNS. Here is a tested Docker Compose setup for coding agents: a DNS allowlist, a logging egress proxy and a kill switch that actually fires.",[85,84,157,30,147,13],"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abaaca13e21d5cbd14d4306-0-cb93fe1b.png","2026-10-01T03:00:15.943Z","2026-10-01T08:44:43.165Z",{"slug":183,"title":184,"type":24,"summary":185,"tags":186,"author":14,"cover_url":190,"published_at":191,"updated_at":192,"reading_minutes":19},"who-blocks-ai-crawlers-robots-txt","Who blocks AI crawlers? robots.txt vs the network edge, with numbers","I scanned robots.txt on the top 300 sites: 33 of 138 block GPTBot, 14 block training but allow AI search. What each AI bot directive controls, why robots.txt is only a request, and a copy-paste policy plus nginx rule for small SaaS sites.",[110,187,188,9,189,13],"robots-txt","seo","saas","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abcd9c1838b650cb96b3d1b-0-11f94198.png","2026-09-30T21:00:20.673Z","2026-10-01T20:47:34.092Z",{"slug":194,"title":195,"type":24,"summary":196,"tags":197,"author":14,"cover_url":201,"published_at":202,"updated_at":203,"reading_minutes":100},"bullet-time-with-first-last-frame-video","Bullet time with first\u002Flast-frame video: orbiting a frozen moment from three stills","A freeze-frame camera orbit built from generated stills: one action shot, two camera-move angles, two first\u002Flast-frame clips between them, stitched and ping-ponged. The pipeline, the seams, and where the model re-imagines the water.",[110,198,199,200],"comfyui","video-generation","flowdsl","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abae46c45201648bfd477a7-0-6f4d036e.png","2026-09-28T22:42:41Z","2026-09-28T22:42:41.6Z",{"slug":205,"title":206,"type":24,"summary":207,"tags":208,"author":14,"cover_url":210,"published_at":211,"updated_at":212,"reading_minutes":19},"an-ai-media-pipeline-that-shows-its-work","An AI media pipeline that shows its work: ComfyUI presets, FlowDSL routing and the misses","How the images on my sites are generated: four ComfyUI presets behind one Go module, job rows as state, FlowDSL flows for routing, per-post media in the admin — and the bugs and model misses I hit shipping it. This post's own images were made the same way.",[110,200,198,209],"image-generation","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6aba8ba317ceba3543925be4-0-2f6b8a5c.png","2026-09-28T15:57:50Z","2026-10-01T20:47:34.327Z",{"slug":214,"title":215,"type":24,"summary":216,"tags":217,"author":14,"cover_url":220,"published_at":221,"updated_at":222,"reading_minutes":223},"openai-embeddings-python-mongodb","Transforming Text into Vectors: OpenAI Embeddings in Python","Learn how to generate text embeddings with the OpenAI API in Python to power semantic search, recommendations, and more. Includes practical examples with MongoDB integration and cost analysis.",[218,110,121,219],"openai","mongodb","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c2d-0-2e60b732.png","2024-11-23T00:00:00Z","2026-09-28T22:31:01.385Z",3,{"slug":225,"title":226,"type":24,"summary":227,"tags":228,"author":14,"cover_url":230,"published_at":231,"updated_at":232,"reading_minutes":233},"check-pricing-availability-ing-domains","Last Chance to Grab Short .ING Domains: The Extended List Part II","Welcome back to the second part of our exciting exploration into the .ING domain zone! This time, I've expanded our horizons to bring you an even larger selection of .ING domain names. List of over 24,000 domain names inside.",[106,229],"business","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c38-0-c55f4c8d.png","2023-12-14T00:00:00Z","2026-09-28T22:31:01.453Z",1,{"slug":235,"title":236,"type":24,"summary":237,"tags":238,"author":14,"cover_url":239,"published_at":240,"updated_at":241,"reading_minutes":242},"impressive-ing-domains","Unveiling the Impressive .ING Domains","Discover the vast potential of the new .ING domain zone in my latest blog post! I've used AI and a Python script to unearth a treasure trove of available domain names. From budget-friendly picks to exclusive premium domains, there's something for every ambition. Plus, a special list of unique, lesser-known domains awaits.",[106,229],"https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c43-0-03be24b7.png","2023-12-11T00:00:00Z","2026-09-28T22:31:01.527Z",2,{"slug":244,"title":245,"type":24,"summary":246,"tags":247,"author":14,"cover_url":250,"published_at":251,"updated_at":252,"reading_minutes":223},"secured-web-server-in-5-minutes","Fortify Web Server Security in 5 Minutes with Tailscale","Tailscale revolutionizes secure networking with its user-friendly approach, effortlessly connecting devices across diverse networks.",[248,95,249],"firewall","webserver","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c4e-0-eae6f62d.png","2023-11-03T00:00:00Z","2026-09-28T22:31:01.597Z",{"slug":254,"title":255,"type":24,"summary":256,"tags":257,"author":14,"cover_url":260,"published_at":261,"updated_at":262,"reading_minutes":62},"lets-encrypt-free-ssl","How to Secure Your Website with Free SSL Certificates for a Lifetime","Let’s Encrypt certificates have revolutionized internet security by providing free, automated, and widely trusted SSL\u002FTLS certificates. The non-profit Certificate Authority (CA) has significantly contributed to a more secure web environment by simplifying the process of securing websites with HTTPS.",[258,259,249],"ssl","https","https:\u002F\u002Fmedia.stufio.com\u002Fmedia\u002Fifcodes\u002Fmediagen\u002F6a\u002F6abad0fa45201648bfd46c59-0-bf2a9a0a.png","2023-11-01T00:00:00Z","2026-09-28T22:39:13.555Z"]