Copper

How I fit 1,000 sites' analytics into Neon's free tier

October 4, 2026

I run a dozen small sites and wanted one page that shows traffic for all of them. I also wanted it to cost nothing, for me and for anyone else who signs up. This is how Copper Analytics does that.

The two limits that matter

Neon's free plan gives a project 1 GB of storage and 100 compute-unit hours a month. Storage is the obvious limit. Compute is the one that bites: any query wakes the database for at least five minutes, so a dashboard that queries on every pageview keeps it awake all month and runs out.

LimitFree allowanceTarget
Storage1 GBUnder 500 MB at 1,000 projects
Compute100 CU-hours a monthAbout 40
Cloudflare requests100,000 a dayOne per pageview

The compute target comes from simple arithmetic at a fixed 0.25 CU. An hourly write wakes the database 24 times a day for about five minutes each: two hours a day, about 15 CU-hours a month. Dashboard use adds the rest.

Pageviews stop at the edge

A Cloudflare Worker receives each pageview. It checks the site key and the origin, drops bots, and reduces the request to about 100 bytes: a path, a referrer domain, a country, a device type. The IP address and user agent go into a daily salted hash and are then discarded.

The event goes to one of 16 Durable Objects, chosen by hashing the site key. Sixteen shards, not one object per project, because the free plan counts Durable Object requests and rows written. Each shard keeps the current hour's counters in memory and writes one SQLite row per 100 events or five seconds, so a restart loses seconds of data and replays the rest.

One write an hour

At five past each hour a cron asks every shard for its finished hours and writes them to Postgres. Each hour from each shard carries an id. The first statement of the transaction inserts that id into a log table; if the row already exists, the hour is skipped. The shard deletes its copy only after the commit. A failed flush is retried the next hour with the same id, so nothing is lost and nothing is counted twice.

Rollups, not rows

Postgres holds three kinds of data:

  • Four counters per project per hour, kept for 14 days, and per day, kept for good.
  • One JSON document per project per day with the top 50 pages, referrers, countries, devices, browsers, operating systems and UTM sources, plus an (other) bucket. Kept for 90 days.
  • The same document per month with the top 100, kept for 25 months.

Unique visitors are the awkward part. The hash changes at midnight UTC, so a visitor cannot be followed across days. The shard remembers who it has seen today and reports "new today" counts each hour, which add up to the day's unique visitors. Over several days the dashboard shows the sum of daily uniques and says so.

A dashboard that lets the database sleep

History only changes at the hourly flush, so the dashboard caches each report and the Worker expires the cache after it writes. The current hour and the live visitor count come straight from the shards. A repeat visit to the overview makes no database query at all.

What the load test measured

The simulation runs 1,000 projects for 30 days: 4.27 million pageviews, about 142,000 a day, with a long tail from 6,000 visits a day down to a handful. It ran against PGlite, which is Postgres compiled to WebAssembly, so there is no network in the timings.

MeasureLimitResult
Database size, with no vacuum at all500 MB248 MB
Database size, live rows only71 MB
Slowest hourly flush, all 16 shards10 s0.92 s

Two honest caveats. Thirty days is not the steady state: daily documents are kept for 90, so their share roughly triples before retention levels it off. And the 248 MB figure is a worst case that real Postgres does not reach, because autovacuum reuses the space that rewriting a document leaves behind.

The real ceiling

Project count is not the limit. Traffic is: Cloudflare's free plan allows about 100,000 requests a day, summed over every site. Past that, the $5 paid plan raises it to millions and nothing in the design changes.

The code is MIT licensed at github.com/masaok/copper-analytics, and docker compose up starts your own copy.