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.

POST an /v

alles andere

Besucher öffnet eine Seite

Seite von Pages oder Ihrem Server

Inline-Skript sendet einen POST
an example.com/assets-k3/v

Worker auf der Route
example.com/assets-k3/*

D1 in Ihrem Konto

Weiter an den Origin

204, ohne Body

Ein Seitenaufruf, gezählt von einem Worker auf einem Pfad der Website

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.

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.

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 302 und cache-control: no-store antworten, 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 Quelle qr festhalten und aus dem Redirect entfernen;
  • einen GET zählen, einen HEAD weiterleiten, 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 Referer und User-Agent einen POST dorthin schicken. Prüfungen gegen Origin und 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 unter connect-src, eine Policy, die bei default-src 'none' beginnt, braucht also auch connect-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 _redirects und _headers eines 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.