scram
A reactor scram is the emergency full shutdown: drop every rod, stop the reaction, ask questions later.
Cloudflare has no hard spending limit. Its budget alerts say so explicitly — "The alert is informational only. It does not cap your usage or impact your account in any way" — and they are computed once a day from the previous day's usage, so the email arrives a day after the money is gone.
scram is the missing enforcement. It runs as a Worker in your own account, adds up what you have actually spent this billing cycle, and when you cross a line you set, it switches the account off.
What it does
- Estimates in minutes, not a day.
- Reads the GraphQL Analytics API every 15 minutes and prices it against Cloudflare's published rates.
- Discovers everything.
- Every Worker, route, custom domain, cron trigger and
workers.devsubdomain in the account, on every run. Deploy a new project and it is covered immediately. - Reversible.
- The state it took away is written down before anything is touched, and
POST /api/restoreputs it back. - Safe by default.
- Ships disarmed. It will watch and report for as long as you want before you let it touch anything.
- Cannot switch itself off.
- scram excludes itself from every plan, because a scram that disables its own route cannot be restored through its own UI.
- Costs almost nothing to run.
- 2,880 Worker invocations a month and a D1 row per check. Inside the free tier of the thing it is protecting you from.
What a trip actually does
For every Worker except the protected ones:
- Routes. Deletes each zone route pointing at it. Traffic stops reaching the Worker and the zone serves whatever it would without one.
- Custom domains. Deletes the Worker custom domain binding.
- Crons. Clears the schedule list, so nothing fires again.
- workers.dev. Disables the subdomain.
What it deliberately does not do: delete Workers, delete code, delete bindings, delete R2 buckets or objects, delete D1 databases or rows, cancel subscriptions, or downgrade plans. Every one of those is either irreversible or does not stop spend any faster than the four above.
Storage keeps accruing while tripped. R2 and D1 charge for bytes at rest, and nothing short of deleting your data stops that. scram will not delete your data. If you are tripping on storage rather than compute, it buys you time, not a cure.
Install
Needs Node 22+ and a Cloudflare account.
git clone https://github.com/pid1/scram && cd scram
npm install
npx wrangler login
./scripts/setup.sh # database, migrations, secrets
npm run deploy
setup.sh prints the generated admin token at the end. Write it down. It is what opens the status page, and it is not recoverable afterwards.
Arming it
scram deploys disarmed, because ARMED is a secret and starts unset. In that state it collects usage, prices it, records a reading, and on crossing the threshold it writes a full trip record and notifies you, marked as a dry run, listing exactly what it would have disabled. Nothing changes.
Let it run for a few days. Compare the number on the status page against Manage Account → Billing → Billable Usage in the dashboard. When you believe it, arm it:
printf 1 | npx wrangler secret put ARMED
Disarm again at any time with npx wrangler secret delete ARMED. Neither needs a redeploy.
Configuration
| Var | Default | Meaning |
|---|---|---|
SCRAM_AT_USD | 20 | Estimated cycle spend that trips the switch |
WARN_AT_USD | 5 | Sends a notification, changes nothing |
BILLING_CYCLE_DAY | 1 | Day of month your cycle starts, per the dashboard |
PROTECT | empty | Extra scripts to never touch. scram is always included |
ACTIONS | all four | routes, custom_domains, crons, subdomain |
CF_ACCOUNT_ID | empty | Only needed if the token can see several accounts |
Thresholds are measured against usage-based spend only. The API token scram needs, and why it needs each scope, is in the README. That token can disable every Worker in the account, which is the entire point of it, and the reason it lives only as a Worker secret.
Endpoints
GET / | Status page |
GET /healthcheck | Unauthenticated, returns ok |
GET /api/status | Current estimate, per-meter breakdown, policy, history |
GET /api/preview | Everything a trip would disable right now |
POST /api/check | Run a check immediately |
POST /api/scram | Trip manually, ignoring the estimate. Honours ARMED |
POST /api/restore | Undo the open trip |
Everything under /api needs Authorization: Bearer <ADMIN_TOKEN>.
What it is not
Currently priced: Workers, D1, R2, Durable Objects, Vectorize, Queues. Rates were verified on 2026-09-19, and the date is shown on the status page so a stale table is visible rather than quietly wrong.
- Storage is an accrual. GB-months are computed the way Cloudflare documents, daily peak averaged over a 30-day month, so mid-cycle the figure is what you have accrued so far, not what the month will end at.
- Unknown R2 operations are priced as Class A, the expensive class, so a new operation type over-estimates rather than under-estimates.
- Blind spots are loud. If a collector fails, the affected product is listed on the status page as a blind spot and the total is flagged as an undercount. If every collector fails, scram notifies you and refuses to read $0 as safe.
- It needs the platform to be up. If Cloudflare's API, the analytics pipeline or the Worker itself is down, it does not fire. A failed check notifies you; a platform-wide outage might not.
The number is an estimate. It is not your invoice, and it is not a guarantee.