Counterscale
Collect page views and explore simple website analytics on your Cloudflare account.
Counterscale is a self-hosted Fathom Analytics/Google Analytics alternative built on Cloudflare (Analytics Engine, R2, 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
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The documented Counterscale deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.
Hosting requirements
- Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
- Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
- Keep Analytics Engine within 100,000 data points/day and 10,000 queries/day; one event can write to multiple datasets.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: 71107f4. Hosting eligibility reflects the deployment documentation and listed assumptions.
- google-analytics ↗
Counterscale is a simple web analytics tracker and dashboard that you self-host on Cloudflare.
- plausible ↗
Counterscale is a simple web analytics tracker and dashboard that you self-host on Cloudflare.
- fathom ↗
Counterscale is a simple web analytics tracker and dashboard that you self-host on Cloudflare.
- workers ↗
{ "main": "./workers/app.ts", "name": "counterscale", "compatibility_flags": ["nodejs_compat_v2"], "compatibility_date": "2024-12-13", "assets": { "binding": "ASSETS", "directory": "./build/client" }, "analytics_engine_datasets": [ { "binding": "WEB_COUNTER_AE", "dataset": "metricsDat
- r2 ↗
"bucket_name": "counterscale-daily-rollups", "preview_bucket_name": "counterscale-daily-rollups-dev", "binding": "DAILY_ROLLUPS" } ], "triggers": { "crons": [ "0 2 * * *" ] } }
- analytics-engine ↗
ts": { "binding": "ASSETS", "directory": "./build/client" }, "analytics_engine_datasets": [ { "binding": "WEB_COUNTER_AE", "dataset": "metricsDataset" } ], "r2_buckets": [ { "bucket_name": "counterscale-daily-rollups", "preview_bucket_name": "counterscale-daily-rollups-dev", "binding": "DAILY_ROLLUPS" } ], "triggers": { "crons": [ "0 2 * * *" ] } }
- free-tier-eligible ↗
{ "main": "./workers/app.ts", "name": "counterscale", "compatibility_flags": ["nodejs_compat_v2"], "compatibility_date": "2024-12-13", "assets": { "binding": "ASSETS", "directory": "./build/client" }, "analytics_engine_datasets": [ { "binding": "WEB_COUNTER_AE", "dataset": "metricsDat
- free-tier-eligible ↗
"bucket_name": "counterscale-daily-rollups", "preview_bucket_name": "counterscale-daily-rollups-dev", "binding": "DAILY_ROLLUPS" } ], "triggers": { "crons": [ "0 2 * * *" ] } }
- free-tier-eligible ↗
ts": { "binding": "ASSETS", "directory": "./build/client" }, "analytics_engine_datasets": [ { "binding": "WEB_COUNTER_AE", "dataset": "metricsDataset" } ], "r2_buckets": [ { "bucket_name": "counterscale-daily-rollups", "preview_bucket_name": "counterscale-daily-rollups-dev", "binding": "DAILY_ROLLUPS" } ], "triggers": { "crons": [ "0 2 * * *" ] } }
- free-tier-eligible ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- free-tier-eligible ↗
infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp
- free-tier-eligible ↗
ion) | 1 million included per month (+$1.00 per additional million) | | **Workers Free** | 100,000 included per day | 10,000 included per day | Pricing availability Currently, you will not be billed for your use of Workers Analytics Engine. Pricing information here is shared in advance, so that you can estimate what your costs will be once Cloudflare starts billing for usage in the coming months. If you are an Enterprise customer, contact your account team for information about Workers Analytics Engine pricing and billing. ### Data points written Every time you call [`writeDataPoint()`](https://developers.cloudflare.com/analytics/analytics-engine/get-started/#2-write-data-points-from-your-worker) in a Work
- MIT ↗
Copyright 2025 Ben Vinegar 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 EV
- architecture ↗
{ "main": "./workers/app.ts", "name": "counterscale", "compatibility_flags": ["nodejs_compat_v2"], "compatibility_date": "2024-12-13", "assets": { "binding": "ASSETS", "directory": "./build/client" }, "analytics_engine_datasets": [ { "binding": "WEB_COUNTER_AE", "dataset": "metricsDat
- architecture ↗
"bucket_name": "counterscale-daily-rollups", "preview_bucket_name": "counterscale-daily-rollups-dev", "binding": "DAILY_ROLLUPS" } ], "triggers": { "crons": [ "0 2 * * *" ] } }
- architecture ↗
ts": { "binding": "ASSETS", "directory": "./build/client" }, "analytics_engine_datasets": [ { "binding": "WEB_COUNTER_AE", "dataset": "metricsDataset" } ], "r2_buckets": [ { "bucket_name": "counterscale-daily-rollups", "preview_bucket_name": "counterscale-daily-rollups-dev", "binding": "DAILY_ROLLUPS" } ], "triggers": { "crons": [ "0 2 * * *" ] } }
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Page-view collection, referrer and audience breakdowns and a simple traffic dashboard; advertising attribution and complete enterprise analytics parity are excluded.
See supporting source ↗Page-view collection, referrer and audience breakdowns and a simple traffic dashboard; advertising attribution and complete enterprise analytics parity are excluded.
See supporting source ↗Page-view collection, referrer and audience breakdowns and a simple traffic dashboard; advertising attribution and complete enterprise analytics parity are excluded.
See supporting source ↗How it works
The shape of Counterscale 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 ↗Configuration and workflow sources
Reviewed commit 71107f4fe840. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 1 files
Cloudflare Workers · compatibility 2024-12-13
counterscale · default
Entrypoint: ./workers/app.ts
Static assets: ./build/client
Cron triggers (UTC): 0 2 * * *
DAILY_ROLLUPS→ R2WEB_COUNTER_AE→ Analytics EngineASSETS→ Static assets
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.
- L20 · scheduled handler exported · references CF_STORAGE_ENABLED, CF_ACCOUNT_ID, CF_BEARER_TOKEN, DAILY_ROLLUPS · calls ctx.waitUntil, extractAsArrow, console.error
- L41 · fetch handler exported · calls getLoadContext, ctx.waitUntil.bind, ctx.passThroughOnException.bind, requestHandler, console.log
Environment references: env.CF_STORAGE_ENABLED · env.CF_ACCOUNT_ID · env.CF_BEARER_TOKEN · env.DAILY_ROLLUPS
Environment references: context.cloudflare.env.CF_ACCOUNT_ID · context.cloudflare.env.CF_BEARER_TOKEN
- L18 · buildValidityBitmap calls (conditional paths may differ): Math.ceil
- L45 · buildUtf8Data calls (conditional paths may differ): encoder.encode, String, data.set, buildValidityBitmap, makeData
- L75 · buildFloat64Data calls (conditional paths may differ): buildValidityBitmap, makeData
- L95 · recordsToTable calls (conditional paths may differ): Object.keys, records.map, buildFloat64Data, buildUtf8Data, values.map, String
- L122 · extractAsArrow calls (conditional paths may differ): subtract, dayjs, toDate, yesterday.startOf, yesterday.endOf, filter, Object.keys, api.getAllCountsByAllColumnsForAllSites, data.forEach, columns.forEach, records.push, recordsToTable, tableToIPC, yesterday.format, bucket.put, console.log
Environment references: process.env.CLOUDFLARE_ACCOUNT_ID · process.env.CLOUDFLARE_API_TOKEN
- L31 · accumulateCountsFromRowResult calls (conditional paths may differ): Number
- L49 · intervalToSql calls (conditional paths may differ): format, utc, startOf, tz, dayjs, subtract, interval.split
- L91 · generateEmptyRowsOverInterval calls (conditional paths may differ): startDateTime.getTime, endDateTime.getTime, format, utc, dayjs, toDate, startOf, tz, add
- L134 · filtersToSql calls (conditional paths may differ): supportedFilters.forEach, Object.hasOwnProperty.call
Build and deployment pipeline · 2 GitHub Actions workflows
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.
Triggers: workflow_run
deploy · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - actions/setup-node@v4
actions/setup-node@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - Shell command
pnpm install - Shell command
pnpm turbo build - cloudflare/wrangler-action@v3
cloudflare/wrangler-action@v3Wrangler command: deploy --var VERSION:${{ github.sha }}
Triggers: push, pull_request
test · no job dependencies declared
- actions/checkout@v4
actions/checkout@v4 - actions/setup-node@v4
actions/setup-node@v4 - pnpm/action-setup@v4
pnpm/action-setup@v4 - Shell command
pnpm install - Shell command
cd packages/tracker && pnpm playwright install --with-deps - Shell command
pnpm turbo build lint typecheck test-ci --concurrency=1 - Upload coverage reports to Codecov
codecov/codecov-action@v3
build: turbo run build
build: tsc --build
build: react-router builddeploy: wrangler deploy --var VERSION:`git rev-parse HEAD`
build: vite build -c vite.loader.config.ts && vite build -c vite.module.config.ts && vite build -c vite.server.config.ts
Repository README
View original on GitHub ↗Full upstream document by @benvinegar · README.md · snapshot 71107f4
Counterscale

Counterscale is a simple web analytics tracker and dashboard that you self-host on Cloudflare.
It's designed to be easy to deploy and maintain, and should cost you near-zero to operate – even at high levels of traffic (Cloudflare's free tier could hypothetically support up to 100k hits/day).
Counterscale is sponsored by Modem, your dev-team's auto-triage PM.
License
Counterscale is free, open source software made available under the MIT license. See: LICENSE.
Limitations
Counterscale is powered primarily by Cloudflare Workers and Workers Analytics Engine. As of February 2025, Workers Analytics Engine has maximum 90 days retention, which means Counterscale can only show the last 90 days of recorded data. We do, however, provide long term storage of your data in an R2 bucket using Apache Arrow files. This long term storage is enabled by default and can be disabled using the CLI.
Installation
Requirements
- macOS or Linux environment
- Node v20 or above
- An active Cloudflare account (either free or paid)
Cloudflare Preparation
If you don't have one already, create a Cloudflare account here and verify your email address.
- Go to your Cloudflare dashboard and, if you do not already have one, set up a Cloudflare Workers subdomain
- Enable Cloudflare Analytics Engine beta for your account. To enable, navigate to Storage & Databases > Analytics Engine and click the "Enable" button (screenshot). You can ignore and exit out of the "Create Dataset" menu that will pop up next.
- Note: If this is your first time using Workers, you have to create a Worker before you can enable the Analytics Engine. Navigate to Workers & Pages > Overview, click the "Create Worker" button (screenshot) to create a "Hello World" worker (it doesn't matter what you name this Worker as you can delete it later).
- Create a Cloudflare API token. This token needs
Account.Account Analyticspermissions at a minimum (screenshot).- WARNING: Keep this window open or copy your API token somewhere safe (e.g. a password manager), because if you close this window you will not be able to access this API token again and have to start over.
Deploy Counterscale
First, sign into Cloudflare and authorize the Cloudflare CLI (Wrangler) using:
npx wrangler login
Afterwards, run the Counterscale installer:
npx @counterscale/cli@latest install
Follow the prompts. You will be asked for the Cloudflare API token you created earlier. You'll also be asked if you want to protect your dashboard with a password:
- If you choose Yes (recommended for public deployments), you'll be prompted to create a password that will be required to access your analytics dashboard.
- If you choose No, your dashboard will be publicly accessible without authentication.
Once the script has finished, the server application should be deployed. Visit https://{subdomain-emitted-during-deploy}.workers.dev to verify.
NOTE: If this is your first time deploying Counterscale, it may take take a few minutes before the Worker subdomain becomes live.
Start Recording Web Traffic from Your Website(s)
You can load the tracking code using one of the following methods:
1. Script Loader (CDN)
When Counterscale is deployed, it makes tracker.js available at the URL you deployed to:
https://{subdomain-emitted-during-deploy}.workers.dev/tracker.js
To start reporting website traffic from your web property, copy/paste the following snippet into your website HTML:
<script
id="counterscale-script"
data-site-id="your-unique-site-id"
src="https://{subdomain-emitted-during-deploy}.workers.dev/tracker.js"
defer
></script>
2. Package/Module
The Counterscale tracker is published as an npm module:
npm install @counterscale/tracker
Initialize Counterscale with your site ID and the URL of your deployed reporting endpoint:
import * as Counterscale from "@counterscale/tracker";
Counterscale.init({
siteId: "your-unique-site-id",
reporterUrl: "https://{subdomain-emitted-during-deploy}.workers.dev/collect",
});
Available Methods
| Method | Parameters | Return Type | Description |
|---|---|---|---|
init(opts) |
ClientOpts |
void |
Initializes the Counterscale client with site configuration. Creates a global client instance if one doesn't exist. |
isInitialized() |
None | boolean |
Checks if the Counterscale client has been initialized. Returns true if client exists, false otherwise. |
getInitializedClient() |
None | Client | undefined |
Returns the initialized client instance or undefined if not initialized. |
trackPageview(opts?) |
TrackPageviewOpts? |
void |
Tracks a pageview event. Requires client to be initialized first. Automatically detects URL and referrer if not provided. |
cleanup() |
None | void |
Cleans up the client instance and removes event listeners. Sets global client to undefined. |
3. Server-Side Module
If you'd prefer to track analytics on the server, instead of running the tracker in the browser, use the /server module:
npm install @counterscale/tracker
import * as Counterscale from "@counterscale/tracker/server";
// Initialize the tracker
Counterscale.init({
siteId: "your-unique-site-id",
reporterUrl:
"https://{subdomain-emitted-during-deploy}.workers.dev/collect",
reportOnLocalhost: false, // optional, defaults to false
timeout: 2000, // optional, defaults to 1000ms
});
// Track a pageview
await Counterscale.trackPageview({
url: "https://example.com/page", // or relative: '/page'
hostname: "example.com", // required for relative URLs
referrer: "https://google.com",
utmSource: "social",
utmMedium: "twitter",
});
Server Module Methods
| Method | Parameters | Return Type | Description |
|---|---|---|---|
init(opts) |
ServerClientOpts |
void |
Initializes the server-side tracker. |
isInitialized() |
None | boolean |
Checks if the tracker has been initialized. |
getInitializedClient() |
None | ServerClient | undefined |
Returns the initialized server client instance. |
trackPageview(opts) |
TrackPageviewOpts |
Promise<void> |
Tracks a pageview event. Requires explicit URL and hostname parameters. |
cleanup() |
None | void |
Cleans up the server client instance. |
The server module is designed for backend applications and differs from the client-side version:
- No DOM-dependent features (auto-tracking, browser instrumentation)
- Uses fetch API instead of XMLHttpRequest
- Requires explicit URL and hostname parameters
- Fire-and-forget - tracking errors won't throw exceptions
Upgrading
For most releases, upgrading is as simple as re-running the CLI installer:
npx @counterscale/cli@latest install
# OR
# npx @counterscale/cli@VERSION install
You won't have to enter a new API key, and your data will carry forrward.
Counterscale uses semantic versioning. If upgrading to a major version (e.g. 2.x, 3.x, 4.x), there may be extra steps. Please consult the release notes.
Troubleshooting
If the website is not immediately available (e.g. "Secure Connection Failed"), it could be because Cloudflare has not yet activated your subdomain (yoursubdomain.workers.dev). This process can take a minute; you can check in on the progress by visiting the newly created worker in your Cloudflare dashboard (Workers & Pages → counterscale).
Advanced
Manually Track Pageviews
When you initialize the Counterscale tracker, set autoTrackPageviews to false. Then, you can manually call Counterscale.trackPageview() when you want to record a pageview.
import * as Counterscale from "@counterscale/tracker";
Counterscale.init({
siteId: "your-unique-site-id",
reporterUrl: "https://{subdomain-emitted-during-deploy}.workers.dev/collect",
autoTrackPageviews: false, // <- don't forget this
});
// ... when a pageview happens
Counterscale.trackPageview();
Custom Domains
The deployment URL can always be changed to go behind a custom domain you own. More here.
CLI Commands
Counterscale provides a command-line interface (CLI) to help you install, configure, and manage your deployment.
Available Commands
install
The main command for installing and deploying Counterscale to Cloudflare.
npx @counterscale/cli@latest install
Options:
--advanced- Enable advanced mode to customize worker name and analytics dataset--verbose- Show additional logging information
auth
Manage authentication settings for your Counterscale deployment.
npx @counterscale/cli@latest auth [subcommand]
Available subcommands:
enable- Enable authentication for your Counterscale deploymentdisable- Disable authentication for your Counterscale deploymentroll- Update/roll the authentication password
Examples:
Enable authentication:
npx @counterscale/cli@latest auth enable
Disable authentication:
npx @counterscale/cli@latest auth disable
Update/roll the password:
npx @counterscale/cli@latest auth roll
storage
Manage long term storage settings for your Counterscale deployment.
npx @counterscale/cli@latest storage [subcommand]
Available subcommands:
enable- Enable storage for your Counterscale deploymentdisable- Disable storage for your Counterscale deployment
Examples:
Enable storage:
npx @counterscale/cli@latest storage enable
Disable storage:
npx @counterscale/cli@latest storage disable
Development
See Contributing for information on how to get started.
Notes
Database
There is only one "database": the Cloudflare Analytics Engine dataset, which is communicated entirely over HTTP using Cloudflare's API.
Right now there is no local "test" database. This means in local development:
- Writes will no-op (no hits will be recorded)
- Reads will be read from the production Analytics Engine dataset (local development shows production data)
Sampling
Cloudflare Analytics Engine uses sampling to make high volume data ingestion/querying affordable at scale (this is similar to most other analytics tools, see Google Analytics on Sampling). You can find out more how sampling works with CF AE here.
Frequently asked about Counterscale
What is Counterscale?+
Counterscale is a self-hosted Fathom Analytics/Google Analytics alternative built on the Cloudflare developer platform. Collect page views and explore simple website analytics on your Cloudflare account.
What does Counterscale replace?+
Counterscale is listed as an alternative to Fathom Analytics, Google Analytics, Plausible. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does Counterscale use?+
Counterscale is built on Analytics Engine, R2, Workers.
How much does Counterscale cost to run?+
The documented Counterscale deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Keep Analytics Engine within 100,000 data points/day and 10,000 queries/day; one event can write to multiple datasets. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is Counterscale open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/benvinegar/counterscale/71107f4fe84053c3703df03ca66411d4144e6f2f/LICENSE. Source code and contributor credit are available at https://github.com/benvinegar/counterscale.



Discussion · 0
sign in to comment →