GoatCounter: web analytics that treats the pageview as the whole unit of truth
Easy web analytics. No tracking of personal data.
At a glance
- What is it?
- A single Go binary that counts visits without cookies, unique identifiers or a consent banner. It does less than Google Analytics on purpose, and the README is unusually honest about the trade.
- Who is it for?
- GoatCounter earns its place on a personal site, a small blog or an internal tool where the question is simply how many people came and from where. It gives up the funnel, the user-level session replay, the attribution modelling and the tag manager that larger analytics products are built around, and no configuration brings those back.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three ways to get pageviews in, and only one needs a script
The integration is a single script tag, which is the whole pitch:
<script data-goatcounter="https://yoursite.goatcounter.com/count"
async src="//gc.zgo.at/count.js"></script>The cost is stated in the features list as just about 3.5K of extra data. If you cannot ship a script, there is a JavaScript-free tracking pixel option, an HTTP and REST API for firing events from your own backend middleware, and a log file importer that handles nginx, Apache, Caddy and CloudFront through `goatcounter help import`. That last one is the most interesting of the four, and it works against the hosted service as well as a self hosted instance.
So the design decision is not really which tracker to use, it is where the counting happens. A script in the page measures what a browser executed, which means it can be blocked, and it can be slowed by the very request it makes. Counting server side or importing logs means the number is derived from traffic the server already saw.
The README also notes that the JavaScript integration is a good option for most people, which is a fair piece of self-awareness from a project that supports three other options.
Unique visits without a cookie, and what that costs
The privacy claim is specific rather than a slogan. The README says GoatCounter does not track users with unique identifiers, identifies unique visits without cookies using a non-identifiable hash, and does not need a GDPR notice. There are separate documentation pages for the privacy policy and for GDPR consent notices, and a help page explaining the session hashing technique.
That last piece is the mechanism worth understanding, because it determines what the numbers mean. A session identifier derived from a non-identifiable hash and a time window is enough to collapse repeated hits on the same page into one visit. It is not enough to follow a reader across pages or across days, and it is not reversible back to a person. The cost is exact: you can count visits and you can attribute a referrer, and you cannot build a profile.
What survives the hashing is aggregate context. The README lists browser information, location and screen size, plus referring sites and campaigns. There is a `campaign.go` in the repository root and a `refspam.go`, which suggests referrer handling is treated as a real problem rather than a display concern.
The 2.5.0 release notes show the same philosophy applied to the raw data. The `User-Agent` header is no longer stored at all, only the browser and system parsed out of it, with the reasoning that mobile browser User-Agents are ridiculously unique and the raw header was only ever kept in case detection failed.
Serving it yourself with SQLite or PostgreSQL
The starting command is short:
% goatcounter serveThat starts a server on `*:8080` and uses a SQLite database at `./goatcounter-data/db.sqlite3`, created if it does not exist. No configuration file, no external service. The first site is created through a wizard on `http://localhost:8080` or from the command line:
% goatcounter db create site -vhost=stats.example.com [email protected]That command prompts for a password, accepts `-password` on the command line, and needs `-db` if you are not using the default.
TLS and automatic ACME certificate generation are built in, so a production instance is one flag away:
% goatcounter serve -listen=:443 -tls=tls,rdr,acmeFor PostgreSQL the `-db` flag follows the `psql` connection string format, and the README points out you can use the `PG*` environment variables instead:
% goatcounter serve -db 'postgresql+dbname=goatcounter'The performance advice in the README is the most useful sentence in the documentation for anyone sizing a deployment. SQLite should work for most smaller sites and PostgreSQL is better for larger ones, but the bottleneck is not the number of pageviews. Ten million pageviews spread over five pages is fast even on SQLite; ten million spread over a million different pages is much slower. Cardinality of your URL space is what costs you, not traffic.
The Docker path, and why the volume is not optional
There is a `Dockerfile` and a `compose.yaml` in the repository root, and the image is on DockerHub as `arp242/goatcounter`. The container form of the run command:
% docker run \
-p 8080:8080 \
-v goatcounter-data:/home/goatcounter/goatcounter-data \
arp242/goatcounterThe README recommends a named volume specifically, and explains why: that volume is where the SQLite database and the ACME certificates live, and anonymous volumes are easy to delete by accident. Configuration through the environment uses `GOATCOUNTER_` prefixed variables, so enabling TLS and automatic certificates in a container looks like this:
% docker run \
-p 80:80 \
-p 443:443 \
-v goatcounter-data:/home/goatcounter/goatcounter-data \
-e GOATCOUNTER_LISTEN=:443 \
-e GOATCOUNTER_TLS=tls,rdr,acme \
arp242/goatcounterThe `Dockerfile` itself is worth reading for the operational picture. It builds with `CGO_ENABLED=1` and a `sqlite_omit_load_extension` build tag, produces a static binary with `-s -w -extldflags=-static`, runs the final image from Alpine as a dedicated `goatcounter` user, exposes 80, 443 and 8080, and defaults the command to `serve -automigrate`. That last default is what applies schema migrations on start.
The `compose.yaml` is not a demo. Postgres runs as `postgres:18-alpine` with hand-tuned settings including `shared_preload_libraries=pg_stat_statements`, `random_page_cost=1.5` with a comment noting most people use an SSD, `effective_io_concurrency=256`, and `jit=off` marked as almost always slower.
Upgrades that move defaults and rewrite tables
This is where a small project tells you more than its feature list does. v2.6.0, published on 2025-06-08, changed the defaults for `-listen` and `-tls` from `-listen=:443 -tls=tls,rdr,acme` to `-listen=:8080 -tls=none`. The stated reason is that many people run nginx or Caddy in front and do not use the built in TLS, and the new defaults are easier to get started with. The database default path moved from `./db/goatcounter.sqlite3` to `./goatcounter-data/db.sqlite3`, with the old file still used if it exists, and the ACME secrets directory moved under the same new prefix.
For most people that is a change from serving publicly on 443 with automatic certificates to serving plain HTTP on 8080. It is a more sensible default and a rude surprise on upgrade if you never read the notes.
v2.5.0, published on 2023-12-14, is the heavier one: quite a few tables rewritten to a more efficient format, which takes a few minutes on a small to medium instance and possibly a few hours on a very large one, and needs enough free disk space to rewrite the `hits` table. That release also required Go 1.21 to compile. You can inspect a migration before running it with `goatcounter db migrate -show`.
v2.7.0, published on 2025-12-15, is incremental and has good small details in it. The `-geodb` flag now accepts a MaxMind account id and licence key to download updates automatically. `secure` and `sameSite` cookie attributes are detected from the client connection rather than from the `-tls` flag, with the caveat that this needs a proxy setting `Scheme: https` or `X-Forwarded-Proto: https`. WebSocket support is detected automatically and `-websocket` is now a no-op. Bot pageviews go into a new `bots` table retained for 30 days, which is never queried but is there for debugging.
What you give up against Google Analytics and Matomo
The README positions GoatCounter explicitly as an alternative to Google Analytics or Matomo, and it is worth being honest about where that holds and where it does not. What it replaces: pageview counts, unique visitors, referrers, campaigns, browser and location and screen size, in an interface the project says works well with screen readers, with accessibility named as a high priority feature rather than an afterthought.
What it does not replace is any part of the data layer. There are no user attributes, no cohorts, no funnels, no conversion goals, no A/B assignment, no session recording, no tag manager, and no third party integrations, because the repository has no such thing. The README's own line about being confused by the myriad of options and flexibility you do not need is an accurate description of the trade from both directions.
The two features that often decide this comparison are worth naming separately. First, export: the README commits you can always export all data, and there are `exportcsv.go` and `exportjson.go` at the repository root with corresponding test files, so export is a maintained code path rather than a promise. Second, the code itself, which you can read. The tree shows a flat Go layout with `hit.go`, `hit_stats.go`, `memstore.go`, `import.go` and `refspam.go` alongside directories for `cron/`, `db/`, `handlers/`, `i18n/` and `pkg/`, so auditing what gets stored is a matter of reading the source rather than a policy page.
Editorial conclusion
GoatCounter earns its place on a personal site, a small blog or an internal tool where the question is simply how many people came and from where. It gives up the funnel, the user-level session replay, the attribution modelling and the tag manager that larger analytics products are built around, and no configuration brings those back. The honest limitations are elsewhere too: SQLite is fine until your URLs are highly varied, and the default bind address changed between major versions, so an upgrade can move a listener. Start with `goatcounter serve` and the first site wizard on port 8080, and read the v2.6.0 default changes before running it anywhere but localhost. The last push was on 2026-09-20, with v2.7.0 released on 2025-12-15.
Frequently asked questions
Is GoatCounter safe?
On privacy, yes by design: the README says it does not track users with unique identifiers, identifies unique visits without cookies using a non-identifiable hash, and does not need a GDPR notice. Since v2.5.0 it no longer stores the `User-Agent` header at all, keeping only the parsed browser and system. If you self host, TLS and automatic ACME certificate generation are built in and configurable from the command line or `GOATCOUNTER_` environment variables.
How do I self host GoatCounter?
Run `goatcounter serve`, which starts on port 8080 with a SQLite database at `./goatcounter-data/db.sqlite3`, then create the first site with `goatcounter db create site` or the wizard on `http://localhost:8080`. Use `-db` with a `psql` style connection string for PostgreSQL, or `-listen=:443 -tls=tls,rdr,acme` for built in TLS and certificate generation.
What is the best free website Analytics tool?
GoatCounter's answer to that question is its own design: about 3.5K of extra data, no cookies, no unique identifiers, and pageview, referrer, campaign, browser, location and screen size statistics. It explicitly positions itself as an alternative to Google Analytics or Matomo, and there is also a free hosted service at goatcounter.com alongside self hosting. The honest caveat is that none of the funnel, cohort or tag manager features of those tools are present.
When should I use PostgreSQL instead of SQLite for GoatCounter?
When your URLs are highly varied rather than when you have high traffic. The README's example is that ten million pageviews spread over five pages is quite fast even on SQLite, while the same volume spread over a million different pages is much slower. The bottleneck is the spread across distinct pages, so a site with many unique paths is the case that needs PostgreSQL.
Can GoatCounter count pageviews without JavaScript?
Yes. The README lists four routes: the JavaScript script tag, a JavaScript-free image based tracking pixel, an HTTP and REST API for use from backend middleware, and a log file importer for nginx, Apache, Caddy, CloudFront or any other HTTP server or proxy, documented under `goatcounter help import`. The importer works with the hosted service as well as a self hosted instance.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/arp242-goatcounter)