Meine Website ist statisch und liegt auf Cloudflare. Ich will wissen, wie viele Leute welche Seite lesen und wie viele den QR-Code auf dem Flyer gescannt haben, ohne Cookie-Banner und ohne meine Besucher an einen fremden Server zu schicken.
Für die erste Hälfte davon liefert Cloudflare schon eine kostenlose Antwort, und für viele Websites reicht sie. Dieser Leitfaden beginnt dort, zeigt, wo sie aufhört zu zählen, und baut dann die Alternative: einen Worker auf einem Pfad Ihrer eigenen Website, der Seitenaufrufe, Klicks auf Kurzlinks und QR-Scans in eine D1-Datenbank in Ihrem eigenen Konto schreibt. Jedes Cloudflare-Limit unten wurde am 11. Oktober 2026 mit der Dokumentation von Cloudflare abgeglichen.
Was Cloudflare kostenlos mitbringt
Web Analytics kostet nichts, und Cloudflare beschreibt es als “privacy-first analytics”, die “does not collect or use your visitors’ personal data”. Auf einem proxied Hostnamen schalten Sie es im Dashboard ein, und Cloudflare fügt sein Skript selbst in Ihr HTML ein. Die FAQ nennt die Bedingungen: Daten der letzten sechs Monate, und “We retain unsampled beacon data for the past 7 days, after this point data is aggregated down to around 10%.”
Wenn Ihre Seiten es laden, Sie mit einem Skript von einem anderen Host leben können und Sie nur Seiten interessieren, hören Sie hier auf. Es kostet nichts, und Sie konfigurieren nichts.
Wo Web Analytics aufhört zu zählen
Eine strikte Content Security Policy lehnt es ab. Das Skript liegt unter
static.cloudflareinsights.com/beacon.min.js. Die automatische Einrichtung schickt ihre
Berichte an /cdn-cgi/rum auf Ihrer eigenen Domain, connect-src 'self' genügt also. Das
Skript selbst kommt aber von einem fremden Host, und die FAQ verlangt, ihn in script-src
aufzunehmen. Dieser Blog ist der gemessene Fall. Seine Policy lautet
script-src 'self' 'unsafe-inline'. Am 11. Oktober 2026 trug eine Seite von 301.sh das
eingefügte Tag, mit integrity-Attribut und dem Token der Website, und Chrome blockierte es:
blockedURI: https://static.cloudflareinsights.com/beacon.min.js
violatedDirective: script-src-elem
disposition: enforce
Der Ressourceneintrag hat eine Übertragungsgröße von 0, und das Beacon-Objekt entsteht nie. Cloudflare fügt das Tag ein, ohne auf die Policy zu schauen. Das Dashboard bleibt leer, und außer einer Zeile in der Browserkonsole sagt Ihnen nichts, warum.
Blocklisten lehnen es namentlich ab. Die FAQ von Cloudflare hat einen Abschnitt mit der
Überschrift “The analytics beacon is blocked by ad-blockers” und antwortet darauf mit
“Cloudflare is aware”. Die Liste
EasyPrivacy, geprüft am
11. Oktober 2026, sperrt den Host, ||cloudflareinsights.com^$third-party, und enthält dazu
allgemeine Pfadregeln, die auf jeder Website greifen: /beacon.min.js, /cdn-cgi/rum? und
/cdn-cgi/rum|. Der Bericht an die eigene Domain hilft nicht, wenn der Pfad auf jeder
Cloudflare-Website derselbe ist.
Es zählt Seiten, keine Links. Ein QR-Code auf einem Flyer, ein Link im Newsletter oder in einer Bio führt nur dann zu einer Seite, wenn Sie eine daraus machen. Der Klick selbst und welcher Code gescannt wurde, sind kein Seitenaufruf.
Die Idee: ein Sammler auf einem Pfad Ihrer Website
Ein Worker kann auf einer Route sitzen, die nur einen Pfad Ihres Hostnamens abdeckt, etwa
example.com/assets-k3/*. Alles außerhalb dieses Pfades geht wie bisher an Ihren Origin,
ebenso alles darunter, was der Worker nicht selbst beantwortet, denn er reicht es mit
fetch(request) weiter. Die Seite schickt beim Laden eine kleine Anfrage an diesen Pfad. Für
den Browser, die CSP und die Blocklisten ist das ein POST an den eigenen Origin, auf einen
Pfad, der wie jeder andere aussieht.
Das funktioniert auch mit einer statischen Website. Die Pages-Dokumentation beschreibt einen Worker “on the same custom domain as your Pages project”, und der TDS-Leitfaden in diesem Blog beruht auf derselben Anordnung. Eine Reihenfolge zählt, laut den Known Issues von Pages: “It is currently not possible to add a custom domain with … a Worker already routed on that domain.” Erst die Custom Domain von Pages anlegen, dann die Route.
Die kleinste Fassung besteht aus einer Tabelle, einem Worker und einer Zeile in der Seite:
CREATE TABLE views (day TEXT, page TEXT, n INTEGER, PRIMARY KEY (day, page));
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === 'POST' && url.pathname === '/assets-k3/v') {
const day = new Date().toISOString().slice(0, 10);
const body = (await request.text()).slice(0, 200);
const page = body.startsWith('/') ? body : 'unknown';
await env.DB.prepare(
'INSERT INTO views (day, page, n) VALUES (?, ?, 1) ' +
'ON CONFLICT (day, page) DO UPDATE SET n = n + 1'
).bind(day, page).run();
return new Response(null, { status: 204, headers: { 'cache-control': 'no-store' } });
}
return fetch(request);
},
};
<script>navigator.sendBeacon('/assets-k3/v', location.pathname)</script>
Die Route ist example.com/assets-k3/* mit einem D1-Binding namens DB. Die Seite schickt
ihren eigenen Pfad im Body. Der Referer des Beacons würde ihn auch tragen, aber nur, solange
die Referrer-Policy der Website es erlaubt: no-referrer lässt den Header weg, origin
kürzt ihn auf den Host, und jeder Aufruf landete auf /. Ein Body, der kein Pfad ist, wird
als unknown gezählt. Wenn Sie Quellen auswerten wollen, schicken Sie document.referrer mit.
Das slice begrenzt nur, was in D1 landet: request.text() hat den ganzen Body zu diesem
Zeitpunkt schon gelesen. Ein Zähler, der offen im Internet steht, liest den Stream deshalb
selbst und bricht ihn ab einer Grenze ab, wie clx bei 2 KB.
Wählen Sie den Pfad so, wie Sie einen Bilderordner benennen würden. Namen wie stats, track
oder beacon sind genau das, wofür die allgemeinen Regeln oben geschrieben sind.
Menschen zählen ohne Cookie
Ein Aufrufzähler ist eine Summe. Ein Besucherzähler muss wissen, ob er jemanden schon gesehen
hat, und üblicherweise erfährt er das über ein Cookie. Ohne Cookie geht es über einen Hash aus
dem, was die Anfrage ohnehin mitbringt, gesalzen mit einem Zufallswert, der einen Tag lebt:
sha256(day salt | site | IP | user agent). Speichern Sie den Hash für den offenen Tag,
zählen Sie die verschiedenen Hashes, und wenn der Tag schließt, behalten Sie die Zahl und
löschen Hashes und Salt. Das Salt muss aus crypto.getRandomValues stammen und darf nie im Log landen.
Danach lässt sich nichts in der Datenbank einer Person zuordnen, auch nicht von Ihnen.
Gezählt werden so verschiedene Kombinationen aus Adresse und Browser pro Tag, das liegt nahe an Menschen, ist aber nicht dasselbe. Ein Handy, das vom Heimnetz ins Mobilfunknetz wechselt, zählt doppelt; zwei Kollegen hinter einer Büroadresse mit demselben Browser zählen einmal. Über eine Woche ist die Zahl die Summe der täglichen Unique-Besucher, wer am Montag und am Donnerstag liest, zählt also doppelt. Der Bericht sollte das in seiner Beschriftung sagen.
Kurzlinks und QR-Codes auf demselben Worker
Ein Kurzlink ist ein Redirect, und ein Redirect führt kein JavaScript aus, also sieht kein Seitenskript je den Klick. Klicks auf Weiterleitungen zählen erklärt, warum nur das zählen kann, was die Anfrage beantwortet. Wenn ein Worker ohnehin Aufrufe in D1 zählt, kann er auch die Links beantworten und die Klicks in dieselben Tabellen schreiben.
Die Links brauchen einen eigenen Hostnamen, etwa k7q.example.com, ohne Origin dahinter.
Workers Custom Domains
passen: “Cloudflare will create DNS records and issue necessary certificates on your behalf.”
Die Dokumentation nennt nur einen vorhandenen CNAME als Hindernis. Gemessen auf einem echten
Konto am 11. Oktober 2026 stoppt jeder vorhandene Eintrag den Vorgang: Die API antwortet mit
409 und Code 100117, “already has externally managed DNS records”. Ein Eintrag, den
jemand von Hand angelegt hat, wird also nie übernommen.
Drei Details machen die Zahlen verlässlich:
- mit
302undcache-control: no-storeantworten, damit jeder Klick den Worker erreicht statt eines gecachten Redirects im Browser; - eine Markierung in die Adresse im QR-Code setzen,
k7q.example.com/menu?q, sie als Quelleqrfesthalten und aus dem Redirect entfernen; - einen
GETzählen, einenHEADweiterleiten, ohne ihn zu zählen, denn den schicken Linkprüfer und Vorschaudienste.
Regeln nach Land und Gerät passen in dieselbe Abfrage. request.cf.country liefert das Land,
der User-Agent verrät, ob mobil oder Desktop, die erste passende Regel gewinnt, und das Hauptziel des
Links fängt den Rest.
Was die kleinen Antworten noch bringen. Seit Juni 2025
berichtet
Cloudflare, dass russische Provider Besuchern von Websites hinter Cloudflare nur “only the
first 16 KB of any web asset” laden lassen. Ein Zähler wie dieser schickt ein Inline-Skript
unter 600 Bytes und bekommt ein leeres 204 zurück; ein Link antwortet nur mit Headern, unter
1 KB samt denen von Cloudflare. Beides bleibt weit unter der Grenze. Gemessen wurde das von
außerhalb Russlands, an der Größe, nicht von innen. Eine Seite, die selbst größer ist als die
Grenze, rettet das nicht: Eine Seite, die nie lädt, schickt keinen Aufruf, den man zählen
könnte. Ein Kurzlink kommt besser weg, denn sein Redirect kommt vollständig an, und der
Besucher geht weiter, wo auch immer das Ziel gehostet ist.
Was es im Free-Plan kostet
Workers Free gibt einem Konto 100.000 Anfragen am Tag und 10 ms CPU pro Anfrage. D1 im Free-Plan gibt 5 Millionen gelesene und 100.000 geschriebene Zeilen am Tag, 500 MB pro Datenbank und 5 GB pro Konto. Die Kontingente setzen um 00:00 UTC zurück und gelten für alle Worker des Kontos zusammen.
Die Schreibvorgänge gehen zuerst aus. Jedes gezählte Ereignis berührt mindestens eine Zeile, mehr, sobald Sie Besucher-Hashes und stündliche Details halten, und D1 zählt jeden Indexeintrag als geschriebene Zeile. clx, der Zähler weiter unten, plant neun Schreibvorgänge für das ungünstigste Ereignis ein und setzt die garantierte Obergrenze im Free-Plan bei etwa 11.000 gezählten Ereignissen am Tag an, bei einer typischen Website ein Mehrfaches davon.
Wenn ein Kontingent ausgeht, scheitern die Teile unterschiedlich. Über dem Anfragelimit
liefert Cloudflare Error 1027
oder reicht die Anfrage an den Origin weiter, je nachdem, wie die Route für den Fehlerfall
eingestellt ist. So oder so wird nichts gezählt, und die Kurzlinks antworten nicht mehr, weil
ihr Host keinen Origin hat, auf den er ausweichen könnte. Ihre Seiten liegen nicht auf der
Route und laden weiter. Über dem Schreiblimit gilt für D1: “will return errors”, das Zählen
stoppt. Ob die Links weiterleiten, hängt vom Code ab: Das Beispiel oben wartet auf seinen
Schreibvorgang und würde mit ihm scheitern. clx schreibt einen Klick mit ctx.waitUntil(),
nachdem der Redirect gesendet ist, und verwirft einen fehlgeschlagenen Schreibvorgang, seine
Links funktionieren also weiter. Workers Paid hebt beides auf, für
mindestens $5 im Monat pro
Konto, mit 10 Millionen Anfragen und 50 Millionen D1-Schreibvorgängen inklusive.
Wo es bricht
- Es ist nicht fälschungssicher. Der Pfad steht im Seitenquelltext, und jeder kann mit
beliebigem
Refererund User-Agent einenPOSTdorthin schicken. Prüfungen gegenOriginund Host halten Browserrauschen fern, kein entschlossenes Skript. - Die Route muss frei sein. Hält schon ein anderer Worker eine Route auf diesem Muster, lehnt Cloudflare eine zweite ab. Nehmen Sie einen anderen Pfad, statt sie zu überschreiben.
- Ihr Inline-Skript braucht die Erlaubnis Ihrer CSP. Mit
script-src 'self'und ohne'unsafe-inline'liefern Sie denselben Code als Datei vom Pfad des Workers aus,<script defer src="/assets-k3/a.js">, was'self'erlaubt. Der Beacon selbst fällt unterconnect-src, eine Policy, die beidefault-src 'none'beginnt, braucht also auchconnect-src 'self'. - Browser Integrity Check gehört Ihnen. Diese Zoneneinstellung von Cloudflare kann manche skriptgesteuerten Clients auf dem Linkhost abweisen, bevor der Worker läuft. Browser kommen durch. Ihr eigenes Monitoring-Skript vielleicht nicht.
- Die eigenen Dateien von Pages hinter dem Worker sind hier ungeprüft. Ob
_redirectsund_headerseines Pages-Projekts für Anfragen gelten, die der Worker weiterreicht, wurde für diesen Leitfaden nicht getestet.
Ohne es selbst zu schreiben
Der Code oben ist ein funktionierender Anfang. Daraus einen Zähler mit Quellen, Ländern,
Geräten, Bots, Besucher-Hashes, Tagesabschlüssen, Linkregeln, QR-Codes und einem Bericht zu
machen, ist ein eigenes Projekt, und dieses Projekt ist clx. Es installiert
einen Worker namens clx-edge und eine D1-Datenbank in Ihr Cloudflare-Konto, über ein
kurzlebiges Bootstrap-Token, das Sie selbst anlegen, und behält auf seiner Seite nur Summen,
stündlich für zwei Tage und täglich für 400; die Aufschlüsselungen bleiben in Ihrem Konto. Jede Website bekommt ihren eigenen Pfad und ihr eigenes generiertes Snippet, zwei
Websites auf clx teilen also nichts in ihrem Quelltext. Links bekommen einen Host pro Website,
mit einem QR-Code für jeden Link und bis zu 10 Regeln. Der Free-Plan umfasst ein
Cloudflare-Konto, 10 Websites und 200 Links. Der Quellcode ist offen unter AGPL-3.0.
Website-Generatoren nutzen es ohne Browser. Die Management-API verbindet das Cloudflare-Konto eines Kunden einmal, legt jede Website an und liefert das Snippet, das beim Build eingebettet wird; über Neubauten hinweg bleibt es gleich. Site Generator, der Affiliate-Websites auf Pages veröffentlicht, schaltet es für Kunden ein, die beim Support danach fragen.
Wann Sie stattdessen 301.st brauchen
Für eine Website und eine Handvoll Links ist der Worker oben oder clx die ganze Antwort, und beides kostet nichts. 301.st ist für eine andere Aufgabe da: viele Domains mit Redirects nach Land und Gerät, Traffic, der zwischen Offers oder Landingpages aufgeteilt wird, Bots, die vorher herausgefiltert werden, und Postbacks, die zeigen, welcher Klick konvertiert hat. Es läuft auf dieselbe Weise in Ihrem Cloudflare-Konto, und ein Kurzlink von clx kann auf einen Flow von 301.st zeigen, wenn Sie beides brauchen.