Cloudsteading
Usage Guard Collector’s author-supplied dashboard showing usage totals and a highlighted D1 spike; synthetic sample account data.
Usage Guard Collector’s author-supplied dashboard showing usage totals and a highlighted D1 spike; synthetic sample account data.

Usage Guard Collector

Watch Cloudflare usage spikes with a small Worker, D1 dashboard and optional Slack or Discord alerts.

Usage Guard Collector is a self-hosted Cloudflare Budget Alerts alternative built on Cloudflare (D1, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@HowardZlh

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

A small monitored account and modest dashboard traffic can fit Workers Free and D1 Free allowances. Analytics access, dataset availability, database growth and CPU use must be checked; no zero-cost installation or bill was measured.

Hosting requirements
  • One Worker Cron runs every six hours, for four scheduled runs and eight GraphQL requests per day, plus actual dashboard requests. Keep account-wide Worker traffic and CPU within the plan limits.
  • Use your own D1 database. Free D1 allowances are account-wide: 5 million rows read/day, 100,000 rows written/day and 5 GB total storage. Indexes, repeated upserts and historical scans consume capacity.
  • A few monitored resources and limited page views are the qualifying workload. The code has no retention pruning or GraphQL pagination; large or long-lived accounts need a separate capacity plan.
  • The monitored account must expose the six requested analytics datasets and fields to its read token. GraphQL availability and limits vary by account and plan; this was not verified with a live account.
  • workers.dev avoids a required domain purchase. Slack/Discord integrations and optional access protection have separate setup and service conditions. No required email, AI, R2, KV, Queue or Durable Object application service is provisioned.
Check current pricing ↗
Review findings & limitations
  • The author-supplied screenshot shows the actual HTML dashboard with synthetic sample account data. It is not a capture of our account, a fresh installation or a measured production bill.
  • The status page has no authentication. It exposes account-wide usage totals to anyone with its URL. Use your own access policy before exposing private usage; this review did not configure or test Access.
  • Cloudflare analytics are usage counts, not invoice data or a spending limit. Neither this collector nor the compared Budget alerts feature stops workloads or caps charges.
  • Only Workers and D1 are deployed. Durable Objects, KV, R2 and Queues are monitored datasets, not required application bindings.
  • The combined GraphQL request asks for six datasets, each limited to 1,000 groups, without pagination. Dataset availability and field limits depend on the monitored account; check its settings and collection logs. A GraphQL error aborts that collection run.
  • The collector refreshes yesterday and today; it does not backfill a complete historical week. Detection begins with the available preceding samples and skips metrics without a baseline. Missing days are not automatically filled with zero.
  • Default spike floors reflect Workers Paid allowances and can be much too high for a Free account. Set THRESHOLDS_JSON for the workload being monitored; monitoring alone is not a quota-protection guarantee.
  • D1 rows are stored per UTC day, product, metric and resource dimension, with no retention cleanup in the reviewed code. Storage, scans, index writes and large batches grow with account size and deployment age.
  • Slack and Discord webhooks are optional. Failed targets are retried on subsequent runs; when one succeeds and another fails, the successful target can receive duplicates. No live delivery or concurrent-run test was performed.
  • Create a separate Account Analytics Read credential for the single monitored account. The collector writes its own D1 history but does not call Cloudflare resource-management write APIs. Do not confuse the runtime read token with deployment credentials.
  • Replace the placeholder D1 ID, apply the migration and set both required runtime secrets. CI checks and a dry-run are defined upstream; it does not deploy the Worker automatically. The hosted Usage Guard service is a waitlist, not a completed product.
Sources checked 01/10/2026

Repository snapshot: f9809bb. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • cloudflare-budget-alerts ↗

    ## Why watch Cloudflare usage instead of waiting for Budget Alerts?

  • workers ↗

    "main": "src/index.ts"

  • d1 ↗

    "binding": "USAGE"

  • free-tier-eligible ↗

    | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |

  • free-tier-eligible ↗

    | Rows read | 5 million / day |

  • free-tier-eligible ↗

    | Rows written | 100,000 / day |

  • free-tier-eligible ↗

    | Storage (per GB stored) | 5 GB (total) |

  • free-tier-eligible ↗

    "triggers": { "crons": ["0 */6 * * *"] }

  • MIT ↗

    MIT License Copyright (c) 2026 HowardZlh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

  • architecture ↗

    "triggers": { "crons": ["0 */6 * * *"] }

  • architecture ↗

    runCollect(env).then((s) => {

  • architecture ↗

    summary.written += await upsertRows(env.USAGE, toRows(date, account), collectedAt);

  • architecture ↗

    export const GRAPHQL_ENDPOINT = "https://api.cloudflare.com/client/v4/graphql";

  • architecture ↗

    const results = await notify(env, formatMessage(today, fresh), fetchImpl);

  • architecture ↗

    <h1>Cloudflare usage, last ${m.dates.length} days</h1>

Upstream screenshot · HowardZlh/usage-guard-collector repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

Cloudflare Budget Alerts logoCloudflare Budget Alerts ↗

An alternative approach to alerting on Cloudflare usage: it detects a large change in daily metrics and can notify Slack or Discord. Budget alerts track monetary billing thresholds and send email. The collector does not reproduce invoice totals, dollar budgets, email delivery or a kill switch; the two can be used together.

See supporting source ↗
external SaaS target
varies
→ D1 + Workers

How it works

The shape of Usage Guard Collector on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
usage-guard-collector
wrangler.jsonc
↓
App
usage-guard-collector
entry
Cloudflare Workers
Entrypoint: src/index.tsConfigured cron (UTC): 0 */6 * * *
↓

Configuration and workflow sources

Reviewed commit f9809bb41ba5. This configuration evidence comes from reading source files. Execution checks, when available, appear in the project's runtime review.

Deployment configuration · 1 file
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-09-01

usage-guard-collector · default

Entrypoint: src/index.ts

Cron triggers (UTC): 0 */6 * * *

  • USAGE → D1

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/index.ts ↗
  • L10 · scheduled handler exported · calls ctx.waitUntil, then, runCollect, console.log, JSON.stringify
  • L19 · fetch handler exported · calls renderIndex
src/run.ts ↗
  • L24 · isoDate calls (conditional paths may differ): slice, d.toISOString
  • L28 · addDays calls (conditional paths may differ): d.setUTCDate, d.getUTCDate, isoDate
  • L43 · runCollect calls (conditional paths may differ): isoDate, now.toISOString, addDays, fetchUsage, upsertRows, toRows, loadTotals, buildSeries, spike, parseThresholds, filterUnnotified, notify, formatMessage, results.filter, results.every, markNotified, String
  • L81 · renderIndex calls (conditional paths may differ): isoDate, addDays, loadTotals, buildSeries, spike, parseThresholds, first, env.USAGE.prepare, dates.push, renderPage, pivot, totals.filter

Environment references: env.CF_API_TOKEN · env.CF_ACCOUNT_ID · env.USAGE · env.THRESHOLDS_JSON

src/graphql.ts ↗
  • L87 · fetchUsage calls (conditional paths may differ): fetchImpl, JSON.stringify, res.json, join, body.errors.map
  • L135 · r2Class calls (conditional paths may differ): R2_CLASS_A.has, R2_CLASS_B.has
  • L143 · kvMetric calls (conditional paths may differ): actionType.toLowerCase, KV_ACTIONS.has
  • L149 · add calls (conditional paths may differ): acc.get, acc.set
  • L157 · toRows calls (conditional paths may differ): add, kvMetric, r2Class, acc.values
src/notify.ts ↗
  • L6 · formatMessage calls (conditional paths may differ): spikes.map, s.today.toLocaleString, s.baseline.toLocaleString, formatRatio, lines.join
  • L21 · post calls (conditional paths may differ): fetchImpl, JSON.stringify, String
  • L40 · notify calls (conditional paths may differ): jobs.push, post, Promise.all

Environment references: env.DISCORD_WEBHOOK_URL · env.SLACK_WEBHOOK_URL

src/page.ts ↗
  • L5 · escapeHtml calls (conditional paths may differ): replaceAll, s.replaceAll
  • L13 · fmtNum calls (conditional paths may differ): Number.isInteger, n.toLocaleString
  • L35 · faviconHref calls (conditional paths may differ): encodeURIComponent
  • L43 · renderPage calls (conditional paths may differ): m.spikes.map, join, m.dates.map, escapeHtml, d.slice, sort, m.table.keys, metrics.map, m.table.get, fmtNum, byDate.get, spikeKeys.has, formatRatio, faviconHref
src/spike.ts ↗
  • L48 · parseThresholds calls (conditional paths may differ): json.trim, JSON.parse, Array.isArray, Object.entries, Number.isFinite
  • L64 · median calls (conditional paths may differ): sort, Math.floor
  • L86 · spike calls (conditional paths may differ): sort, Object.keys, median, out.push
  • L106 · formatRatio calls (conditional paths may differ): Number.isFinite, ratio.toFixed
src/store.ts ↗
  • L12 · upsertRows calls (conditional paths may differ): db.prepare, db.batch, rows.map, stmt.bind
  • L32 · loadTotals calls (conditional paths may differ): all, bind, db.prepare
  • L50 · buildSeries calls (conditional paths may differ): hist.push
  • L65 · pivot calls (conditional paths may differ): m.get, byDate.set, m.set
  • L76 · filterUnnotified calls (conditional paths may differ): all, bind, db.prepare, res.results.map, spikes.filter, seen.has
  • L90 · markNotified calls (conditional paths may differ): db.prepare, db.batch, spikes.map, splitKey, stmt.bind
  • L108 · splitKey calls (conditional paths may differ): key.indexOf, key.slice
Build and deployment pipeline · 1 GitHub Actions workflow

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

ci · .github/workflows/ci.yml ↗

Triggers: push, pull_request

test · no job dependencies declared

  1. actions/checkout@v4actions/checkout@v4
  2. pnpm/action-setup@v4pnpm/action-setup@v4
  3. actions/setup-node@v4actions/setup-node@v4
  4. Shell commandpnpm install --frozen-lockfile
  5. Shell commandpnpm lint
  6. Shell commandpnpm typecheck
  7. Shell commandpnpm test
  8. Shell commandpnpm exec wrangler deploy --dry-run --outdir /tmp/ugc-dry
package.json ↗
  • deploy: wrangler d1 migrations apply USAGE --remote && wrangler deploy

Full upstream document by @HowardZlh · README.md · snapshot f9809bb

usage-guard-collector

Self-hosted Cloudflare usage spike alerts with a one-click deploy: a read-only Worker that watches D1 rows read, Durable Objects requests, KV, R2 and Queues, and pings Discord or Slack when today is 10x last week.

It pulls daily counts from the GraphQL Analytics API into your own D1 every 6 hours. One HTML page. MIT.

It never calls a Cloudflare write API. It cannot stop a Worker, delete a database or change a setting. The token you give it is Account Analytics Read and nothing else.

Deploy to Cloudflare

The collector's GET / page: a Spikes list flagging d1.rows_read at 318.2x its 7-day median, above a table of daily totals per metric with that row highlighted

That's the whole UI. Sample data, not a real account.

Why watch Cloudflare usage instead of waiting for Budget Alerts?

Because Budget Alerts fire on the invoice total, and by then the meter has been running for days. Cloudflare's own docs call Budget alerts "informational only. They do not pause or cap usage."

I keep a public list of this year's Cloudflare bill spikes: Why Cloudflare bills spike: 8 D1 and Durable Objects cases from 2026. Seven are D1 rows read, one is a Durable Objects alarm loop. Amounts run from $176 to about $34,895.

If you'd rather see a number before deploying anything, the Workers pricing calculator prices a month of usage across D1, Durable Objects, KV, R2 and Queues. No login, and the result URL is shareable.

The shape is the same every time. One meter ran 10,000x above normal, and the first notification was the invoice or a quota email. One thread was 1.476 trillion rows read; another was a $5 subscription that grew to about $4,115 in a month.

The GraphQL Analytics API already has every number, per day, per resource. What was missing was a thing that reads them on a schedule, remembers last week, and says "this one is 500x yesterday" somewhere I will see it. This repo is that thing, and nothing more.

What it collects: Workers, D1, Durable Objects, KV, R2, Queues

Each row is one resource's metric for one UTC day. Dataset and field names were checked against the live schema by introspection on 2026-09-15 (src/graphql.ts).

Product GraphQL dataset Metrics stored Resource dimension
Workers workersInvocationsAdaptive requests, errors, cpu_ms scriptName
D1 d1AnalyticsAdaptiveGroups rows_read, rows_written, read_queries, write_queries databaseId
Durable Objects durableObjectsInvocationsAdaptiveGroups requests scriptName
KV kvOperationsAdaptiveGroups read, write, delete, list namespaceId
R2 r2OperationsAdaptiveGroups class_a, class_b (per the R2 pricing page lists) bucketName
Queues queueMessageOperationsAdaptiveGroups operations (billable) queueId

These are analytics counts, not the invoice. Cloudflare's docs say the analytics datasets include traffic that billing excludes, so expect the numbers to sit a little above what you are charged for.

How a spike is detected: 10x the 7-day median plus a floor

spike(today, last7) in src/spike.ts is a pure function. A metric is flagged when today's account-wide total is at least 10x the median of the previous 7 days and also above an absolute floor for that metric.

The floor stops "3 requests vs 0 yesterday" from paging you. Defaults are 1% of the monthly included quota on Workers Paid (2026-09-15 pricing pages): d1.rows_read 250,000,000, do.requests 10,000, r2.class_a 10,000.

Override any floor with the THRESHOLDS_JSON var in wrangler.jsonc:

"vars": { "THRESHOLDS_JSON": "{\"d1.rows_read\": 5000000, \"kv.read\": 50000}" }

The first day after deploy flags nothing; there is no baseline yet. "Today" is a partial UTC day, so a spike shows up as soon as the running total crosses the bar, not at midnight.

One-click deploy to Cloudflare: one form, two secrets

The button above clones this repo into your GitHub or GitLab account, creates the D1 database, runs the migration and deploys the Worker. The form asks for two secrets: CF_ACCOUNT_ID (the 32-hex id in your dashboard URL) and CF_API_TOKEN (see "The API token" below). Everything else has a default.

Webhooks are not on that form. Add one afterwards with wrangler secret put DISCORD_WEBHOOK_URL or from the Worker's Settings page.

If your account already has a D1 named usage-guard, the form pre-selects it because the name matches. Pick "create new" unless you mean to share that table; I found this out by binding a test deploy to my live one.

Deploy to your own account in six commands

You need wrangler logged in to the account you want to watch (npx wrangler login), Node 22.5 or newer, and pnpm.

git clone https://github.com/HowardZlh/usage-guard-collector && cd usage-guard-collector
pnpm install
pnpm exec wrangler d1 create usage-guard          # paste the database_id it prints into wrangler.jsonc
pnpm exec wrangler secret put CF_ACCOUNT_ID       # the 32-hex id in your dashboard URL
pnpm exec wrangler secret put CF_API_TOKEN        # see "The API token" below
pnpm run deploy                                   # applies the migration, then wrangler deploy

Optional, after deploy:

pnpm exec wrangler secret put DISCORD_WEBHOOK_URL   # or SLACK_WEBHOOK_URL, or both

The Cron runs at 00:00, 06:00, 12:00 and 18:00 UTC. To not wait, open the Worker in the dashboard and use the Cron trigger's "Run now". Or run it locally:

cp .dev.vars.example .dev.vars   # fill CF_ACCOUNT_ID / CF_API_TOKEN
pnpm exec wrangler d1 migrations apply USAGE --local
pnpm dev                          # then: curl "http://127.0.0.1:8787/__scheduled?cron=0+*/6+*+*+*"

The API token: Account Analytics Read and nothing else

Create a token that can read analytics and nothing else. In the dashboard:

  1. Profile (top right) → API Tokens → Create Token → Create Custom Token.
  2. Name it, e.g. usage-guard-collector.
  3. Permissions: one row, Account → Account Analytics → Read. No Zone row, no Workers / D1 / KV / R2 Edit rows.
  4. Account Resources: Include → the one account you are watching.
  5. Continue to summary → Create Token. Copy it once; it is not shown again.

The Worker reads it as env.CF_API_TOKEN and never logs it. The scheduled log line is a JSON object with counts and, if something failed, the HTTP status or GraphQL error message. No headers, no token.

The status page, and how to put it behind Cloudflare Access

GET / renders the last 7 days as one table (metric x day) plus the current spike list. Plain HTML string, no framework, no external scripts, noindex. Anything else is 404.

It has no login. If you would rather not have your usage table on a public URL, put the Worker behind Cloudflare Access: Zero Trust → Access → Applications → Add → Self-hosted, domain = your Worker's hostname, policy = your email. The Cron is unaffected by Access.

Does it fit in the Workers Free plan?

Yes. One Cron trigger (Free allows 5), 4 invocations a day, 2 GraphQL requests per run, a few dozen D1 rows written per run and one read per page view.

All of that is a rounding error inside the Free tier. The collector can watch a Paid account from a Free one.

What it does not do

No write operations of any kind (no kill switch). One account per deploy. No email. No auth on the page. No billing data.

If one of those is what you need, the "Hosted version" line below is the honest answer.

Tests: zero network, real migration

pnpm test        # vitest, coverage gate: lines >= 85%, branches >= 80% (currently 100 / 100)
pnpm typecheck
pnpm lint        # biome

Every fetch is stubbed (vi.stubGlobal) across success / empty / non-2xx. D1 runs on node:sqlite with the real migration.

Hosted version (waitlist)

Usage Guard is the half of this you might not want to run yourself: multiple accounts, e-mail, a stop switch. It is a waitlist page today, not a product; the form asks one question. guard.guushu.com

Questions and support

Questions about a deploy, a dataset that came back empty, or a spike that looks wrong: open a thread in GitHub Discussions. Threads there are indexed, so the next person with the same error can find the answer.

For anything that does not fit a thread, the Guush Studio Discord is here: discord.gg/kYTzwkmGW. It is small and mostly a changelog right now.

License

MIT. See LICENSE.

Frequently asked about Usage Guard Collector

What is Usage Guard Collector?+

Usage Guard Collector is a self-hosted Cloudflare Budget Alerts alternative built on the Cloudflare developer platform. Watch Cloudflare usage spikes with a small Worker, D1 dashboard and optional Slack or Discord alerts.

What does Usage Guard Collector replace?+

Usage Guard Collector is listed as an alternative to Cloudflare Budget Alerts. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does Usage Guard Collector use?+

Usage Guard Collector is built on D1, Workers.

How much does Usage Guard Collector cost to run?+

A small monitored account and modest dashboard traffic can fit Workers Free and D1 Free allowances. Analytics access, dataset availability, database growth and CPU use must be checked; no zero-cost installation or bill was measured. One Worker Cron runs every six hours, for four scheduled runs and eight GraphQL requests per day, plus actual dashboard requests. Keep account-wide Worker traffic and CPU within the plan limits. Use your own D1 database. Free D1 allowances are account-wide: 5 million rows read/day, 100,000 rows written/day and 5 GB total storage. Indexes, repeated upserts and historical scans consume capacity. A few monitored resources and limited page views are the qualifying workload. The code has no retention pruning or GraphQL pagination; large or long-lived accounts need a separate capacity plan. The monitored account must expose the six requested analytics datasets and fields to its read token. GraphQL availability and limits vary by account and plan; this was not verified with a live account. workers.dev avoids a required domain purchase. Slack/Discord integrations and optional access protection have separate setup and service conditions. No required email, AI, R2, KV, Queue or Durable Object application service is provisioned. Check current Cloudflare pricing before deploying.

Is Usage Guard Collector open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/HowardZlh/usage-guard-collector/f9809bb41ba54beaa668c5eadb3d33c3875b99f0/LICENSE. Source code and contributor credit are available at https://github.com/HowardZlh/usage-guard-collector.

Community rating

No ratings yet. Tried this project? Share your experience.

One rating per verified account. You can change or remove yours. Accounts are email verified; use of the software is self-reported.

Discuss your experience ↓
Sign in to rate

Discussion · 0

sign in to comment →
No comments yet — be the first.